client.go 8.76 KB
Newer Older
1 2 3 4
package node

import (
	"context"
5
	"errors"
6
	"fmt"
7 8 9
	"math/big"
	"time"

Hamdi Allam's avatar
Hamdi Allam committed
10 11 12
	"github.com/ethereum-optimism/optimism/op-service/client"
	"github.com/ethereum-optimism/optimism/op-service/retry"

Hamdi Allam's avatar
Hamdi Allam committed
13
	"github.com/ethereum/go-ethereum"
14
	"github.com/ethereum/go-ethereum/common"
15 16 17 18 19 20 21 22 23 24
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/rpc"
)

const (
	// defaultDialTimeout is default duration the processor will wait on
	// startup to make a connection to the backend
	defaultDialTimeout = 5 * time.Second

Hamdi Allam's avatar
Hamdi Allam committed
25 26 27 28
	// defaultDialAttempts is the default attempts a connection will be made
	// before failing
	defaultDialAttempts = 5

29 30 31 32 33 34
	// defaultRequestTimeout is the default duration the processor will
	// wait for a request to be fulfilled
	defaultRequestTimeout = 10 * time.Second
)

type EthClient interface {
35
	BlockHeaderByNumber(*big.Int) (*types.Header, error)
36
	BlockHeaderByHash(common.Hash) (*types.Header, error)
37
	BlockHeadersByRange(*big.Int, *big.Int) ([]types.Header, error)
38

Hamdi Allam's avatar
Hamdi Allam committed
39 40
	TxByHash(common.Hash) (*types.Transaction, error)

41
	StorageHash(common.Address, *big.Int) (common.Hash, error)
42
	FilterLogs(ethereum.FilterQuery) (Logs, error)
43 44 45 46

	// Close closes the underlying RPC connection.
	// RPC close does not return any errors, but does shut down e.g. a websocket connection.
	Close()
47 48
}

Hamdi Allam's avatar
Hamdi Allam committed
49
type clnt struct {
Hamdi Allam's avatar
Hamdi Allam committed
50
	rpc RPC
51 52
}

53 54
func DialEthClient(ctx context.Context, rpcUrl string, metrics Metricer) (EthClient, error) {
	ctx, cancel := context.WithTimeout(ctx, defaultDialTimeout)
55 56
	defer cancel()

Hamdi Allam's avatar
Hamdi Allam committed
57
	bOff := retry.Exponential()
58
	rpcClient, err := retry.Do(ctx, defaultDialAttempts, bOff, func() (*rpc.Client, error) {
Hamdi Allam's avatar
Hamdi Allam committed
59 60 61 62
		if !client.IsURLAvailable(rpcUrl) {
			return nil, fmt.Errorf("address unavailable (%s)", rpcUrl)
		}

63
		client, err := rpc.DialContext(ctx, rpcUrl)
Hamdi Allam's avatar
Hamdi Allam committed
64 65 66 67 68 69 70
		if err != nil {
			return nil, fmt.Errorf("failed to dial address (%s): %w", rpcUrl, err)
		}

		return client, nil
	})

71 72 73 74
	if err != nil {
		return nil, err
	}

Hamdi Allam's avatar
Hamdi Allam committed
75
	return &clnt{rpc: NewRPC(rpcClient, metrics)}, nil
76 77
}

78
// BlockHeaderByHash retrieves the block header attributed to the supplied hash
Hamdi Allam's avatar
Hamdi Allam committed
79
func (c *clnt) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
80 81 82
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

Hamdi Allam's avatar
Hamdi Allam committed
83 84
	var header *types.Header
	err := c.rpc.CallContext(ctxwt, &header, "eth_getBlockByHash", hash, false)
85 86
	if err != nil {
		return nil, err
Hamdi Allam's avatar
Hamdi Allam committed
87 88
	} else if header == nil {
		return nil, ethereum.NotFound
89 90
	}

91 92 93 94 95 96
	// sanity check on the data returned
	if header.Hash() != hash {
		return nil, errors.New("header mismatch")
	}

	return header, nil
97 98
}

99
// BlockHeaderByNumber retrieves the block header attributed to the supplied height
Hamdi Allam's avatar
Hamdi Allam committed
100
func (c *clnt) BlockHeaderByNumber(number *big.Int) (*types.Header, error) {
101 102 103
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

Hamdi Allam's avatar
Hamdi Allam committed
104 105
	var header *types.Header
	err := c.rpc.CallContext(ctxwt, &header, "eth_getBlockByNumber", toBlockNumArg(number), false)
106 107
	if err != nil {
		return nil, err
Hamdi Allam's avatar
Hamdi Allam committed
108 109
	} else if header == nil {
		return nil, ethereum.NotFound
110 111 112 113 114 115
	}

	return header, nil
}

// BlockHeadersByRange will retrieve block headers within the specified range -- inclusive. No restrictions
116 117
// are placed on the range such as blocks in the "latest", "safe" or "finalized" states. If the specified
// range is too large, `endHeight > latest`, the resulting list is truncated to the available headers
Hamdi Allam's avatar
Hamdi Allam committed
118
func (c *clnt) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]types.Header, error) {
Hamdi Allam's avatar
Hamdi Allam committed
119 120 121 122 123 124 125 126 127
	// avoid the batch call if there's no range
	if startHeight.Cmp(endHeight) == 0 {
		header, err := c.BlockHeaderByNumber(startHeight)
		if err != nil {
			return nil, err
		}
		return []types.Header{*header}, nil
	}

128
	count := new(big.Int).Sub(endHeight, startHeight).Uint64() + 1
129
	headers := make([]types.Header, count)
130
	batchElems := make([]rpc.BatchElem, count)
131

132 133
	for i := uint64(0); i < count; i++ {
		height := new(big.Int).Add(startHeight, new(big.Int).SetUint64(i))
134
		batchElems[i] = rpc.BatchElem{Method: "eth_getBlockByNumber", Args: []interface{}{toBlockNumArg(height), false}, Result: &headers[i]}
135 136 137 138
	}

	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()
Hamdi Allam's avatar
Hamdi Allam committed
139
	err := c.rpc.BatchCallContext(ctxwt, batchElems)
140 141 142 143 144 145 146 147 148 149
	if err != nil {
		return nil, err
	}

	// Parse the headers.
	//  - Ensure integrity that they build on top of each other
	//  - Truncate out headers that do not exist (endHeight > "latest")
	size := 0
	for i, batchElem := range batchElems {
		if batchElem.Error != nil {
150 151 152 153 154
			if size == 0 {
				return nil, batchElem.Error
			} else {
				break // try return whatever headers are available
			}
155 156 157 158
		} else if batchElem.Result == nil {
			break
		}

159 160
		if i > 0 && headers[i].ParentHash != headers[i-1].Hash() {
			return nil, fmt.Errorf("queried header %s does not follow parent %s", headers[i].Hash(), headers[i-1].Hash())
161 162 163 164 165
		}

		size = size + 1
	}

166
	headers = headers[:size]
167 168 169
	return headers, nil
}

Hamdi Allam's avatar
Hamdi Allam committed
170
func (c *clnt) TxByHash(hash common.Hash) (*types.Transaction, error) {
Hamdi Allam's avatar
Hamdi Allam committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

	var tx *types.Transaction
	err := c.rpc.CallContext(ctxwt, &tx, "eth_getTransactionByHash", hash)
	if err != nil {
		return nil, err
	} else if tx == nil {
		return nil, ethereum.NotFound
	}

	return tx, nil
}

185
// StorageHash returns the sha3 of the storage root for the specified account
Hamdi Allam's avatar
Hamdi Allam committed
186
func (c *clnt) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) {
187 188 189 190
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

	proof := struct{ StorageHash common.Hash }{}
Hamdi Allam's avatar
Hamdi Allam committed
191
	err := c.rpc.CallContext(ctxwt, &proof, "eth_getProof", address, nil, toBlockNumArg(blockNumber))
192 193 194 195 196 197 198
	if err != nil {
		return common.Hash{}, err
	}

	return proof.StorageHash, nil
}

199 200 201 202
func (c *clnt) Close() {
	c.rpc.Close()
}

203 204 205 206
type Logs struct {
	Logs          []types.Log
	ToBlockHeader *types.Header
}
Hamdi Allam's avatar
Hamdi Allam committed
207

208
// FilterLogs returns logs that fit the query parameters. The underlying request is a batch
Hamdi Allam's avatar
Hamdi Allam committed
209
// request including `eth_getBlockByNumber` to allow the caller to check that connected
210 211
// node has the state necessary to fulfill this request
func (c *clnt) FilterLogs(query ethereum.FilterQuery) (Logs, error) {
Hamdi Allam's avatar
Hamdi Allam committed
212 213
	arg, err := toFilterArg(query)
	if err != nil {
214
		return Logs{}, err
Hamdi Allam's avatar
Hamdi Allam committed
215 216
	}

217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
	var logs []types.Log
	var header types.Header

	batchElems := make([]rpc.BatchElem, 2)
	batchElems[0] = rpc.BatchElem{Method: "eth_getBlockByNumber", Args: []interface{}{toBlockNumArg(query.ToBlock), false}, Result: &header}
	batchElems[1] = rpc.BatchElem{Method: "eth_getLogs", Args: []interface{}{arg}, Result: &logs}

	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()
	err = c.rpc.BatchCallContext(ctxwt, batchElems)
	if err != nil {
		return Logs{}, err
	}

	if batchElems[0].Error != nil {
		return Logs{}, fmt.Errorf("unable to query for the `FilterQuery#ToBlock` header: %w", batchElems[0].Error)
	}

	if batchElems[1].Error != nil {
		return Logs{}, fmt.Errorf("unable to query logs: %w", batchElems[1].Error)
	}

	return Logs{Logs: logs, ToBlockHeader: &header}, nil
Hamdi Allam's avatar
Hamdi Allam committed
240 241
}

Sabnock01's avatar
Sabnock01 committed
242 243
// Modeled off op-service/client.go. We can refactor this once the client/metrics portion
// of op-service/client has been generalized
Hamdi Allam's avatar
Hamdi Allam committed
244 245 246 247 248 249 250 251

type RPC interface {
	Close()
	CallContext(ctx context.Context, result any, method string, args ...any) error
	BatchCallContext(ctx context.Context, b []rpc.BatchElem) error
}

type rpcClient struct {
Hamdi Allam's avatar
Hamdi Allam committed
252
	rpc     *rpc.Client
Hamdi Allam's avatar
Hamdi Allam committed
253 254 255
	metrics Metricer
}

256
func NewRPC(client *rpc.Client, metrics Metricer) RPC {
Hamdi Allam's avatar
Hamdi Allam committed
257 258 259 260
	return &rpcClient{client, metrics}
}

func (c *rpcClient) Close() {
Hamdi Allam's avatar
Hamdi Allam committed
261
	c.rpc.Close()
Hamdi Allam's avatar
Hamdi Allam committed
262 263 264 265
}

func (c *rpcClient) CallContext(ctx context.Context, result any, method string, args ...any) error {
	record := c.metrics.RecordRPCClientRequest(method)
Hamdi Allam's avatar
Hamdi Allam committed
266
	err := c.rpc.CallContext(ctx, result, method, args...)
Hamdi Allam's avatar
Hamdi Allam committed
267 268 269 270 271 272
	record(err)
	return err
}

func (c *rpcClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error {
	record := c.metrics.RecordRPCClientBatchRequest(b)
Hamdi Allam's avatar
Hamdi Allam committed
273
	err := c.rpc.BatchCallContext(ctx, b)
Hamdi Allam's avatar
Hamdi Allam committed
274 275 276 277 278 279
	record(err)
	return err
}

// Needed private utils from geth

280 281 282
func toBlockNumArg(number *big.Int) string {
	if number == nil {
		return "latest"
Hamdi Allam's avatar
Hamdi Allam committed
283 284
	}
	if number.Sign() >= 0 {
285
		return hexutil.EncodeBig(number)
286
	}
287
	// It's negative.
Hamdi Allam's avatar
Hamdi Allam committed
288
	return rpc.BlockNumber(number.Int64()).String()
289
}
Hamdi Allam's avatar
Hamdi Allam committed
290 291

func toFilterArg(q ethereum.FilterQuery) (interface{}, error) {
292
	arg := map[string]interface{}{"address": q.Addresses, "topics": q.Topics}
Hamdi Allam's avatar
Hamdi Allam committed
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
	if q.BlockHash != nil {
		arg["blockHash"] = *q.BlockHash
		if q.FromBlock != nil || q.ToBlock != nil {
			return nil, errors.New("cannot specify both BlockHash and FromBlock/ToBlock")
		}
	} else {
		if q.FromBlock == nil {
			arg["fromBlock"] = "0x0"
		} else {
			arg["fromBlock"] = toBlockNumArg(q.FromBlock)
		}
		arg["toBlock"] = toBlockNumArg(q.ToBlock)
	}
	return arg, nil
}