From fb5c6c7ec1ad15b438757d988f35179ed6d6dc12 Mon Sep 17 00:00:00 2001 From: Peter Kieltyka Date: Tue, 22 Oct 2024 20:25:33 -0400 Subject: [PATCH] ethrpc: strictness validation level when unmarshalling blocks and transactions --- cmd/chain-watch/main.go | 43 ++++++++-- ethmonitor/ethmonitor.go | 30 +++++-- ethrpc/ethrpc.go | 84 ++++++++++-------- ethrpc/ethrpc_test.go | 3 +- ethrpc/filter.go | 6 +- ethrpc/interface.go | 4 + ethrpc/jsonrpc.go | 58 ++++++++----- ethrpc/methods.go | 12 +-- ethrpc/option.go | 21 +++++ ethrpc/strictness.go | 21 +++++ ethrpc/syncing.go | 2 +- ethrpc/{block.go => unmarshal.go} | 100 +++++++++++++--------- go-ethereum/core/types/block.go | 39 +++++++-- go-ethereum/core/types/gen_header_json.go | 10 ++- 14 files changed, 300 insertions(+), 133 deletions(-) create mode 100644 ethrpc/strictness.go rename ethrpc/{block.go => unmarshal.go} (73%) diff --git a/cmd/chain-watch/main.go b/cmd/chain-watch/main.go index 5b688f71..9e5be50a 100644 --- a/cmd/chain-watch/main.go +++ b/cmd/chain-watch/main.go @@ -44,6 +44,11 @@ func init() { ETH_NODE_URL = testConfig["ARB_NOVA_URL"] ETH_NODE_WSS_URL = testConfig["ARB_NOVA_WSS_URL"] } + + if testConfig["ETHERLINK_MAINNET_URL"] != "" { + ETH_NODE_URL = testConfig["ETHERLINK_MAINNET_URL"] + ETH_NODE_WSS_URL = testConfig["ETHERLINK_MAINNET_WSS_URL"] + } } func main() { @@ -51,11 +56,38 @@ func main() { // Provider // provider, err := ethrpc.NewProvider(ETH_NODE_URL) - provider, err := ethrpc.NewProvider(ETH_NODE_URL, ethrpc.WithStreaming(ETH_NODE_WSS_URL)) + provider, err := ethrpc.NewProvider(ETH_NODE_URL, ethrpc.WithStreaming(ETH_NODE_WSS_URL), ethrpc.WithNoValidation()) + if err != nil { + log.Fatal(err) + } + + blk, err := provider.BlockByNumber(context.Background(), big.NewInt(3750259)) + if err != nil { + log.Fatal(err) + } + + latest, err := provider.HeaderByNumber(context.Background(), big.NewInt(3750259)) if err != nil { log.Fatal(err) } + fmt.Println("expected block hash: 0x84466d04eec7eb4f6de99e6ac5db9484c042ca7ad5148a61598165edcf93cb1c") + fmt.Println("blk num:", blk.Number().String(), "hash:", blk.Hash().Hex()) + + hh := blk.Header() + fmt.Println("???", hh.Hash().Hex()) + fmt.Println("latest num:", latest.Number.String(), "hash:", latest.Hash().Hex()) + + byNum, err := provider.HeaderByHash(context.Background(), latest.Hash()) + if err != nil { + log.Fatal(err) + } + + _ = byNum + // spew.Dump(byNum) + + // return + chainID, _ := provider.ChainID(context.Background()) fmt.Println("=> chain id:", chainID.String()) @@ -66,15 +98,16 @@ func main() { monitorOptions.WithLogs = true monitorOptions.BlockRetentionLimit = 64 monitorOptions.StreamingRetryAfter = 1 * time.Minute - // monitorOptions.StartBlockNumber = nil // track the head + monitorOptions.StartBlockNumber = nil // track the head latestBlock, err := provider.BlockByNumber(context.Background(), nil) if err != nil { - panic(err) + log.Fatal(err) } + _ = latestBlock - monitorOptions.StartBlockNumber = big.NewInt(0).Sub(latestBlock.Number(), big.NewInt(10)) - // monitorOptions.StartBlockNumber = big.NewInt(47496451) + // monitorOptions.StartBlockNumber = big.NewInt(0).Sub(latestBlock.Number(), big.NewInt(10)) + monitorOptions.StartBlockNumber = big.NewInt(3754824) // monitorOptions.Bootstrap = true monitorOptions.Logger = logger.NewLogger(logger.LogLevel_DEBUG) diff --git a/ethmonitor/ethmonitor.go b/ethmonitor/ethmonitor.go index 14045c48..07692669 100644 --- a/ethmonitor/ethmonitor.go +++ b/ethmonitor/ethmonitor.go @@ -158,6 +158,11 @@ func NewMonitor(provider ethrpc.RawInterface, options ...Options) (*Monitor, err } opts.BlockRetentionLimit += opts.TrailNumBlocksBehindHead + if opts.BlockRetentionLimit < 2 { + // minimum 2 blocks to track, as we need the previous + // block to verify the current block + opts.BlockRetentionLimit = 2 + } if opts.DebugLogging { stdLogger, ok := opts.Logger.(*logger.StdLogAdapter) @@ -679,7 +684,7 @@ func (m *Monitor) filterLogs(ctx context.Context, blockHash common.Hash, topics if err != nil { return nil, resp, err } - logs, err := unmarshalLogs(resp) + logs, err := m.unmarshalLogs(resp) return logs, resp, err } @@ -696,7 +701,7 @@ func (m *Monitor) filterLogs(ctx context.Context, blockHash common.Hash, topics if err != nil { return nil, resp, err } - logs, err := unmarshalLogs(resp) + logs, err := m.unmarshalLogs(resp) return logs, resp, err } @@ -786,7 +791,7 @@ func (m *Monitor) fetchNextBlock(ctx context.Context) (*types.Block, []byte, boo if err != nil { return nil, resp, miss, err } - block, err := unmarshalBlock(resp) + block, err := m.unmarshalBlock(resp) return block, resp, miss, err } @@ -796,7 +801,7 @@ func (m *Monitor) fetchNextBlock(ctx context.Context) (*types.Block, []byte, boo if err != nil { return nil, resp, miss, err } - block, err := unmarshalBlock(resp) + block, err := m.unmarshalBlock(resp) return block, resp, miss, err } @@ -890,7 +895,7 @@ func (m *Monitor) fetchBlockByHash(ctx context.Context, hash common.Hash) (*type if err != nil { return nil, nil, err } - block, err := unmarshalBlock(resp) + block, err := m.unmarshalBlock(resp) return block, nil, err } @@ -900,7 +905,7 @@ func (m *Monitor) fetchBlockByHash(ctx context.Context, hash common.Hash) (*type if err != nil { return nil, nil, err } - block, err := unmarshalBlock(resp) + block, err := m.unmarshalBlock(resp) return block, resp, err } @@ -1131,16 +1136,23 @@ func clampDuration(x, y time.Duration) time.Duration { } } -func unmarshalBlock(blockPayload []byte) (*types.Block, error) { +func (m *Monitor) unmarshalBlock(blockPayload []byte) (*types.Block, error) { var block *types.Block - err := ethrpc.IntoBlock(blockPayload, &block) + + getStrictnessLevel, ok := m.provider.(ethrpc.StrictnessLevelGetter) + if !ok { + return nil, fmt.Errorf("provider does not support strictness level") + } + strictness := getStrictnessLevel.StrictnessLevel() + + err := ethrpc.IntoBlock(blockPayload, &block, strictness) if err != nil { return nil, err } return block, nil } -func unmarshalLogs(logsPayload []byte) ([]types.Log, error) { +func (m *Monitor) unmarshalLogs(logsPayload []byte) ([]types.Log, error) { var logs []types.Log err := json.Unmarshal(logsPayload, &logs) if err != nil { diff --git a/ethrpc/ethrpc.go b/ethrpc/ethrpc.go index 17704619..3660341e 100644 --- a/ethrpc/ethrpc.go +++ b/ethrpc/ethrpc.go @@ -32,6 +32,7 @@ type Provider struct { jwtToken string // optional streamClosers []StreamCloser streamUnsubscribers []StreamUnsubscriber + strictness StrictnessLevel chainID *big.Int chainIDMu sync.Mutex @@ -65,6 +66,7 @@ var ( var _ Interface = &Provider{} var _ RawInterface = &Provider{} +var _ StrictnessLevelGetter = &Provider{} // Provider adheres to the go-ethereum bind.ContractBackend interface. In case we ever // want to break this interface, we could also write an adapter type to keep them compat. @@ -82,6 +84,10 @@ func (s *Provider) SetHTTPClient(httpClient *http.Client) { s.httpClient = httpClient } +func (p *Provider) StrictnessLevel() StrictnessLevel { + return p.strictness +} + func (p *Provider) Do(ctx context.Context, calls ...Call) ([]byte, error) { if len(calls) == 0 { return nil, nil @@ -104,6 +110,8 @@ func (p *Provider) Do(ctx context.Context, calls ...Call) ([]byte, error) { return nil, superr.Wrap(ErrRequestFail, fmt.Errorf("failed to marshal JSONRPC request: %w", err)) } + fmt.Println("=> batch:", string(b)) + req, err := http.NewRequest(http.MethodPost, p.nodeURL, bytes.NewBuffer(b)) if err != nil { return nil, superr.Wrap(ErrRequestFail, fmt.Errorf("failed to initialize http.Request: %w", err)) @@ -187,7 +195,7 @@ func (p *Provider) ChainID(ctx context.Context) (*big.Int, error) { } var ret *big.Int - _, err := p.Do(ctx, ChainID().Into(&ret)) + _, err := p.Do(ctx, ChainID().Strict(p.strictness).Into(&ret)) if err != nil { return nil, err } @@ -198,13 +206,13 @@ func (p *Provider) ChainID(ctx context.Context) (*big.Int, error) { func (p *Provider) BlockNumber(ctx context.Context) (uint64, error) { var ret uint64 - _, err := p.Do(ctx, BlockNumber().Into(&ret)) + _, err := p.Do(ctx, BlockNumber().Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) BalanceAt(ctx context.Context, account common.Address, blockNum *big.Int) (*big.Int, error) { var ret *big.Int - _, err := p.Do(ctx, BalanceAt(account, blockNum).Into(&ret)) + _, err := p.Do(ctx, BalanceAt(account, blockNum).Strict(p.strictness).Into(&ret)) return ret, err } @@ -213,15 +221,15 @@ func (p *Provider) SendTransaction(ctx context.Context, tx *types.Transaction) e return err } -func (s *Provider) SendRawTransaction(ctx context.Context, signedTxHex string) (common.Hash, error) { +func (p *Provider) SendRawTransaction(ctx context.Context, signedTxHex string) (common.Hash, error) { var txnHash common.Hash - _, err := s.Do(ctx, SendRawTransaction(signedTxHex).Into(&txnHash)) + _, err := p.Do(ctx, SendRawTransaction(signedTxHex).Strict(p.strictness).Into(&txnHash)) return txnHash, err } func (p *Provider) RawBlockByHash(ctx context.Context, hash common.Hash) (json.RawMessage, error) { var result json.RawMessage - _, err := p.Do(ctx, RawBlockByHash(hash).Into(&result)) + _, err := p.Do(ctx, RawBlockByHash(hash).Strict(p.strictness).Into(&result)) if err != nil { return nil, err } @@ -233,13 +241,13 @@ func (p *Provider) RawBlockByHash(ctx context.Context, hash common.Hash) (json.R func (p *Provider) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { var ret *types.Block - _, err := p.Do(ctx, BlockByHash(hash).Into(&ret)) + _, err := p.Do(ctx, BlockByHash(hash).Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) RawBlockByNumber(ctx context.Context, blockNum *big.Int) (json.RawMessage, error) { var result json.RawMessage - _, err := p.Do(ctx, RawBlockByNumber(blockNum).Into(&result)) + _, err := p.Do(ctx, RawBlockByNumber(blockNum).Strict(p.strictness).Into(&result)) if err != nil { return nil, err } @@ -251,19 +259,19 @@ func (p *Provider) RawBlockByNumber(ctx context.Context, blockNum *big.Int) (jso func (p *Provider) BlockByNumber(ctx context.Context, blockNum *big.Int) (*types.Block, error) { var ret *types.Block - _, err := p.Do(ctx, BlockByNumber(blockNum).Into(&ret)) + _, err := p.Do(ctx, BlockByNumber(blockNum).Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) PeerCount(ctx context.Context) (uint64, error) { var ret uint64 - _, err := p.Do(ctx, PeerCount().Into(&ret)) + _, err := p.Do(ctx, PeerCount().Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { var head *types.Header - _, err := p.Do(ctx, HeaderByHash(hash).Into(&head)) + _, err := p.Do(ctx, HeaderByHash(hash).Strict(p.strictness).Into(&head)) if err == nil && head == nil { return nil, ethereum.NotFound } @@ -272,7 +280,7 @@ func (p *Provider) HeaderByHash(ctx context.Context, hash common.Hash) (*types.H func (p *Provider) HeaderByNumber(ctx context.Context, blockNum *big.Int) (*types.Header, error) { var head *types.Header - _, err := p.Do(ctx, HeaderByNumber(blockNum).Into(&head)) + _, err := p.Do(ctx, HeaderByNumber(blockNum).Strict(p.strictness).Into(&head)) if err == nil && head == nil { return nil, ethereum.NotFound } @@ -284,7 +292,7 @@ func (p *Provider) HeadersByNumbers(ctx context.Context, blockNumbers []*big.Int var calls []Call for index, blockNum := range blockNumbers { - calls = append(calls, HeaderByNumber(blockNum).Into(&headers[index])) + calls = append(calls, HeaderByNumber(blockNum).Strict(p.strictness).Into(&headers[index])) } _, err := p.Do(ctx, calls...) @@ -300,7 +308,7 @@ func (p *Provider) HeadersByNumberRange(ctx context.Context, fromBlockNumber, to } func (p *Provider) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, pending bool, err error) { - _, err = p.Do(ctx, TransactionByHash(hash).Into(&tx, &pending)) + _, err = p.Do(ctx, TransactionByHash(hash).Strict(p.strictness).Into(&tx, &pending)) if err == nil && tx == nil { return nil, false, ethereum.NotFound } @@ -312,19 +320,19 @@ func (p *Provider) TransactionSender(ctx context.Context, tx *types.Transaction, if err != nil { return sender, nil } - _, err = p.Do(ctx, TransactionSender(tx, block, index).Into(&sender)) + _, err = p.Do(ctx, TransactionSender(tx, block, index).Strict(p.strictness).Into(&sender)) return sender, err } func (p *Provider) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { var ret uint - _, err := p.Do(ctx, TransactionCount(blockHash).Into(&ret)) + _, err := p.Do(ctx, TransactionCount(blockHash).Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { var tx *types.Transaction - _, err := p.Do(ctx, TransactionInBlock(blockHash, index).Into(&tx)) + _, err := p.Do(ctx, TransactionInBlock(blockHash, index).Strict(p.strictness).Into(&tx)) if err == nil && tx == nil { return nil, ethereum.NotFound } @@ -333,7 +341,7 @@ func (p *Provider) TransactionInBlock(ctx context.Context, blockHash common.Hash func (p *Provider) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { var receipt *types.Receipt - _, err := p.Do(ctx, TransactionReceipt(txHash).Into(&receipt)) + _, err := p.Do(ctx, TransactionReceipt(txHash).Strict(p.strictness).Into(&receipt)) if err == nil && receipt == nil { return nil, ethereum.NotFound } @@ -342,37 +350,37 @@ func (p *Provider) TransactionReceipt(ctx context.Context, txHash common.Hash) ( func (p *Provider) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) { var progress *ethereum.SyncProgress - _, err := p.Do(ctx, SyncProgress().Into(&progress)) + _, err := p.Do(ctx, SyncProgress().Strict(p.strictness).Into(&progress)) return progress, err } func (p *Provider) NetworkID(ctx context.Context) (*big.Int, error) { var version *big.Int - _, err := p.Do(ctx, NetworkID().Into(&version)) + _, err := p.Do(ctx, NetworkID().Strict(p.strictness).Into(&version)) return version, err } func (p *Provider) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNum *big.Int) ([]byte, error) { var result []byte - _, err := p.Do(ctx, StorageAt(account, key, blockNum).Into(&result)) + _, err := p.Do(ctx, StorageAt(account, key, blockNum).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) CodeAt(ctx context.Context, account common.Address, blockNum *big.Int) ([]byte, error) { var result []byte - _, err := p.Do(ctx, CodeAt(account, blockNum).Into(&result)) + _, err := p.Do(ctx, CodeAt(account, blockNum).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) NonceAt(ctx context.Context, account common.Address, blockNum *big.Int) (uint64, error) { var result uint64 - _, err := p.Do(ctx, NonceAt(account, blockNum).Into(&result)) + _, err := p.Do(ctx, NonceAt(account, blockNum).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) RawFilterLogs(ctx context.Context, q ethereum.FilterQuery) (json.RawMessage, error) { var result json.RawMessage - _, err := p.Do(ctx, RawFilterLogs(q).Into(&result)) + _, err := p.Do(ctx, RawFilterLogs(q).Strict(p.strictness).Into(&result)) if err != nil { return nil, err } @@ -381,79 +389,79 @@ func (p *Provider) RawFilterLogs(ctx context.Context, q ethereum.FilterQuery) (j func (p *Provider) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { var logs []types.Log - _, err := p.Do(ctx, FilterLogs(q).Into(&logs)) + _, err := p.Do(ctx, FilterLogs(q).Strict(p.strictness).Into(&logs)) return logs, err } func (p *Provider) PendingBalanceAt(ctx context.Context, account common.Address) (*big.Int, error) { var ret *big.Int - _, err := p.Do(ctx, PendingBalanceAt(account).Into(&ret)) + _, err := p.Do(ctx, PendingBalanceAt(account).Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) PendingStorageAt(ctx context.Context, account common.Address, key common.Hash) ([]byte, error) { var result []byte - _, err := p.Do(ctx, PendingStorageAt(account, key).Into(&result)) + _, err := p.Do(ctx, PendingStorageAt(account, key).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { var result []byte - _, err := p.Do(ctx, PendingCodeAt(account).Into(&result)) + _, err := p.Do(ctx, PendingCodeAt(account).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { var result uint64 - _, err := p.Do(ctx, PendingNonceAt(account).Into(&result)) + _, err := p.Do(ctx, PendingNonceAt(account).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) PendingTransactionCount(ctx context.Context) (uint, error) { var ret uint - _, err := p.Do(ctx, PendingTransactionCount().Into(&ret)) + _, err := p.Do(ctx, PendingTransactionCount().Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNum *big.Int) ([]byte, error) { var result []byte - _, err := p.Do(ctx, CallContract(msg, blockNum).Into(&result)) + _, err := p.Do(ctx, CallContract(msg, blockNum).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) { var result []byte - _, err := p.Do(ctx, CallContractAtHash(msg, blockHash).Into(&result)) + _, err := p.Do(ctx, CallContractAtHash(msg, blockHash).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { var result []byte - _, err := p.Do(ctx, PendingCallContract(msg).Into(&result)) + _, err := p.Do(ctx, PendingCallContract(msg).Strict(p.strictness).Into(&result)) return result, err } func (p *Provider) SuggestGasPrice(ctx context.Context) (*big.Int, error) { var ret *big.Int - _, err := p.Do(ctx, SuggestGasPrice().Into(&ret)) + _, err := p.Do(ctx, SuggestGasPrice().Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { var ret *big.Int - _, err := p.Do(ctx, SuggestGasTipCap().Into(&ret)) + _, err := p.Do(ctx, SuggestGasTipCap().Strict(p.strictness).Into(&ret)) return ret, err } func (p *Provider) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error) { var fh *ethereum.FeeHistory - _, err := p.Do(ctx, FeeHistory(blockCount, lastBlock, rewardPercentiles).Into(&fh)) + _, err := p.Do(ctx, FeeHistory(blockCount, lastBlock, rewardPercentiles).Strict(p.strictness).Into(&fh)) return fh, err } func (p *Provider) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { var result uint64 - _, err := p.Do(ctx, EstimateGas(msg).Into(&result)) + _, err := p.Do(ctx, EstimateGas(msg).Strict(p.strictness).Into(&result)) return result, err } @@ -575,6 +583,6 @@ func (p *Provider) contractQuery(ctx context.Context, contractAddress string, in } var result []string - _, err = p.Do(ctx, contractQueryBuilder.Into(&result)) + _, err = p.Do(ctx, contractQueryBuilder.Strict(p.strictness).Into(&result)) return result, err } diff --git a/ethrpc/ethrpc_test.go b/ethrpc/ethrpc_test.go index 2d58002a..774878f1 100644 --- a/ethrpc/ethrpc_test.go +++ b/ethrpc/ethrpc_test.go @@ -142,6 +142,7 @@ func ExampleBatchCall() { header *types.Header errBlock *types.Block ) + // strict := true _, err = p.Do( context.Background(), ethrpc.ChainID().Into(&chainID), @@ -209,7 +210,7 @@ func TestRaw(t *testing.T) { require.NotEmpty(t, payload) var block *types.Block - err = ethrpc.IntoBlock(payload, &block) + err = ethrpc.IntoBlock(payload, &block, 1) require.NoError(t, err) require.Equal(t, uint64(38470000), block.NumberU64()) } diff --git a/ethrpc/filter.go b/ethrpc/filter.go index da64fd4a..414db564 100644 --- a/ethrpc/filter.go +++ b/ethrpc/filter.go @@ -8,8 +8,10 @@ import ( func toFilterArg(q ethereum.FilterQuery) (interface{}, error) { arg := map[string]interface{}{ - "address": q.Addresses, - "topics": q.Topics, + "topics": q.Topics, + } + if len(q.Addresses) > 0 { + arg["address"] = q.Addresses } if q.BlockHash != nil { arg["blockHash"] = *q.BlockHash diff --git a/ethrpc/interface.go b/ethrpc/interface.go index c266e069..5f1fa80f 100644 --- a/ethrpc/interface.go +++ b/ethrpc/interface.go @@ -193,3 +193,7 @@ type RawInterface interface { RawBlockByNumber(ctx context.Context, blockNum *big.Int) (json.RawMessage, error) RawFilterLogs(ctx context.Context, q ethereum.FilterQuery) (json.RawMessage, error) } + +type StrictnessLevelGetter interface { + StrictnessLevel() StrictnessLevel +} diff --git a/ethrpc/jsonrpc.go b/ethrpc/jsonrpc.go index bb708f11..acba634a 100644 --- a/ethrpc/jsonrpc.go +++ b/ethrpc/jsonrpc.go @@ -11,16 +11,22 @@ import ( ) type Call struct { - request jsonrpc.Message - response *jsonrpc.Message - resultFn func(message json.RawMessage) error - err error + request jsonrpc.Message + response *jsonrpc.Message + resultFn func(message json.RawMessage) error + err error + strictness StrictnessLevel } func NewCall(method string, params ...any) Call { return NewCallBuilder[any](method, nil, params...).Into(nil) } +func (c Call) Strict(strictness StrictnessLevel) Call { + c.strictness = strictness + return c +} + func (c *Call) Error() string { if c == nil || c.err == nil { return "" @@ -32,13 +38,14 @@ func (c *Call) Unwrap() error { return c.err } -type IntoFn[T any] func(raw json.RawMessage, ret *T) error +type IntoFn[T any] func(raw json.RawMessage, ret *T, strictness StrictnessLevel) error type CallBuilder[T any] struct { - err error - method string - params []any - intoFn IntoFn[T] + err error + method string + params []any + intoFn IntoFn[T] + strictness StrictnessLevel } func NewCallBuilder[T any](method string, intoFn IntoFn[T], params ...any) CallBuilder[T] { @@ -49,6 +56,11 @@ func NewCallBuilder[T any](method string, intoFn IntoFn[T], params ...any) CallB } } +func (b CallBuilder[T]) Strict(strictness StrictnessLevel) CallBuilder[T] { + b.strictness = strictness + return b +} + func (b CallBuilder[T]) Into(ret *T) Call { if b.err != nil { return Call{err: b.err} @@ -60,7 +72,7 @@ func (b CallBuilder[T]) Into(ret *T) Call { return nil } if b.intoFn != nil { - return b.intoFn(message, ret) + return b.intoFn(message, ret, b.strictness) } return json.Unmarshal(message, ret) }, @@ -68,9 +80,15 @@ func (b CallBuilder[T]) Into(ret *T) Call { } type CallBuilder2[T1, T2 any] struct { - method string - params []any - intoFn func(message json.RawMessage, ret1 *T1, ret2 *T2) error + method string + params []any + intoFn func(message json.RawMessage, ret1 *T1, ret2 *T2, strictness StrictnessLevel) error + strictness StrictnessLevel +} + +func (b CallBuilder2[T1, T2]) Strict(strictness StrictnessLevel) CallBuilder2[T1, T2] { + b.strictness = strictness + return b } func (b CallBuilder2[T1, T2]) Into(ret1 *T1, ret2 *T2) Call { @@ -80,7 +98,7 @@ func (b CallBuilder2[T1, T2]) Into(ret1 *T1, ret2 *T2) Call { if b.intoFn == nil { panic("CallBuilder2 must have a non-nil intoFn") } - return b.intoFn(message, ret1, ret2) + return b.intoFn(message, ret1, ret2, b.strictness) }, } } @@ -117,7 +135,7 @@ func toCallArg(msg ethereum.CallMsg) any { return arg } -func hexIntoBigInt(message json.RawMessage, ret **big.Int) error { +func hexIntoBigInt(message json.RawMessage, ret **big.Int, strictness StrictnessLevel) error { var result hexutil.Big if err := json.Unmarshal(message, &result); err != nil { return err @@ -126,7 +144,7 @@ func hexIntoBigInt(message json.RawMessage, ret **big.Int) error { return nil } -func hexIntoUint64(message json.RawMessage, ret *uint64) error { +func hexIntoUint64(message json.RawMessage, ret *uint64, strictness StrictnessLevel) error { if len(message) == 4 && string(message) == "null" { *ret = 0 return nil @@ -140,7 +158,7 @@ func hexIntoUint64(message json.RawMessage, ret *uint64) error { return nil } -func hexIntoUint(message json.RawMessage, ret *uint) error { +func hexIntoUint(message json.RawMessage, ret *uint, strictness StrictnessLevel) error { if len(message) == 4 && string(message) == "null" { *ret = 0 return nil @@ -154,7 +172,7 @@ func hexIntoUint(message json.RawMessage, ret *uint) error { return nil } -func hexIntoBigUint64(message json.RawMessage, ret **big.Int) error { +func hexIntoBigUint64(message json.RawMessage, ret **big.Int, strictness StrictnessLevel) error { var result hexutil.Uint64 if err := json.Unmarshal(message, &result); err != nil { return err @@ -163,7 +181,7 @@ func hexIntoBigUint64(message json.RawMessage, ret **big.Int) error { return nil } -func hexIntoBytes(message json.RawMessage, ret *[]byte) error { +func hexIntoBytes(message json.RawMessage, ret *[]byte, strictness StrictnessLevel) error { var result hexutil.Bytes if err := json.Unmarshal(message, &result); err != nil { return err @@ -172,7 +190,7 @@ func hexIntoBytes(message json.RawMessage, ret *[]byte) error { return nil } -func hexIntoHash(message json.RawMessage, ret *common.Hash) error { +func hexIntoHash(message json.RawMessage, ret *common.Hash, strictness StrictnessLevel) error { var result common.Hash if err := json.Unmarshal(message, &result); err != nil { return err diff --git a/ethrpc/methods.go b/ethrpc/methods.go index 7cc27e50..a1fa3c7b 100644 --- a/ethrpc/methods.go +++ b/ethrpc/methods.go @@ -101,6 +101,7 @@ func HeaderByHash(hash common.Hash) CallBuilder[*types.Header] { return CallBuilder[*types.Header]{ method: "eth_getBlockByHash", params: []any{hash, false}, + intoFn: IntoHeader, } } @@ -108,6 +109,7 @@ func HeaderByNumber(blockNum *big.Int) CallBuilder[*types.Header] { return CallBuilder[*types.Header]{ method: "eth_getBlockByNumber", params: []any{toBlockNumArg(blockNum), false}, + intoFn: IntoHeader, } } @@ -123,7 +125,7 @@ func TransactionSender(tx *types.Transaction, block common.Hash, index uint) Cal return CallBuilder[common.Address]{ method: "eth_getTransactionByBlockHashAndIndex", params: []any{block, hexutil.Uint64(index)}, - intoFn: func(raw json.RawMessage, ret *common.Address) error { + intoFn: func(raw json.RawMessage, ret *common.Address, strictness StrictnessLevel) error { var meta struct { Hash common.Hash From common.Address @@ -160,7 +162,7 @@ func TransactionReceipt(txHash common.Hash) CallBuilder[*types.Receipt] { return CallBuilder[*types.Receipt]{ method: "eth_getTransactionReceipt", params: []any{txHash}, - intoFn: func(raw json.RawMessage, receipt **types.Receipt) error { + intoFn: func(raw json.RawMessage, receipt **types.Receipt, strictness StrictnessLevel) error { err := json.Unmarshal(raw, receipt) if err == nil && receipt == nil { return ethereum.NotFound @@ -180,7 +182,7 @@ func SyncProgress() CallBuilder[*ethereum.SyncProgress] { func NetworkID() CallBuilder[*big.Int] { return CallBuilder[*big.Int]{ method: "net_version", - intoFn: func(raw json.RawMessage, ret **big.Int) error { + intoFn: func(raw json.RawMessage, ret **big.Int, strictness StrictnessLevel) error { var ( verString string version = &big.Int{} @@ -316,7 +318,7 @@ func ContractQuery(contractAddress common.Address, inputAbiExpr, outputAbiExpr s return CallBuilder[[]string]{ method: "eth_call", params: []any{toCallArg(msg), toBlockNumArg(nil)}, - intoFn: func(message json.RawMessage, ret *[]string) error { + intoFn: func(message json.RawMessage, ret *[]string, strictness StrictnessLevel) error { var result hexutil.Bytes if err := json.Unmarshal(message, &result); err != nil { return err @@ -381,7 +383,7 @@ func FeeHistory(blockCount uint64, lastBlock *big.Int, rewardPercentiles []float return CallBuilder[*ethereum.FeeHistory]{ method: "eth_feeHistory", params: []any{hexutil.Uint(blockCount), toBlockNumArg(lastBlock), rewardPercentiles}, - intoFn: func(raw json.RawMessage, ret **ethereum.FeeHistory) error { + intoFn: func(raw json.RawMessage, ret **ethereum.FeeHistory, strictness StrictnessLevel) error { var res feeHistoryResult if err := json.Unmarshal(raw, &res); err != nil { return err diff --git a/ethrpc/option.go b/ethrpc/option.go index 844be125..f12cdfb7 100644 --- a/ethrpc/option.go +++ b/ethrpc/option.go @@ -52,3 +52,24 @@ func WithJWTAuthorization(jwtToken string) Option { p.jwtToken = jwtToken } } + +// 0: strict transactions – validates only transaction V, R, S values (default) +// 1: disabled, no validation +// 2: strict block and transactions – validates block hash, sender address, and transaction signatures +func WithStrictnessLevel(strictness StrictnessLevel) Option { + return func(p *Provider) { + p.strictness = strictness + } +} + +func WithNoValidation() Option { + return func(p *Provider) { + p.strictness = StrictnessLevel_Disabled + } +} + +func WithStrictValidation() Option { + return func(p *Provider) { + p.strictness = StrictnessLevel_Strict + } +} diff --git a/ethrpc/strictness.go b/ethrpc/strictness.go new file mode 100644 index 00000000..95a3a47e --- /dev/null +++ b/ethrpc/strictness.go @@ -0,0 +1,21 @@ +package ethrpc + +// StrictnessLevel is the level of strictness for validation when unmarshalling +// blocks and transactions from RPC responses from a node. +type StrictnessLevel uint8 + +const ( + StrictnessLevel_Default StrictnessLevel = iota // 0: semi-strict transactions – validates only transaction V, R, S values (default) + StrictnessLevel_Disabled // 1: disabled, no validation on blocks or transactions + StrictnessLevel_Strict // 2: strict block and transactions – validates block hash, sender address, and transaction signatures +) + +var StrictnessLevels = map[uint8]string{ + 0: "DEFAULT", + 1: "NONE", + 2: "STRICT", +} + +func (x StrictnessLevel) String() string { + return StrictnessLevels[uint8(x)] +} diff --git a/ethrpc/syncing.go b/ethrpc/syncing.go index 533f807b..9a8070d4 100644 --- a/ethrpc/syncing.go +++ b/ethrpc/syncing.go @@ -7,7 +7,7 @@ import ( "github.com/0xsequence/ethkit/go-ethereum/common/hexutil" ) -func intoSyncingProgress(raw json.RawMessage, ret **ethereum.SyncProgress) error { +func intoSyncingProgress(raw json.RawMessage, ret **ethereum.SyncProgress, strictness StrictnessLevel) error { var ( syncing bool p *rpcProgress diff --git a/ethrpc/block.go b/ethrpc/unmarshal.go similarity index 73% rename from ethrpc/block.go rename to ethrpc/unmarshal.go index ff04d1b6..fd7cc0d5 100644 --- a/ethrpc/block.go +++ b/ethrpc/unmarshal.go @@ -12,8 +12,16 @@ import ( "github.com/0xsequence/ethkit/go-ethereum/core/types" ) +type rpcBlock struct { + Hash common.Hash `json:"hash"` + Transactions []rpcTransaction `json:"transactions"` + UncleHashes []common.Hash `json:"uncles"` + Withdrawals types.Withdrawals `json:"withdrawals"` +} + type rpcTransaction struct { - tx *types.Transaction + tx *types.Transaction + txVRSValid bool txExtraInfo } @@ -24,30 +32,52 @@ type txExtraInfo struct { TxType string `json:"type,omitempty"` } -type rpcBlock struct { - Hash common.Hash `json:"hash"` - Transactions []rpcTransaction `json:"transactions"` - UncleHashes []common.Hash `json:"uncles"` - Withdrawals types.Withdrawals `json:"withdrawals"` -} - func (tx *rpcTransaction) UnmarshalJSON(msg []byte) error { - if err := json.Unmarshal(msg, &tx.tx); err != nil { + err := json.Unmarshal(msg, &tx.tx) + if err != nil { // for unsupported txn types, we don't completely fail, // ie. some chains like arbitrum nova will return a non-standard type - if err != types.ErrTxTypeNotSupported { + // + // as well, we ignore ErrInvalidSig, but if strictness is enabled, + // then we check it in the caller. + if err != types.ErrTxTypeNotSupported && err != types.ErrInvalidSig { return err } + + // we set internal flag to check if txn has valid VRS signature + if err == types.ErrInvalidSig { + tx.txVRSValid = false + } else { + tx.txVRSValid = true + } } - return json.Unmarshal(msg, &tx.txExtraInfo) + + err = json.Unmarshal(msg, &tx.txExtraInfo) + if err != nil { + return err + } + + return nil } -func IntoJSONRawMessage(raw json.RawMessage, ret *json.RawMessage) error { +func IntoJSONRawMessage(raw json.RawMessage, ret *json.RawMessage, strictness StrictnessLevel) error { *ret = raw return nil } -func IntoBlock(raw json.RawMessage, ret **types.Block) error { +func IntoHeader(raw json.RawMessage, ret **types.Header, strictness StrictnessLevel) error { + var header *types.Header + if err := json.Unmarshal(raw, &header); err != nil { + return err + } + if strictness == StrictnessLevel_Strict { + header.SetHash(header.ComputedBlockHash()) + } + *ret = header + return nil +} + +func IntoBlock(raw json.RawMessage, ret **types.Block, strictness StrictnessLevel) error { if len(raw) == 0 { return ethereum.NotFound } @@ -74,6 +104,10 @@ func IntoBlock(raw json.RawMessage, ret **types.Block) error { setSenderFromServer(tx.tx, *tx.From, body.Hash) } + if (strictness == StrictnessLevel_Default || strictness == StrictnessLevel_Strict) && !tx.txVRSValid { + return fmt.Errorf("invalid transaction v, r, s") + } + if tx.txExtraInfo.TxType != "" { txType, err := hexutil.DecodeUint64(tx.txExtraInfo.TxType) if err != nil { @@ -81,6 +115,7 @@ func IntoBlock(raw json.RawMessage, ret **types.Block) error { } if txType > types.DynamicFeeTxType { // skip the txn, its a non-standard type we don't care about + // NOTE: this is currently skipped blob txn types continue } } @@ -100,41 +135,22 @@ func IntoBlock(raw json.RawMessage, ret **types.Block) error { Withdrawals: body.Withdrawals, }) - // TODO: Remove this, we shouldn't need to use the block cache - // in order for it to contain the correct block hash - block.SetHash(body.Hash) + // ... + if strictness == StrictnessLevel_Strict { + block.SetHash(block.ComputedBlockHash()) + } else { + block.SetHash(body.Hash) + } *ret = block return nil } -// unused -/*func intoBlocks(raw json.RawMessage, ret *[]*types.Block) error { - var list []json.RawMessage - - err := json.Unmarshal(raw, &list) - if err != nil { - return err - } - - blocks := make([]*types.Block, len(list)) - - for i := range list { - err = intoBlock(list[i], &blocks[i]) - if err != nil { - return err - } - } - - *ret = blocks - return nil -}*/ - -func IntoTransaction(raw json.RawMessage, tx **types.Transaction) error { - return IntoTransactionWithPending(raw, tx, nil) +func IntoTransaction(raw json.RawMessage, tx **types.Transaction, strictness StrictnessLevel) error { + return IntoTransactionWithPending(raw, tx, nil, strictness) } -func IntoTransactionWithPending(raw json.RawMessage, tx **types.Transaction, pending *bool) error { +func IntoTransactionWithPending(raw json.RawMessage, tx **types.Transaction, pending *bool, strictness StrictnessLevel) error { var body *rpcTransaction if err := json.Unmarshal(raw, &body); err != nil { return err @@ -144,6 +160,8 @@ func IntoTransactionWithPending(raw json.RawMessage, tx **types.Transaction, pen return fmt.Errorf("server returned transaction without signature") } + // TODO: use strictness level here for transaction hash + if body.From != nil && body.BlockHash != nil { setSenderFromServer(body.tx, *body.From, *body.BlockHash) } diff --git a/go-ethereum/core/types/block.go b/go-ethereum/core/types/block.go index 8b1225ce..93eab85d 100644 --- a/go-ethereum/core/types/block.go +++ b/go-ethereum/core/types/block.go @@ -60,7 +60,7 @@ func (n *BlockNonce) UnmarshalText(input []byte) error { return hexutil.UnmarshalFixedText("BlockNonce", input, n[:]) } -//go:generate go run github.com/fjl/gencodec -type Header -field-override headerMarshaling -out gen_header_json.go +//go:generate go run github.com/fjl/gencodec@latest -type Header -field-override headerMarshaling -out gen_header_json.go //go:generate go run ../../rlp/rlpgen -type Header -out gen_header_rlp.go // Header represents a block header in the Ethereum blockchain. @@ -95,6 +95,9 @@ type Header struct { // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + + // ... TODO: add a note its added by ethkit... + BlockHash common.Hash `json:"hash"` } // field type overrides for gencodec @@ -106,15 +109,26 @@ type headerMarshaling struct { Time hexutil.Uint64 Extra hexutil.Bytes BaseFee *hexutil.Big - Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON + BlockHash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON BlobGasUsed *hexutil.Uint64 ExcessBlobGas *hexutil.Uint64 } -// Hash returns the block hash of the header, which is simply the keccak256 hash of its -// RLP encoding. +func (h *Header) SetHash(hash common.Hash) { + h.BlockHash = hash +} + func (h *Header) Hash() common.Hash { - // TODO: Don't use, for some reason this method doesn't computes the correct block hash + return h.BlockHash +} + +// ComputedBlockHash returns the block hash of the header, which is simply the keccak256 hash of its +// RLP encoding. +// +// NOTE: in go-ethereum this is named just Hash(), but in ethkit we use Hash() as just +// the contents from the header, not the full block hash. +func (h *Header) ComputedBlockHash() common.Hash { + // NOTE/TODO: Don't use, for some reason this method doesn't computes the correct block hash // in some chains, I tested Rinkeby and Polygon and in both it computed a different hash // i.e.: @@ -318,6 +332,7 @@ func CopyHeader(h *Header) *Header { cpy.ParentBeaconRoot = new(common.Hash) *cpy.ParentBeaconRoot = *h.ParentBeaconRoot } + cpy.BlockHash = h.BlockHash return &cpy } @@ -491,10 +506,20 @@ func (b *Block) WithWithdrawals(withdrawals []*Withdrawal) *Block { return b } -// TODO: Hack to keep the "correct" hash value provided by the RPC provider -// delete this code ASAP, this shouldn't be neccesary +// added by ethkit, this will set the hash of the block directly +// instead of computing it from the header. We have separate method, ComputedBlockHash() +// for when we want to recompute the hash from the header. func (b *Block) SetHash(hash common.Hash) { b.hash.Store(&hash) + if b.header != nil { + b.header.SetHash(hash) + } +} + +// added by ethkit, this will recompute the hash of the block from the header +// based on v, r, s values in the signature. +func (b *Block) ComputedBlockHash() common.Hash { + return b.header.ComputedBlockHash() } // Hash returns the keccak256 hash of b's header. diff --git a/go-ethereum/core/types/gen_header_json.go b/go-ethereum/core/types/gen_header_json.go index 197822bc..877a8498 100644 --- a/go-ethereum/core/types/gen_header_json.go +++ b/go-ethereum/core/types/gen_header_json.go @@ -1,5 +1,3 @@ -// Code generated by github.com/fjl/gencodec. DO NOT EDIT. - package types import ( @@ -36,7 +34,7 @@ func (h Header) MarshalJSON() ([]byte, error) { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` - Hash common.Hash `json:"hash"` + BlockHash common.Hash `json:"hash" gencodec:"required"` } var enc Header enc.ParentHash = h.ParentHash @@ -59,7 +57,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(h.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot - enc.Hash = h.Hash() + enc.BlockHash = h.BlockHash return json.Marshal(&enc) } @@ -86,6 +84,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + BlockHash *common.Hash `json:"hash" gencodec:"required"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -163,5 +162,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.ParentBeaconRoot != nil { h.ParentBeaconRoot = dec.ParentBeaconRoot } + if dec.BlockHash != nil { + h.BlockHash = *dec.BlockHash + } return nil }