From 6f6d006f74ffc650b9a598e8fcb1c757b8aaa15a Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Fri, 15 May 2026 12:04:37 +0200 Subject: [PATCH 01/76] core/txpool/blobpool: silence GetRLP miss-log spam (#34965) Avoids every legacy tx hash query hitting the blob pool on the path of BlobPool.GetRLP. --- core/txpool/blobpool/blobpool.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index f8021e00c4..3b2bc03422 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1575,12 +1575,15 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { // e.g. type_byte || [..., version, [blobs], [comms], [proofs]] func (p *BlobPool) GetRLP(hash common.Hash) []byte { data := p.getRLP(hash) + if len(data) == 0 { + // Not in this pool, do not log. + return nil + } rlp, err := encodeForNetwork(data) if err != nil { log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) return nil } - return rlp } From 8a0223e8da596a409df02c11027320df97327e83 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 15 May 2026 21:51:46 +0800 Subject: [PATCH 02/76] core/txpool: use blobTxForPool inside of `Reset` function (#34960) This PR fixes a bug in the current blobpool `Reset` function where it used the Transaction type instead of blobTxForPool. Decoding transactions fetched from the pool as Transaction type caused an error because the blobpool stores blobTxForPool types. --- core/txpool/blobpool/blobpool.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 3b2bc03422..d33629365f 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1107,13 +1107,13 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - var tx types.Transaction - if err = rlp.DecodeBytes(data, &tx); err != nil { + var ptx blobTxForPool + if err = rlp.DecodeBytes(data, &ptx); err != nil { log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) continue } - announcable = append(announcable, tx.WithoutBlobTxSidecar()) - log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", tx.Hash()) + announcable = append(announcable, ptx.Tx) + log.Trace("Blob transaction now announcable", "from", addr, "nonce", meta.nonce, "id", meta.id, "hash", ptx.Tx.Hash()) } } } From d4027f3d4654b374b0962946b03d6f12e3b269ee Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Mon, 18 May 2026 05:37:12 +0300 Subject: [PATCH 03/76] node: normalize HTTP vhost host matching (#34693) --- node/rpcstack.go | 1 + node/rpcstack_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/node/rpcstack.go b/node/rpcstack.go index 20d488b734..1db2ed3f44 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -463,6 +463,7 @@ func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Either invalid (too many colons) or no port specified host = r.Host } + host = strings.ToLower(host) if ipAddr := net.ParseIP(host); ipAddr != nil { // It's an IP address, we can serve that h.next.ServeHTTP(w, r) diff --git a/node/rpcstack_test.go b/node/rpcstack_test.go index bd75dac4eb..f5668abb08 100644 --- a/node/rpcstack_test.go +++ b/node/rpcstack_test.go @@ -60,6 +60,9 @@ func TestVhosts(t *testing.T) { resp := rpcRequest(t, url, testMethod, "host", "test") assert.Equal(t, resp.StatusCode, http.StatusOK) + respUpper := rpcRequest(t, url, testMethod, "host", "TeSt:1234") + assert.Equal(t, respUpper.StatusCode, http.StatusOK) + resp2 := rpcRequest(t, url, testMethod, "host", "bad") assert.Equal(t, resp2.StatusCode, http.StatusForbidden) } From 3d1e6aa6c395540126b93dd936f39e16e72fc47f Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 18 May 2026 22:30:41 +0800 Subject: [PATCH 04/76] signer/core: fix unconditional http request metadata scheme overwrite (#34653) --- signer/core/api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/signer/core/api.go b/signer/core/api.go index 12acf925f0..3b7b53a312 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -196,8 +196,9 @@ func MetadataFromContext(ctx context.Context) Metadata { if info.Transport != "" { if info.Transport == "http" { m.Scheme = info.HTTP.Version + } else { + m.Scheme = info.Transport } - m.Scheme = info.Transport } if info.RemoteAddr != "" { m.Remote = info.RemoteAddr From 1149f76dca22c05976f8ac33b167cc69aaff2de8 Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 19 May 2026 01:05:00 -0500 Subject: [PATCH 05/76] internal/ethapi: add eth_baseFee RPC method (#34904) This method is similar to `eth_blobBaseFee` but returns the next base fee. --- eth/api_backend.go | 8 ++++++++ internal/ethapi/api.go | 5 +++++ internal/ethapi/api_test.go | 1 + internal/ethapi/backend.go | 1 + internal/ethapi/transaction_args_test.go | 1 + 5 files changed, 16 insertions(+) diff --git a/eth/api_backend.go b/eth/api_backend.go index 33fe4fe5d9..8bf91ba680 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/filtermaps" @@ -430,6 +431,13 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB return b.gpo.FeeHistory(ctx, blockCount, lastBlock, rewardPercentiles) } +func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { + if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { + return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + } + return nil +} + func (b *EthAPIBackend) BlobBaseFee(ctx context.Context) *big.Int { if excess := b.CurrentHeader().ExcessBlobGas; excess != nil { return eip4844.CalcBlobFee(b.ChainConfig(), b.CurrentHeader()) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 6d38c6c7c8..68f56920ab 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -146,6 +146,11 @@ func (api *EthereumAPI) BlobBaseFee(ctx context.Context) *hexutil.Big { return (*hexutil.Big)(api.b.BlobBaseFee(ctx)) } +// BaseFee returns the base fee for the next block. +func (api *EthereumAPI) BaseFee(ctx context.Context) *hexutil.Big { + return (*hexutil.Big)(api.b.BaseFee(ctx)) +} + // Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not // yet received the latest block headers from its peers. In case it is synchronizing: // - startingBlock: block number this node started to synchronize from diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 63e75bd3e3..f191643ce2 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -500,6 +500,7 @@ func (b testBackend) FeeHistory(ctx context.Context, blockCount uint64, lastBloc return nil, nil, nil, nil, nil, nil, nil } func (b testBackend) BlobBaseFee(ctx context.Context) *big.Int { return new(big.Int) } +func (b testBackend) BaseFee(ctx context.Context) *big.Int { return new(big.Int) } func (b testBackend) ChainDb() ethdb.Database { return b.db } func (b testBackend) AccountManager() *accounts.Manager { return b.accman } func (b testBackend) ExtRPCEnabled() bool { return false } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index af3d592b82..65112a5294 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -46,6 +46,7 @@ type Backend interface { SuggestGasTipCap(ctx context.Context) (*big.Int, error) FeeHistory(ctx context.Context, blockCount uint64, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*big.Int, [][]*big.Int, []*big.Int, []float64, []*big.Int, []float64, error) BlobBaseFee(ctx context.Context) *big.Int + BaseFee(ctx context.Context) *big.Int ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 30791f32b5..4b7774c9b7 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -318,6 +318,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { return big.NewInt(42), nil } func (b *backendMock) BlobBaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } +func (b *backendMock) BaseFee(ctx context.Context) *big.Int { return big.NewInt(42) } func (b *backendMock) CurrentHeader() *types.Header { return b.current } func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config } From 4f4bfdbea7c2f987306e5585e6a259ce20af5043 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 18:21:43 +0800 Subject: [PATCH 06/76] beacon/light/sync: check error (#34818) --- beacon/light/sync/update_sync.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beacon/light/sync/update_sync.go b/beacon/light/sync/update_sync.go index d84a3d64da..b15b967433 100644 --- a/beacon/light/sync/update_sync.go +++ b/beacon/light/sync/update_sync.go @@ -98,7 +98,10 @@ func (s *CheckpointInit) Process(requester request.Requester, events []request.E case ssDefault: if resp != nil { if checkpoint := resp.(*types.BootstrapData); checkpoint.Header.Hash() == common.Hash(req.(ReqCheckpointData)) { - s.chain.CheckpointInit(*checkpoint) + err := s.chain.CheckpointInit(*checkpoint) + if err != nil { + return + } s.initialized = true return } From 970e3cd6f01331fab4888d4f185f7081c88e029d Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 18:33:09 +0800 Subject: [PATCH 07/76] beacon/light: fix lock after lock deadlock (#34800) --- beacon/light/committee_chain.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/beacon/light/committee_chain.go b/beacon/light/committee_chain.go index 4fa87785c0..7fc735d893 100644 --- a/beacon/light/committee_chain.go +++ b/beacon/light/committee_chain.go @@ -182,6 +182,12 @@ func (s *CommitteeChain) Reset() { s.chainmu.Lock() defer s.chainmu.Unlock() + s.resetLocked() +} + +// ResetLocked resets the committee chain without locking. The caller should hold +// the chainmu lock. +func (s *CommitteeChain) resetLocked() { if err := s.rollback(0); err != nil { log.Error("Error writing batch into chain database", "error", err) } @@ -201,22 +207,22 @@ func (s *CommitteeChain) CheckpointInit(bootstrap types.BootstrapData) error { } period := bootstrap.Header.SyncPeriod() if err := s.deleteFixedCommitteeRootsFrom(period + 2); err != nil { - s.Reset() + s.resetLocked() return err } if s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot) != nil { - s.Reset() + s.resetLocked() if err := s.addFixedCommitteeRoot(period, bootstrap.CommitteeRoot); err != nil { - s.Reset() + s.resetLocked() return err } } if err := s.addFixedCommitteeRoot(period+1, common.Hash(bootstrap.CommitteeBranch[0])); err != nil { - s.Reset() + s.resetLocked() return err } if err := s.addCommittee(period, bootstrap.Committee); err != nil { - s.Reset() + s.resetLocked() return err } s.changeCounter++ From e3ce773b8c3f934869bbded6e57c0f24a9983a24 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 19 May 2026 21:22:03 +0800 Subject: [PATCH 08/76] internal/ethapi: propagate SetHead errors to API (#35001) Return blockchain rewind failures from debug_setHead instead of ignoring them. --- eth/api_backend.go | 4 ++-- internal/ethapi/api.go | 3 +-- internal/ethapi/api_test.go | 2 +- internal/ethapi/backend.go | 2 +- internal/ethapi/transaction_args_test.go | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 8bf91ba680..5e3558d8eb 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -62,9 +62,9 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header { return b.eth.blockchain.CurrentBlock() } -func (b *EthAPIBackend) SetHead(number uint64) { +func (b *EthAPIBackend) SetHead(number uint64) error { b.eth.handler.downloader.Cancel() - b.eth.blockchain.SetHead(number) + return b.eth.blockchain.SetHead(number) } func (b *EthAPIBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 68f56920ab..109169e0b0 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -2131,8 +2131,7 @@ func (api *DebugAPI) SetHead(number hexutil.Uint64) error { if header.Number.Uint64() <= uint64(number) { return errors.New("not allowed to rewind to a future block") } - api.b.SetHead(uint64(number)) - return nil + return api.b.SetHead(uint64(number)) } // NetAPI offers network related RPC methods diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index f191643ce2..561ce2c2d2 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -508,7 +508,7 @@ func (b testBackend) RPCGasCap() uint64 { return 10000000 func (b testBackend) RPCEVMTimeout() time.Duration { return time.Second } func (b testBackend) RPCTxFeeCap() float64 { return 0 } func (b testBackend) UnprotectedAllowed() bool { return false } -func (b testBackend) SetHead(number uint64) {} +func (b testBackend) SetHead(number uint64) error { return nil } func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { if number == rpc.LatestBlockNumber { return b.chain.CurrentBlock(), nil diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 65112a5294..f23be85782 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -58,7 +58,7 @@ type Backend interface { RPCTxSyncMaxTimeout() time.Duration // Blockchain API - SetHead(number uint64) + SetHead(number uint64) error HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 4b7774c9b7..ccb46a810d 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -337,7 +337,7 @@ func (b *backendMock) RPCGasCap() uint64 { return 0 } func (b *backendMock) RPCEVMTimeout() time.Duration { return time.Second } func (b *backendMock) RPCTxFeeCap() float64 { return 0 } func (b *backendMock) UnprotectedAllowed() bool { return false } -func (b *backendMock) SetHead(number uint64) {} +func (b *backendMock) SetHead(number uint64) error { return nil } func (b *backendMock) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { return nil, nil } From 1bdc4a60d958ab3f5ef26f8bd428486f5efd18aa Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 19 May 2026 21:51:53 +0800 Subject: [PATCH 09/76] core, consensus, internal, eth, miner: construct block accessList (#34957) This PR finally lands EIP-7928, collecting the block accessList during the block execution and verifying against the block header. --------- Co-authored-by: jwasinger Co-authored-by: Marius van der Wijden --- beacon/engine/gen_ed.go | 79 +- beacon/engine/types.go | 134 +- cmd/evm/internal/t8ntool/execution.go | 16 +- consensus/beacon/consensus.go | 20 +- consensus/clique/clique.go | 3 +- consensus/consensus.go | 7 +- consensus/ethash/consensus.go | 3 +- core/bal_test.go | 1319 +++++++++++++++++ core/bintrie_witness_test.go | 3 +- core/block_validator.go | 39 + core/chain_makers.go | 27 +- core/genesis.go | 1 + core/state_processor.go | 109 +- core/state_transition.go | 3 +- core/types.go | 5 + core/types/bal/bal.go | 54 +- core/types/bal/bal_encoding.go | 47 +- core/types/bal/bal_test.go | 136 +- core/types/block.go | 5 +- core/types/hashes.go | 3 + core/vm/evm.go | 5 + eth/tracers/api.go | 2 +- .../tracetest/selfdestruct_state_test.go | 2 +- internal/ethapi/simulate.go | 21 +- miner/worker.go | 29 +- params/protocol_params.go | 10 + 26 files changed, 1883 insertions(+), 199 deletions(-) create mode 100644 core/bal_test.go diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index c733b3f350..02a1fd3805 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executableDataMarshaling)(nil) @@ -17,24 +18,25 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -60,30 +62,32 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) enc.SlotNumber = (*hexutil.Uint64)(e.SlotNumber) + enc.BlockAccessList = e.BlockAccessList return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + SlotNumber *hexutil.Uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -160,5 +164,8 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.SlotNumber != nil { e.SlotNumber = (*uint64)(dec.SlotNumber) } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 9b0b186df7..5c31ee4e98 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" ) @@ -82,24 +83,25 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - SlotNumber *uint64 `json:"slotNumber,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + SlotNumber *uint64 `json:"slotNumber,omitempty"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` } // JSON type overrides for executableData. @@ -303,56 +305,66 @@ func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.H requestsHash = &h } - header := &types.Header{ - ParentHash: data.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: data.FeeRecipient, - Root: data.StateRoot, - TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), - ReceiptHash: data.ReceiptsRoot, - Bloom: types.BytesToBloom(data.LogsBloom), - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(data.Number), - GasLimit: data.GasLimit, - GasUsed: data.GasUsed, - Time: data.Timestamp, - BaseFee: data.BaseFeePerGas, - Extra: data.ExtraData, - MixDigest: data.Random, - WithdrawalsHash: withdrawalsRoot, - ExcessBlobGas: data.ExcessBlobGas, - BlobGasUsed: data.BlobGasUsed, - ParentBeaconRoot: beaconRoot, - RequestsHash: requestsHash, - SlotNumber: data.SlotNumber, + // If Amsterdam is enabled, data.BlockAccessList is always non-nil, + // even for empty blocks with no state transitions. + // + // If Amsterdam is not enabled yet, blockAccessListHash is expected + // to be nil. + var blockAccessListHash *common.Hash + if data.BlockAccessList != nil { + hash := data.BlockAccessList.Hash() + blockAccessListHash = &hash } - return types.NewBlockWithHeader(header). - WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), - nil + header := &types.Header{ + ParentHash: data.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: data.FeeRecipient, + Root: data.StateRoot, + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: data.ReceiptsRoot, + Bloom: types.BytesToBloom(data.LogsBloom), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(data.Number), + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Time: data.Timestamp, + BaseFee: data.BaseFeePerGas, + Extra: data.ExtraData, + MixDigest: data.Random, + WithdrawalsHash: withdrawalsRoot, + ExcessBlobGas: data.ExcessBlobGas, + BlobGasUsed: data.BlobGasUsed, + ParentBeaconRoot: beaconRoot, + RequestsHash: requestsHash, + SlotNumber: data.SlotNumber, + BlockAccessListHash: blockAccessListHash, + } + return types.NewBlockWithHeader(header).WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}), nil } // BlockToExecutableData constructs the ExecutableData structure by filling the // fields from the given block. It assumes the given block is post-merge block. func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.BlobTxSidecar, requests [][]byte) *ExecutionPayloadEnvelope { data := &ExecutableData{ - BlockHash: block.Hash(), - ParentHash: block.ParentHash(), - FeeRecipient: block.Coinbase(), - StateRoot: block.Root(), - Number: block.NumberU64(), - GasLimit: block.GasLimit(), - GasUsed: block.GasUsed(), - BaseFeePerGas: block.BaseFee(), - Timestamp: block.Time(), - ReceiptsRoot: block.ReceiptHash(), - LogsBloom: block.Bloom().Bytes(), - Transactions: encodeTransactions(block.Transactions()), - Random: block.MixDigest(), - ExtraData: block.Extra(), - Withdrawals: block.Withdrawals(), - BlobGasUsed: block.BlobGasUsed(), - ExcessBlobGas: block.ExcessBlobGas(), - SlotNumber: block.SlotNumber(), + BlockHash: block.Hash(), + ParentHash: block.ParentHash(), + FeeRecipient: block.Coinbase(), + StateRoot: block.Root(), + Number: block.NumberU64(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFeePerGas: block.BaseFee(), + Timestamp: block.Time(), + ReceiptsRoot: block.ReceiptHash(), + LogsBloom: block.Bloom().Bytes(), + Transactions: encodeTransactions(block.Transactions()), + Random: block.MixDigest(), + ExtraData: block.Extra(), + Withdrawals: block.Withdrawals(), + BlobGasUsed: block.BlobGasUsed(), + ExcessBlobGas: block.ExcessBlobGas(), + SlotNumber: block.SlotNumber(), + BlockAccessList: block.AccessList(), } // Add blobs. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index a2de58ad46..043e675494 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/ethdb" @@ -172,6 +173,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, includedTxs types.Transactions blobGasUsed = uint64(0) receipts = make(types.Receipts, 0) + + // TODO return blockAccessList as a part of result + blockAccessList = bal.NewConstructionBlockAccessList() ) vmContext := vm.BlockContext{ CanTransfer: core.CanTransfer, @@ -231,14 +235,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } evm := vm.NewEVM(vmContext, statedb, chainConfig, vmConfig) if beaconRoot := pre.Env.ParentBeaconBlockRoot; beaconRoot != nil { - core.ProcessBeaconBlockRoot(*beaconRoot, evm) + core.ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) } if pre.Env.BlockHashes != nil && chainConfig.IsPrague(new(big.Int).SetUint64(pre.Env.Number), pre.Env.Timestamp) { var ( prevNumber = pre.Env.Number - 1 prevHash = pre.Env.BlockHashes[math.HexOrDecimal64(prevNumber)] ) - core.ProcessParentBlockHash(prevHash, evm) + core.ProcessParentBlockHash(prevHash, evm, blockAccessList) } for i := 0; txIt.Next(); i++ { tx, err := txIt.Tx() @@ -271,11 +275,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } } statedb.SetTxContext(tx.Hash(), len(receipts), uint32(len(receipts)+1)) + var ( snapshot = statedb.Snapshot() gp = gaspool.Snapshot() ) - receipt, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) + receipt, bal, err := core.ApplyTransactionWithEVM(msg, gaspool, statedb, vmContext.BlockNumber, blockHash, pre.Env.Timestamp, tx, evm) if err != nil { statedb.RevertToSnapshot(snapshot) log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err) @@ -292,6 +297,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, } blobGasUsed += txBlobGas receipts = append(receipts, receipt) + blockAccessList.Merge(bal) } statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)) @@ -336,10 +342,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, for _, receipt := range receipts { allLogs = append(allLogs, receipt.Logs...) } - requests, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) + requests, bal, err := core.PostExecution(context.Background(), chainConfig, vmContext.BlockNumber, vmContext.Time, allLogs, evm, uint32(len(receipts)+1)) if err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("failed to process post-execution: %v", err)) } + blockAccessList.Merge(bal) + // Commit block root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber), chainConfig.IsCancun(vmContext.BlockNumber, vmContext.Time)) if err != nil { diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 72ac75c036..4237418e73 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -342,9 +343,9 @@ func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine and processes withdrawals on top. -func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { if !beacon.IsPoSHeader(header) { - beacon.ethone.Finalize(chain, header, state, body) + beacon.ethone.Finalize(chain, header, state, body, blockAccessIndex, bal) return } // Withdrawals processing. @@ -352,7 +353,20 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // Convert amount from gwei to wei. amount := new(uint256.Int).SetUint64(w.Amount) amount = amount.Mul(amount, uint256.NewInt(params.GWei)) - state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + prev := state.AddBalance(w.Address, amount, tracing.BalanceIncreaseWithdrawal) + + // Populate the block-level accessList if Amsterdam is enabled + if chain.Config().IsAmsterdam(header.Number, header.Time) { + if w.Amount == 0 { + // Zero amount withdrawal, account is accessed potential + // without state changes. + bal.AccountRead(w.Address) + } else { + // Non-zero amount withdrawal, account is accessed with + // a balance change. + bal.BalanceChange(blockAccessIndex, w.Address, new(uint256.Int).Add(&prev, amount)) + } + } } // No block reward which is issued by consensus layer instead. } diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index ceaec44656..f44afde241 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/keccak" @@ -573,7 +574,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Finalize implements consensus.Engine. There is no post-transaction // consensus rules in clique, do nothing here. -func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // No block rewards in PoA, so the state remains as is } diff --git a/consensus/consensus.go b/consensus/consensus.go index 4ba389292f..e4f7b7a6a1 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/params" ) @@ -79,12 +80,12 @@ type Engine interface { // rules of a particular engine. The changes are executed inline. Prepare(chain ChainHeaderReader, header *types.Header) error - // Finalize runs any post-transaction state modifications (e.g. block rewards - // or process withdrawals) but does not assemble the block. + // Finalize runs any post-transaction consensus-specific state modifications + // (e.g. block rewards or process withdrawals) but does not assemble the block. // // Note: The state database might be updated to reflect any consensus rules // that happen at finalization (e.g. block rewards). - Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) + Finalize(chain ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index ee9d9d97d6..21adc9d279 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto/keccak" "github.com/ethereum/go-ethereum/params" @@ -504,7 +505,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H } // Finalize implements consensus.Engine, accumulating the block and uncle rewards. -func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body) { +func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state vm.StateDB, body *types.Body, blockAccessIndex uint32, bal *bal.ConstructionBlockAccessList) { // Accumulate any block and uncle rewards accumulateRewards(chain.Config(), state, header, body.Uncles) } diff --git a/core/bal_test.go b/core/bal_test.go new file mode 100644 index 0000000000..f0b9dc6443 --- /dev/null +++ b/core/bal_test.go @@ -0,0 +1,1319 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "bytes" + "crypto/ecdsa" + "maps" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" +) + +// EIP-7928 BAL inclusion tests. +// +// Each test exercises a single rule from the spec and asserts both presence +// and absence in the resulting block access list. + +// balChainConfig returns a MergedTestChainConfig clone with Amsterdam active from genesis. +func balChainConfig() *params.ChainConfig { + cfg := *params.MergedTestChainConfig + cfg.AmsterdamTime = new(uint64) + blob := *cfg.BlobScheduleConfig + blob.Amsterdam = blob.Osaka + cfg.BlobScheduleConfig = &blob + return &cfg +} + +// balTestEnv bundles common identities used across the tests. +type balTestEnv struct { + cfg *params.ChainConfig + signer types.Signer + key *ecdsa.PrivateKey + from common.Address + gspec *Genesis +} + +// newBALTestEnv builds an Amsterdam chain config, funds a sender and pre-deploys +// the EIP-7928 system contracts. Extra accounts can be merged into Alloc. +func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv { + cfg := balChainConfig() + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + from := crypto.PubkeyToAddress(key.PublicKey) + + alloc := types.GenesisAlloc{ + from: {Balance: newGwei(1_000_000_000)}, + params.BeaconRootsAddress: {Nonce: 1, Code: params.BeaconRootsCode, Balance: common.Big0}, + params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0}, + params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0}, + params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0}, + } + maps.Copy(alloc, extra) + return &balTestEnv{ + cfg: cfg, + signer: types.LatestSigner(cfg), + key: key, + from: from, + gspec: &Genesis{Config: cfg, Alloc: alloc}, + } +} + +// run generates exactly one Amsterdam block and returns its BAL. +func (e *balTestEnv) run(t *testing.T, gen func(*BlockGen)) (*bal.BlockAccessList, types.Receipts) { + t.Helper() + engine := beacon.New(ethash.NewFaker()) + _, blocks, receipts := GenerateChainWithGenesis(e.gspec, engine, 1, func(_ int, b *BlockGen) { + gen(b) + }) + if blocks[0].AccessList() == nil { + t.Fatal("expected non-nil block access list") + } + return blocks[0].AccessList(), receipts[0] +} + +// --- assertion helpers --- + +func findAccount(b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + for i := range *b { + if (*b)[i].Address == addr { + return &(*b)[i] + } + } + return nil +} + +func hasSlotIn(slots []*uint256.Int, key common.Hash) bool { + want := new(uint256.Int).SetBytes(key[:]) + for _, s := range slots { + if s.Cmp(want) == 0 { + return true + } + } + return false +} + +func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Hash) bool { + aa := findAccount(b, addr) + if aa == nil { + return false + } + want := new(uint256.Int).SetBytes(key[:]) + for _, w := range aa.StorageWrites { + if w.Slot.Cmp(want) == 0 { + return true + } + } + return false +} + +func assertPresent(t *testing.T, b *bal.BlockAccessList, addr common.Address) *bal.AccountAccess { + t.Helper() + aa := findAccount(b, addr) + if aa == nil { + t.Fatalf("address %x missing from BAL\n%s", addr, b.PrettyPrint()) + } + return aa +} + +func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) { + t.Helper() + if findAccount(b, addr) != nil { + t.Fatalf("address %x must NOT be in BAL\n%s", addr, b.PrettyPrint()) + } +} + +func assertEmpty(t *testing.T, aa *bal.AccountAccess) { + t.Helper() + if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 || + len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 { + t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa) + } +} + +// --- tx builders --- + +func (e *balTestEnv) tx(nonce uint64, to *common.Address, value *big.Int, gas uint64, tipGwei int64, data []byte) *types.Transaction { + return types.MustSignNewTx(e.key, e.signer, &types.DynamicFeeTx{ + ChainID: e.cfg.ChainID, + Nonce: nonce, + To: to, + Value: value, + Gas: gas, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(tipGwei), + Data: data, + }) +} + +// ============================== Account inclusion ============================== + +// TestBALTxSenderAndRecipient: a value transfer records balance+nonce for sender +// and a balance entry for the recipient. +func TestBALTxSenderAndRecipient(t *testing.T) { + to := common.HexToAddress("0xc0ffee") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender missing balance change") + } + recipient := assertPresent(t, b, to) + if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("recipient balance: %+v", recipient.BalanceChanges) + } +} + +// TestBALZeroValueRecipient: a tx with value 0 still lists the recipient, +// but without a balance entry. +func TestBALZeroValueRecipient(t *testing.T) { + to := common.HexToAddress("0x0123456789abcdef") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + + r := assertPresent(t, b, to) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-value recipient should have no balance entry: %+v", r.BalanceChanges) + } +} + +// TestBALEmptyBlockExcludesCoinbase: an empty block (no txs, no withdrawals) +// never touches the coinbase, so it must NOT appear in the BAL — the zero +// block reward alone does not trigger inclusion. +func TestBALEmptyBlockExcludesCoinbase(t *testing.T) { + coinbase := common.Address{0xc0} + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal but does not record any access. + g.SetCoinbase(coinbase) + }) + assertAbsent(t, b, coinbase) +} + +// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase +// and the balance change appears in the BAL. +func TestBALCoinbaseTipCapturesBalance(t *testing.T) { + coinbase := common.Address{0xc0} + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(coinbase) + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil)) + }) + + cb := assertPresent(t, b, coinbase) + if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges) + } +} + +// TestBALSystemAddressExcluded: SYSTEM_ADDRESS (0xff…fe) is not in the BAL +// for a regular block. +func TestBALSystemAddressExcluded(t *testing.T) { + to := common.HexToAddress("0xabba") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil)) + }) + assertAbsent(t, b, params.SystemAddress) +} + +// TestBALSystemAddressIncludedWhenTouched: SYSTEM_ADDRESS becomes a regular +// account in the BAL once it experiences state access (here: receives value). +func TestBALSystemAddressIncludedWhenTouched(t *testing.T) { + sys := params.SystemAddress + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &sys, big.NewInt(1000), params.TxGas, 0, nil)) + }) + + aa := assertPresent(t, b, sys) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 { + t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileInvokedFromContractIncluded: a precompile that is invoked +// indirectly — via STATICCALL from a regular contract — must still appear in +// the BAL with no balance entry. +func TestBALPrecompileInvokedFromContractIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + caller := common.HexToAddress("0xca11") + // PUSH1 0 (retSize) PUSH1 0 (retOff) PUSH1 0 (argsSize) PUSH1 0 (argsOff) + // PUSH20 0x04 GAS STATICCALL POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, identity.Bytes()...) + code = append(code, 0x5a, 0xfa, 0x50, 0x00) + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile invoked via STATICCALL must not record balance: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileCalledNoValueIncluded: a tx targeting the identity precompile +// with zero value lists the precompile but records no balance entry. +func TestBALPrecompileCalledNoValueIncluded(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(0), 50_000, 0, []byte{0xde, 0xad})) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("precompile must not record balance change: %+v", aa.BalanceChanges) + } +} + +// TestBALPrecompileValueTransferRecordsBalance: a precompile receives ETH only +// in the form of a value transfer — the balance entry is then recorded. +func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) { + identity := common.BytesToAddress([]byte{0x04}) + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &identity, big.NewInt(5), 50_000, 0, nil)) + }) + + aa := assertPresent(t, b, identity) + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 { + t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges) + } +} + +// TestBALBalanceProbeOnNonExistent: BALANCE against a never-allocated address +// still adds it to the BAL with an empty change set. +func TestBALBalanceProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x31, 0x50, 0x00) // BALANCE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeSizeProbeOnNonExistent: EXTCODESIZE against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeSizeProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xcafecafecafecafecafecafecafecafecafecafe") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3b, 0x50, 0x00) // EXTCODESIZE, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeHashProbeOnNonExistent: EXTCODEHASH against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeHashProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfacefacefacefacefacefacefacefacefacefacE") + caller := common.HexToAddress("0xc1") + code := append([]byte{0x73}, probe.Bytes()...) // PUSH20 probe + code = append(code, 0x3f, 0x50, 0x00) // EXTCODEHASH, POP, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALExtCodeCopyProbeOnNonExistent: EXTCODECOPY against a never-allocated +// address adds it to the BAL with an empty change set. +func TestBALExtCodeCopyProbeOnNonExistent(t *testing.T) { + probe := common.HexToAddress("0xfeedfeedfeedfeedfeedfeedfeedfeedfeedfeed") + caller := common.HexToAddress("0xc1") + // PUSH1 0 (length) PUSH1 0 (codeOffset) PUSH1 0 (destOffset) + // PUSH20 probe EXTCODECOPY STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73} + code = append(code, probe.Bytes()...) + code = append(code, 0x3c, 0x00) // EXTCODECOPY, STOP + + env := newBALTestEnv(types.GenesisAlloc{caller: {Code: code, Balance: common.Big0}}) + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, probe)) +} + +// TestBALAccessListNotAutoPromoted: an EIP-2930 access-list entry that is +// never actually touched must NOT appear in the BAL. +func TestBALAccessListNotAutoPromoted(t *testing.T) { + to := common.HexToAddress("0xabba") + dormant := common.HexToAddress("0xd0d0") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{ + ChainID: env.cfg.ChainID, + Nonce: 0, + To: &to, + Value: big.NewInt(0), + Gas: params.TxGas + 4000, + GasFeeCap: newGwei(10), + GasTipCap: newGwei(0), + AccessList: types.AccessList{{Address: dormant, StorageKeys: nil}}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, dormant) +} + +// ============================== CALL family ============================== + +// makeStubCaller emits a single CALL-family op against `target` then STOPs, +// with zero call data and discarded return data. +// +// op = 0xf1 (CALL) / 0xf2 (CALLCODE): +// stack = retSize, retOff, argsSize, argsOff, value, addr, gas +// op = 0xf4 (DELEGATECALL) / 0xfa (STATICCALL): +// stack = retSize, retOff, argsSize, argsOff, addr, gas +func makeStubCaller(op byte, target common.Address) []byte { + // retSize, retOff, argsSize, argsOff = 0 + prelude := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00} + if op == 0xf1 || op == 0xf2 { // CALL/CALLCODE need an extra value=0 + prelude = append(prelude, 0x60, 0x00) + } + prelude = append(prelude, 0x73) // PUSH20 + prelude = append(prelude, target.Bytes()...) + prelude = append(prelude, 0x5a) // GAS + prelude = append(prelude, op) + prelude = append(prelude, 0x50, 0x00) // POP, STOP + return prelude +} + +// TestBALCallTargetWithEmptyChangeSet: a zero-value CALL to an existing +// contract that has no state changes lists the target with empty entries. +func TestBALCallTargetWithEmptyChangeSet(t *testing.T) { + target := common.HexToAddress("0xbabe") + env := newBALTestEnv(types.GenesisAlloc{ + target: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &target, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALCallCodeTargetIncluded: CALLCODE puts the target in the BAL with an +// empty change set (CALLCODE executes target's code in the caller's storage +// context, so the target itself records no state changes). +func TestBALCallCodeTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf2 /* CALLCODE */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALDelegateCallTargetIncluded: DELEGATECALL puts both caller and target +// in the BAL even when neither produces state changes. +func TestBALDelegateCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xf4 /* DELEGATECALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// TestBALStaticCallTargetIncluded: STATICCALL puts the target in the BAL with +// no balance entry recorded. +func TestBALStaticCallTargetIncluded(t *testing.T) { + target := common.HexToAddress("0xdeed") + caller := common.HexToAddress("0xca11") + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeStubCaller(0xfa /* STATICCALL */, target), Balance: common.Big0}, + target: {Code: []byte{0x00}, Balance: common.Big0}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 200_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertEmpty(t, assertPresent(t, b, target)) +} + +// ============================== Revert behaviour ============================== + +// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still +// records the touched contract in the BAL with an empty change set. +func TestBALRevertedTxStillIncluded(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + // PUSH1 0 PUSH1 0 REVERT + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + assertEmpty(t, assertPresent(t, b, reverter)) +} + +// TestBALSenderRecordedOnRevert: even when the top-level call reverts, the +// sender's final nonce and balance MUST be recorded. +func TestBALSenderRecordedOnRevert(t *testing.T) { + reverter := common.HexToAddress("0xbeef") + revertCode := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + env := newBALTestEnv(types.GenesisAlloc{reverter: {Code: revertCode, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &reverter, big.NewInt(0), 100_000, 0, nil)) + }) + + sender := assertPresent(t, b, env.from) + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges) + } + if len(sender.BalanceChanges) == 0 { + t.Fatalf("sender balance change (gas paid) must be present on revert") + } +} + +// ============================== Storage inclusion ============================== + +// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and +// keeps it out of storage_reads. +func TestBALStorageWriteRecorded(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x01)) + // PUSH1 0x42 PUSH1 0x01 SSTORE STOP + code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot 0x01 must NOT appear in storage_reads") + } +} + +// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads. +func TestBALStorageSloadOnly(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x07)) + // PUSH1 0x07 SLOAD POP STOP + code := []byte{0x60, 0x07, 0x54, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("expected slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must NOT appear in storage_changes") + } +} + +// TestBALStorageReadThenWriteOnlyInWrites: SLOAD followed by SSTORE on the +// same slot drops the slot from storage_reads (write-wins invariant). +func TestBALStorageReadThenWriteOnlyInWrites(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x05)) + // PUSH1 5 SLOAD POP PUSH1 0x42 PUSH1 5 SSTORE STOP + code := []byte{ + 0x60, 0x05, 0x54, 0x50, + 0x60, 0x42, 0x60, 0x05, 0x55, + 0x00, + } + env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("slot must be in storage_changes\n%s", b.PrettyPrint()) + } + if hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("slot must NOT appear in storage_reads (write-wins)\n%s", b.PrettyPrint()) + } +} + +// TestBALNoOpSSTOREDemotesToRead: an SSTORE whose value equals the committed +// value lands the slot in storage_reads only. +func TestBALNoOpSSTOREDemotesToRead(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x09)) + // SSTORE(0x09, 0x42) — slot pre-state is 0x42, so the write is a no-op. + code := []byte{0x60, 0x42, 0x60, 0x09, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasSlotIn(aa.StorageReads, slot) { + t.Fatalf("no-op SSTORE should leave slot in storage_reads\n%s", b.PrettyPrint()) + } + if hasStorageWrite(b, contract, slot) { + t.Fatalf("no-op SSTORE must NOT register a write") + } +} + +// TestBALStorageWriteZeroIsAWrite: writing 0 to a non-zero slot is still a +// state change and lands in storage_changes. +func TestBALStorageWriteZeroIsAWrite(t *testing.T) { + contract := common.HexToAddress("0xc1") + slot := common.BigToHash(big.NewInt(0x03)) + // PUSH1 0 PUSH1 3 SSTORE STOP + code := []byte{0x60, 0x00, 0x60, 0x03, 0x55, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + contract: { + Code: code, + Balance: common.Big0, + Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(0x42))}, + }, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &contract, big.NewInt(0), 100_000, 0, nil)) + }) + + aa := assertPresent(t, b, contract) + if !hasStorageWrite(b, contract, slot) { + t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint()) + } + for _, w := range aa.StorageWrites { + if w.Slot.Uint64() == 0x03 { + if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() { + t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses) + } + } + } +} + +// ============================== CREATE / contract deployment ============================== + +// TestBALCreateDeploysCode: a successful contract-creation tx records the new +// address with nonce 0→1, a balance entry (value transferred), and a code entry. +func TestBALCreateDeploysCode(t *testing.T) { + env := newBALTestEnv(nil) + // Init: deploy runtime [0x00] (single STOP byte). + // PUSH1 0 PUSH1 0 MSTORE8 PUSH1 1 PUSH1 0 RETURN + init := []byte{0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(7), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) { + t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges) + } + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 { + t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges) + } +} + +// TestBALCreateEmptyRuntimeNoCodeEntry: when init code returns 0 bytes the +// new address is still listed with nonce 0→1 but no code entry. +func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + // Init: PUSH1 0 PUSH1 0 RETURN → returns 0 bytes + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + aa := assertPresent(t, b, created) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("empty runtime must NOT record a code entry, got %+v", aa.CodeChanges) + } +} + +// TestBALCreateInitRevertEmptyChangeSet: when init code reverts, the would-be +// contract address is in the BAL with an empty change set. +func TestBALCreateInitRevertEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // PUSH1 0 PUSH1 0 REVERT + init := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateInitOOGEmptyChangeSet: init code that runs out of gas leaves +// the deployed address in the BAL with an empty change set. +func TestBALCreateInitOOGEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + // Infinite loop: JUMPDEST PUSH1 0 JUMP — burns gas until OOG. + init := []byte{0x5b, 0x60, 0x00, 0x56} + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 60_000, 0, init)) + }) + + created := receipts[0].ContractAddress + assertEmpty(t, assertPresent(t, b, created)) +} + +// TestBALCreateAddressCollisionStillIncluded: when CREATE targets an address +// that already holds a contract, the deployment fails but the address was +// probed during execution and MUST appear in the BAL with an empty change set. +func TestBALCreateAddressCollisionStillIncluded(t *testing.T) { + env := newBALTestEnv(nil) + // For a top-level CREATE tx the deployed address is CreateAddress(sender, 0). + // Pre-allocate a contract at that address to provoke ErrContractAddressCollision. + collide := crypto.CreateAddress(env.from, 0) + env.gspec.Alloc[collide] = types.Account{ + Nonce: 1, + Code: []byte{0x00}, + Balance: common.Big0, + } + + // Init code doesn't matter — execution never starts. + init := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + aa := assertPresent(t, b, collide) + // The address must be present but the pre-existing nonce/code MUST NOT + // be overwritten by the failed creation. + if len(aa.NonceChanges) != 0 { + t.Fatalf("collision must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("collision must not write code: %+v", aa.CodeChanges) + } +} + +// TestBALInEVMCreatePreAccessAbortDestinationExcluded: if a CREATE frame +// aborts BEFORE the destination is read from state (here: the caller has 0 +// balance and CREATE requests value > 0, tripping evm.create's CanTransfer +// check before GetCodeHash), the would-be address MUST NOT appear in the +// BAL — only "if target account is accessed" qualifies for inclusion. +func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 1 (value) CREATE POP STOP + code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x01, 0xf0, 0x50, 0x00} + env := newBALTestEnv(types.GenesisAlloc{ + factory: {Code: code, Balance: common.Big0, Nonce: 1}, // factory has no balance + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 200_000, 0, nil)) + }) + + // The address that WOULD have been deployed had the create succeeded. + wouldBeDest := crypto.CreateAddress(factory, 1) + assertAbsent(t, b, wouldBeDest) + + // The factory itself is in BAL (it ran), but its nonce MUST NOT have been + // bumped because evm.create returned before the SetNonce call. + aa := assertPresent(t, b, factory) + if len(aa.NonceChanges) != 0 { + t.Fatalf("factory nonce must not be bumped on pre-access abort: %+v", aa.NonceChanges) + } +} + +// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract +// (not a top-level CREATE tx) records the deployed address in the BAL. +func TestBALInEVMCreateDeploysContract(t *testing.T) { + factory := common.HexToAddress("0xfac4") + // Factory code: + // Write 5-byte init code (0x60 0x00 0x60 0x00 0xf3) into memory starting at offset 0. + // Then CREATE(value=0, offset=0, length=5). + // + // Layout: store the init code as a single 32-byte word at offset 0 via MSTORE + // with leftmost 27 bytes garbage, then call CREATE with offset = 27, length = 5. + initBlob := []byte{0x60, 0x00, 0x60, 0x00, 0xf3} + var word [32]byte + copy(word[32-len(initBlob):], initBlob) + code := []byte{0x7f} // PUSH32 + code = append(code, word[:]...) + code = append(code, 0x60, 0x00, 0x52) // PUSH1 0, MSTORE + // CREATE expects [value, offset, length] with value on bottom of stack. + code = append(code, + 0x60, 0x05, // PUSH1 5 (length) + 0x60, 0x1b, // PUSH1 27 (offset) + 0x60, 0x00, // PUSH1 0 (value) + 0xf0, // CREATE + 0x00, // STOP (discard result) + ) + + env := newBALTestEnv(types.GenesisAlloc{factory: {Code: code, Balance: common.Big0, Nonce: 1}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &factory, big.NewInt(0), 300_000, 0, nil)) + }) + + // Deployed address depends on the factory's nonce at the moment of CREATE, + // which is the factory's genesis nonce (1). + deployed := crypto.CreateAddress(factory, 1) + aa := assertPresent(t, b, deployed) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges) + } +} + +// ============================== SELFDESTRUCT ============================== + +// TestBALSelfDestructBeneficiaryWithZeroBalance: SELFDESTRUCT to a fresh +// beneficiary when the destructing account has 0 balance — both addresses are +// listed with empty change sets (no balance entry). +func TestBALSelfDestructBeneficiaryWithZeroBalance(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code performs SELFDESTRUCT to beneficiary inside the constructor, + // so EIP-6780's same-tx requirement is satisfied. The destructing account + // starts with balance 0 because the creation tx sends 0 value. + // PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, receipts := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(0), 200_000, 0, init)) + }) + + created := receipts[0].ContractAddress + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 0 { + t.Fatalf("zero-value SELFDESTRUCT must not credit beneficiary: %+v", ben.BalanceChanges) + } + cc := assertPresent(t, b, created) + if len(cc.BalanceChanges) != 0 { + t.Fatalf("destructing contract must not record a balance entry: %+v", cc.BalanceChanges) + } +} + +// TestBALSelfDestructBeneficiaryWithValueTransfer: SELFDESTRUCT from a freshly +// created contract that received positive value — beneficiary records the +// credit; destructing account's balance entry is omitted because its +// pre-transaction balance was 0. +func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) { + beneficiary := common.HexToAddress("0xbeefbeef") + env := newBALTestEnv(nil) + // Init code: PUSH20 SELFDESTRUCT + init := append([]byte{0x73}, beneficiary.Bytes()...) + init = append(init, 0xff) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, nil, big.NewInt(100), 200_000, 0, init)) + }) + + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 { + t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges) + } +} + +// TestBALSelfDestructPreExistingContract: SELFDESTRUCT on a pre-existing +// contract with positive balance records balance→0 for the contract and the +// corresponding credit on the beneficiary. EIP-6780 means the contract is +// only credited and not deleted, but its balance moves regardless. +func TestBALSelfDestructPreExistingContract(t *testing.T) { + suicidal := common.HexToAddress("0x5e1f") + beneficiary := common.HexToAddress("0xbeefbeef") + // PUSH20 SELFDESTRUCT + code := append([]byte{0x73}, beneficiary.Bytes()...) + code = append(code, 0xff) + + env := newBALTestEnv(types.GenesisAlloc{ + suicidal: {Code: code, Balance: big.NewInt(50)}, + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &suicidal, big.NewInt(0), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, suicidal) + if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() { + t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges) + } + ben := assertPresent(t, b, beneficiary) + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 { + t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges) + } +} + +// ============================== Mid-tx balance round-trip ============================== + +// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a +// transaction but returns to its pre-transaction value, the address is still +// listed in the BAL but MUST NOT have a balance entry. +func TestBALMidTxBalanceRoundTrip(t *testing.T) { + bouncer := common.HexToAddress("0xb0unce") + // On receiving value, the bouncer immediately CALLs CALLER with CALLVALUE + // and zero data. Net effect: bouncer.balance returns to its pre-tx value. + // + // PUSH1 0 (retSize) + // PUSH1 0 (retOff) + // PUSH1 0 (argsSize) + // PUSH1 0 (argsOff) + // CALLVALUE + // CALLER + // GAS + // CALL + // POP + // STOP + code := []byte{ + 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, + 0x34, // CALLVALUE + 0x33, // CALLER + 0x5a, // GAS + 0xf1, // CALL + 0x50, // POP + 0x00, // STOP + } + env := newBALTestEnv(types.GenesisAlloc{bouncer: {Code: code, Balance: common.Big0}}) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &bouncer, big.NewInt(1234), 200_000, 0, nil)) + }) + + aa := assertPresent(t, b, bouncer) + if len(aa.BalanceChanges) != 0 { + t.Fatalf("mid-tx round-trip must not record a balance entry: %+v", aa.BalanceChanges) + } +} + +// ============================== System contracts (pre/post-execution) ============================== + +// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses +// accessed during pre/post-execution" MUST be included in the BAL. That +// means all four of the post-merge system contracts touched by every +// Amsterdam block: +// +// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set) +// - EIP-2935 history storage (pre-execution) +// - EIP-7002 withdrawal queue (post-execution) +// - EIP-7251 consolidation queue (post-execution) +func TestBALSystemContractsPresent(t *testing.T) { + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + // SetCoinbase initialises b.bal; SetParentBeaconRoot triggers EIP-4788. + g.SetCoinbase(common.Address{0xc0}) + g.SetParentBeaconRoot(common.Hash{0xbe, 0xac}) + }) + + for _, sys := range []struct { + name string + addr common.Address + }{ + {"BeaconRoots (4788)", params.BeaconRootsAddress}, + {"HistoryStorage (2935)", params.HistoryStorageAddress}, + {"WithdrawalQueue (7002)", params.WithdrawalQueueAddress}, + {"ConsolidationQueue (7251)", params.ConsolidationQueueAddress}, + } { + if findAccount(b, sys.addr) == nil { + t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint()) + } + } +} + +// ============================== Withdrawals ============================== + +// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts +// the recipient in the BAL (with no balance entry). +func TestBALWithdrawalZeroAmountIncluded(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 0}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 0 { + t.Fatalf("zero-amount withdrawal must not record balance: %+v", r.BalanceChanges) + } +} + +// TestBALWithdrawalNonZeroAmountRecordsBalance: a positive-amount withdrawal +// records a balance change for the recipient. +func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) { + recipient := common.HexToAddress("0xdada") + env := newBALTestEnv(nil) + + b, _ := env.run(t, func(g *BlockGen) { + g.SetCoinbase(common.Address{0xc0}) + g.AddWithdrawal(&types.Withdrawal{Validator: 1, Address: recipient, Amount: 7}) + }) + + r := assertPresent(t, b, recipient) + if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 { + t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges) + } +} + +// ============================== EIP-7702 authority ============================== + +// TestBALAuthorityIncludedOnSetCodeTx: the authority of an EIP-7702 set-code +// transaction is added to the BAL once its delegation is loaded, recording +// both the nonce bump and the delegation-pointer code entry. +func TestBALAuthorityIncludedOnSetCodeTx(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) == 0 { + t.Fatalf("authority nonce should be bumped by delegation: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) == 0 { + t.Fatalf("authority code (delegation pointer) should be recorded: %+v", aa.CodeChanges) + } +} + +// TestBALDelegationTargetNotIncludedOnAuthOnly: the EIP-7702 delegation target +// MUST NOT appear in the BAL when only the authorization is installed and the +// target is never loaded as an execution target. +func TestBALDelegationTargetNotIncludedOnAuthOnly(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + delegate := common.HexToAddress("0xdeadbeef") // never accessed + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + tx := types.MustSignNewTx(env.key, env.signer, &types.SetCodeTx{ + ChainID: uint256.MustFromBig(env.cfg.ChainID), + Nonce: 0, + To: env.from, // tx.to is an EOA with no code: delegate is never called + Value: new(uint256.Int), + Gas: 200_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: []types.SetCodeAuthorization{auth}, + }) + g.AddTx(tx) + }) + + assertAbsent(t, b, delegate) +} + +// newSetCodeTx is a small constructor used by the multi-auth tests below. +func (e *balTestEnv) newSetCodeTx(t *testing.T, nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction { + t.Helper() + tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{ + ChainID: uint256.MustFromBig(e.cfg.ChainID), + Nonce: nonce, + To: to, + Value: new(uint256.Int), + Gas: 400_000, + GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())), + GasTipCap: new(uint256.Int), + AuthList: auths, + }), e.signer, e.key) + if err != nil { + t.Fatalf("sign SetCodeTx: %v", err) + } + return tx +} + +// TestBALAuthFailedBeforeLoadExcluded: an EIP-7702 auth whose ChainID check +// fails returns before the authority is loaded, so the authority address +// MUST NOT appear in the BAL. +func TestBALAuthFailedBeforeLoadExcluded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // wrong chain → fails ChainID check (pre-load) + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + assertAbsent(t, b, authority) +} + +// TestBALAuthFailedAfterLoadEmptyChangeSet: an EIP-7702 auth that fails the +// nonce check happens AFTER the authority's code is loaded (and the address +// added to accessed_addresses), so the authority MUST appear in the BAL — +// but with no nonce or code change. +func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + + // The authority's actual nonce is 0; supplying auth.Nonce=99 makes + // validation fail only after the code has been loaded. + auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.HexToAddress("0xdeadbeef"), + Nonce: 99, + }) + if err != nil { + t.Fatalf("sign auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 0 { + t.Fatalf("failed auth must not bump nonce: %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("failed auth must not record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and +// pre-load-failed auths lists only the loaded authorities in the BAL. +func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) { + env := newBALTestEnv(nil) + goodKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + badKey, _ := crypto.HexToECDSA("0303030303030303030303030303030303030303030303030303003030303030") + good := crypto.PubkeyToAddress(goodKey.PublicKey) + bad := crypto.PubkeyToAddress(badKey.PublicKey) + delegate := common.HexToAddress("0xdeadbeef") + + goodAuth, err := types.SignSetCode(goodKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign good auth: %v", err) + } + badAuth, err := types.SignSetCode(badKey, types.SetCodeAuthorization{ + ChainID: *uint256.NewInt(999), // fails before load + Address: delegate, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign bad auth: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{goodAuth, badAuth})) + }) + + assertPresent(t, b, good) // loaded → in BAL + assertAbsent(t, b, bad) // never loaded → not in BAL +} + +// TestBALAuthCodeRoundTripNoCodeEntry: two auths on the same authority that +// (1) install a delegation and (2) clear it again. Final code equals pre-tx +// code (empty), so the BAL records only the cumulative nonce bump and NO +// code change. +func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, // empty → A + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: common.Address{}, // delegation to zero clears the code (A → empty) + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } + if len(aa.CodeChanges) != 0 { + t.Fatalf("code round-trip (empty→A→empty) must NOT record a code change: %+v", aa.CodeChanges) + } +} + +// TestBALAuthCodeOverwrittenFinalRecorded: two auths on the same authority +// switching delegation A → B record exactly one code change carrying the +// final delegation pointer (B), not the intermediate value. +func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { + env := newBALTestEnv(nil) + authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020") + authority := crypto.PubkeyToAddress(authKey.PublicKey) + delegateA := common.HexToAddress("0xa11ce") + delegateB := common.HexToAddress("0xb0b0b0") + + auth1, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateA, + Nonce: 0, + }) + if err != nil { + t.Fatalf("sign auth1: %v", err) + } + auth2, err := types.SignSetCode(authKey, types.SetCodeAuthorization{ + ChainID: *uint256.MustFromBig(env.cfg.ChainID), + Address: delegateB, + Nonce: 1, + }) + if err != nil { + t.Fatalf("sign auth2: %v", err) + } + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth1, auth2})) + }) + + aa := assertPresent(t, b, authority) + if len(aa.CodeChanges) != 1 { + t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges) + } + want := types.AddressToDelegation(delegateB) + if !bytes.Equal(aa.CodeChanges[0].Code, want) { + t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code) + } + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) + } +} diff --git a/core/bintrie_witness_test.go b/core/bintrie_witness_test.go index 5f6239e4fa..b49ac83bb5 100644 --- a/core/bintrie_witness_test.go +++ b/core/bintrie_witness_test.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" @@ -202,7 +203,7 @@ func TestProcessParentBlockHash(t *testing.T) { } vmContext := NewEVMBlockContext(header, nil, new(common.Address)) evm := vm.NewEVM(vmContext, statedb, chainConfig, vm.Config{}) - ProcessParentBlockHash(header.ParentHash, evm) + ProcessParentBlockHash(header.ParentHash, evm, bal.NewConstructionBlockAccessList()) } // Read block hashes for block 0 .. num-1 for i := 0; i < num; i++ { diff --git a/core/block_validator.go b/core/block_validator.go index 008444fbbc..4086a2ead7 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -111,6 +111,28 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } } + // Block access list hash must be present in header after the + // Amsterdam hard fork. + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + // If the block does not include an access list, compute it locally during + // execution and validate it against the access list hash in the header. + // + // If the block includes an attached access list, validate it directly here. + if block.AccessList() != nil { + computed := block.AccessList().Hash() + if *block.Header().BlockAccessListHash != computed { + return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash) + } else if err := block.AccessList().Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } + } else if block.Header().BlockAccessListHash != nil || block.AccessList() != nil { + return errors.New("block had access list before Amsterdam") + } + // Ancestor block must be known. if !v.bc.HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { if !v.bc.HasBlock(block.ParentHash(), block.NumberU64()-1) { @@ -160,6 +182,23 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD } else if res.Requests != nil { return errors.New("block has requests before prague fork") } + // Verify Block-level accessList once Amsterdam is enabled + if v.config.IsAmsterdam(block.Number(), block.Time()) { + if res.Bal == nil { + return errors.New("block access list is not available in amsterdam") + } + if block.Header().BlockAccessListHash == nil { + return errors.New("block access list hash not set in header") + } + enc := res.Bal.ToEncodingObj() + local, remote := enc.Hash(), *block.Header().BlockAccessListHash + if local != remote { + return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote) + } + if err := enc.Validate(block.GasLimit()); err != nil { + return fmt.Errorf("invalid block access list: %v", err) + } + } // Validate the state root against the received state root and throw // an error if they don't match. if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { diff --git a/core/chain_makers.go b/core/chain_makers.go index cfd6302794..2e856b5161 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/params" @@ -50,6 +51,7 @@ type BlockGen struct { receipts []*types.Receipt uncles []*types.Header withdrawals []*types.Withdrawal + bal *bal.ConstructionBlockAccessList engine consensus.Engine } @@ -99,7 +101,7 @@ func (b *BlockGen) Difficulty() *big.Int { func (b *BlockGen) SetParentBeaconRoot(root common.Hash) { b.header.ParentBeaconRoot = &root blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) - ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{})) + ProcessBeaconBlockRoot(root, vm.NewEVM(blockContext, b.statedb, b.cm.config, vm.Config{}), b.bal) } // addTx adds a transaction to the generated block. If no coinbase has @@ -118,7 +120,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti evm = vm.NewEVM(blockContext, b.statedb, b.cm.config, vmConfig) ) b.statedb.SetTxContext(tx.Hash(), len(b.txs), uint32(len(b.txs)+1)) - receipt, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) + receipt, bal, err := ApplyTransaction(evm, b.gasPool, b.statedb, b.header, tx) if err != nil { panic(err) } @@ -134,6 +136,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti if b.header.BlobGasUsed != nil { *b.header.BlobGasUsed += receipt.BlobGasUsed } + b.bal.Merge(bal) } // AddTx adds a transaction to the generated block. If no coinbase has @@ -304,10 +307,11 @@ func (b *BlockGen) OffsetTime(seconds int64) { // ConsensusLayerRequests returns the EIP-7685 requests which have accumulated so far. func (b *BlockGen) ConsensusLayerRequests() [][]byte { - return b.collectRequests(true) + requests, _ := b.collectRequests(true) + return requests } -func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { +func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte, bal *bal.ConstructionBlockAccessList) { statedb := b.statedb if readonly { // The system contracts clear themselves on a system-initiated read. @@ -323,11 +327,11 @@ func (b *BlockGen) collectRequests(readonly bool) (requests [][]byte) { blockContext := NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) evm := vm.NewEVM(blockContext, statedb, b.cm.config, vm.Config{}) - requests, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) + requests, bal, err := PostExecution(context.Background(), b.cm.config, b.header.Number, b.header.Time, blockLogs, evm, uint32(len(b.txs)+1)) if err != nil { panic(fmt.Sprintf("failed to run post-execution: %v", err)) } - return requests + return requests, bal } // GenerateChain creates a chain of n blocks. The first block's @@ -354,6 +358,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse genblock := func(i int, parent *types.Block, triedb *triedb.Database, statedb *state.StateDB) (*types.Block, types.Receipts) { b := &BlockGen{i: i, cm: cm, parent: parent, statedb: statedb, engine: engine} b.header = cm.makeHeader(parent, statedb, b.engine) + b.bal = bal.NewConstructionBlockAccessList() // Set the difficulty for clique block. The chain maker doesn't have access // to a chain, so the difficulty will be left unset (nil). Set it here to the @@ -386,7 +391,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse blockContext := NewEVMBlockContext(b.header, cm, &b.header.Coinbase) blockContext.Random = &common.Hash{} // enable post-merge instruction set evm := vm.NewEVM(blockContext, statedb, cm.config, vm.Config{}) - ProcessParentBlockHash(b.header.ParentHash, evm) + ProcessParentBlockHash(b.header.ParentHash, evm, b.bal) } // Execute any user modifications to the block @@ -394,11 +399,12 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse gen(i, b) } - requests := b.collectRequests(false) + requests, bal := b.collectRequests(false) if requests != nil { reqHash := types.CalcRequestsHash(requests) b.header.RequestsHash = &reqHash } + b.bal.Merge(bal) body := types.Body{ Transactions: b.txs, @@ -414,8 +420,11 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse body.Withdrawals = make([]*types.Withdrawal, 0) } } + // Apply the consensus-specific post-transaction changes + b.engine.Finalize(cm, b.header, statedb, &body, uint32(len(b.txs)+1), b.bal) + // Assemble the block for delivery. - block := AssembleBlock(b.engine, cm, b.header, statedb, &body, b.receipts) + block := AssembleBlock(cm, b.header, statedb, &body, b.receipts, b.bal) // Write state changes to db root, err := statedb.Commit(b.header.Number.Uint64(), config.IsEIP158(b.header.Number), config.IsCancun(b.header.Number, b.header.Time)) diff --git a/core/genesis.go b/core/genesis.go index 6a0affa52e..e1c67e57c2 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -555,6 +555,7 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { if head.SlotNumber == nil { head.SlotNumber = new(uint64) } + head.BlockAccessListHash = &types.EmptyBlockAccessListHash } } return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) diff --git a/core/state_processor.go b/core/state_processor.go index 13466b7815..5690a152e7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/telemetry" @@ -81,13 +82,16 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated misc.ApplyDAOHardFork(tracingStateDB) } var ( - context = NewEVMBlockContext(header, p.chain, nil) - signer = types.MakeSigner(config, header.Number, header.Time) - evm = vm.NewEVM(context, tracingStateDB, config, cfg) + context = NewEVMBlockContext(header, p.chain, nil) + signer = types.MakeSigner(config, header.Number, header.Time) + evm = vm.NewEVM(context, tracingStateDB, config, cfg) + blockAccessList = bal.NewConstructionBlockAccessList() ) defer evm.Release() + // Run the pre-execution system calls - PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time()) + blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := TransactionToMessage(tx, signer, header.BaseFee) @@ -99,76 +103,92 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated telemetry.StringAttribute("tx.hash", tx.Hash().Hex()), telemetry.Int64Attribute("tx.index", int64(i)), ) - - receipt, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) + receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { spanEnd(&err) return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) + blockAccessList.Merge(bal) spanEnd(nil) } - requests, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) + requests, bal, err := PostExecution(ctx, config, block.Number(), block.Time(), allLogs, evm, uint32(len(block.Transactions())+1)) if err != nil { return nil, err } - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body()) + blockAccessList.Merge(bal) + + // Finalize the block, applying any consensus engine specific extras + // (e.g. block rewards). + // + // TODO(rjl493456442) integrate it into the PostExecution. + p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body(), uint32(len(block.Transactions())+1), blockAccessList) return &ProcessResult{ Receipts: receipts, Requests: requests, Logs: allLogs, GasUsed: gp.Used(), + Bal: blockAccessList, }, nil } // PreExecution processes pre-execution system calls. -func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) { +func PreExecution(ctx context.Context, beaconRoot *common.Hash, parent common.Hash, config *params.ChainConfig, evm *vm.EVM, number *big.Int, time uint64) *bal.ConstructionBlockAccessList { _, _, spanEnd := telemetry.StartSpan(ctx, "core.preExecution") defer spanEnd(nil) + var blockAccessList *bal.ConstructionBlockAccessList + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } // EIP-4788 if beaconRoot != nil { - ProcessBeaconBlockRoot(*beaconRoot, evm) + ProcessBeaconBlockRoot(*beaconRoot, evm, blockAccessList) } // EIP-2935 if config.IsPrague(number, time) || config.IsUBT(number, time) { - ProcessParentBlockHash(parent, evm) + ProcessParentBlockHash(parent, evm, blockAccessList) } + return blockAccessList } // PostExecution processes post-execution system calls when Prague is enabled. // If Prague is not activated, it returns null requests to differentiate from // empty requests. -func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, err error) { +func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.Int, time uint64, allLogs []*types.Log, evm *vm.EVM, blockAccessIndex uint32) (requests [][]byte, blockAccessList *bal.ConstructionBlockAccessList, err error) { _, _, spanEnd := telemetry.StartSpan(ctx, "core.postExecution") defer spanEnd(&err) + if config.IsAmsterdam(number, time) { + blockAccessList = bal.NewConstructionBlockAccessList() + } // Read requests if Prague is enabled. if config.IsPrague(number, time) { + rules := config.Rules(number, true, time) // IsMerge is always true + requests = [][]byte{} // EIP-6110 if err := ParseDepositLogs(&requests, allLogs, config); err != nil { - return nil, fmt.Errorf("failed to parse deposit logs: %w", err) + return nil, nil, fmt.Errorf("failed to parse deposit logs: %w", err) } // EIP-7002 - if err := ProcessWithdrawalQueue(&requests, evm, blockAccessIndex); err != nil { - return nil, fmt.Errorf("failed to process withdrawal queue: %w", err) + if err := ProcessWithdrawalQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process withdrawal queue: %w", err) } // EIP-7251 - if err := ProcessConsolidationQueue(&requests, evm, blockAccessIndex); err != nil { - return nil, fmt.Errorf("failed to process consolidation queue: %w", err) + if err := ProcessConsolidationQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil { + return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err) } } - return requests, nil + return requests, blockAccessList, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database // and uses the input parameters for its environment similar to ApplyTransaction. However, // this method takes an already created EVM instance as input. -func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, err error) { +func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, blockTime uint64, tx *types.Transaction, evm *vm.EVM) (receipt *types.Receipt, bal *bal.ConstructionBlockAccessList, err error) { if hooks := evm.Config.Tracer; hooks != nil { if hooks.OnTxStart != nil { hooks.OnTxStart(evm.GetVMContext(), tx, msg.From) @@ -180,12 +200,12 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { - return nil, err + return nil, nil, err } // Update the state with pending changes. var root []byte if evm.ChainConfig().IsByzantium(blockNumber) { - evm.StateDB.Finalise(true) + bal = evm.StateDB.Finalise(true) } else { root = statedb.IntermediateRoot(evm.ChainConfig().IsEIP158(blockNumber)).Bytes() } @@ -194,7 +214,7 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, if statedb.Database().Type().Is(state.TypeUBT) { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, gp.CumulativeUsed(), root), bal, nil } // MakeReceipt generates the receipt object for a transaction given its execution result. @@ -239,10 +259,10 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // and uses the input parameters for its environment. It returns the receipt // for the transaction and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, error) { +func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(evm.ChainConfig(), header.Number, header.Time), header.BaseFee) if err != nil { - return nil, err + return nil, nil, err } // Create a new context to be used in the EVM environment return ApplyTransactionWithEVM(msg, gp, statedb, header.Number, header.Hash(), header.Time, tx, evm) @@ -250,7 +270,7 @@ func ApplyTransaction(evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header * // ProcessBeaconBlockRoot applies the EIP-4788 system call to the beacon block root // contract. This method is exported to be used in tests. -func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { +func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -267,18 +287,19 @@ func ProcessBeaconBlockRoot(beaconRoot common.Hash, evm *vm.EVM) { Data: beaconRoot[:], } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.BeaconRootsAddress) _, _, _ = evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessParentBlockHash stores the parent block hash in the history storage contract // as per EIP-2935/7709. -func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { +func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM, blockAccessList *bal.ConstructionBlockAccessList) { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -295,6 +316,7 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { Data: prevHash.Bytes(), } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(evm.GetRules(), common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, 0) evm.StateDB.AddAddressToAccessList(params.HistoryStorageAddress) _, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) @@ -304,22 +326,22 @@ func ProcessParentBlockHash(prevHash common.Hash, evm *vm.EVM) { if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + blockAccessList.Merge(evm.StateDB.Finalise(true)) } // ProcessWithdrawalQueue calls the EIP-7002 withdrawal queue contract. // It returns the opaque request data returned by the contract. -func ProcessWithdrawalQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { - return processRequestsSystemCall(requests, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex) +func ProcessWithdrawalQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x01, params.WithdrawalQueueAddress, blockAccessIndex, blockAccessList) } // ProcessConsolidationQueue calls the EIP-7251 consolidation queue contract. // It returns the opaque request data returned by the contract. -func ProcessConsolidationQueue(requests *[][]byte, evm *vm.EVM, blockAccessIndex uint32) error { - return processRequestsSystemCall(requests, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex) +func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { + return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList) } -func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32) error { +func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error { if tracer := evm.Config.Tracer; tracer != nil { onSystemCallStart(tracer, evm.GetVMContext()) if tracer.OnSystemCallEnd != nil { @@ -335,16 +357,19 @@ func processRequestsSystemCall(requests *[][]byte, evm *vm.EVM, requestType byte To: &addr, } evm.SetTxContext(NewEVMTxContext(msg)) + evm.StateDB.Prepare(rules, common.Address{}, common.Address{}, nil, nil, nil) evm.StateDB.SetTxContext(common.Hash{}, 0, blockAccessIndex) evm.StateDB.AddAddressToAccessList(addr) ret, _, err := evm.Call(msg.From, *msg.To, msg.Data, vm.NewGasBudget(30_000_000), common.U2560) if evm.StateDB.AccessEvents() != nil { evm.StateDB.AccessEvents().Merge(evm.AccessEvents) } - evm.StateDB.Finalise(true) + bal := evm.StateDB.Finalise(true) if err != nil { return fmt.Errorf("system call failed to execute: %v", err) } + blockAccessList.Merge(bal) + if len(ret) == 0 { return nil // skip empty output } @@ -387,8 +412,16 @@ func onSystemCallStart(tracer *tracing.Hooks, ctx *tracing.VMContext) { // AssembleBlock finalizes the state and assembles the block with provided // body and receipts. -func AssembleBlock(engine consensus.Engine, chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) *types.Block { - engine.Finalize(chain, header, state, body) +func AssembleBlock(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt, blockAccessList *bal.ConstructionBlockAccessList) *types.Block { + // Assign the post-transition state root header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) - return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + + if !chain.Config().IsAmsterdam(header.Number, header.Time) { + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + } + // Assign the BlockAccessListHash if Amsterdam has been enabled + bal := blockAccessList.ToEncodingObj() + balHash := bal.Hash() + header.BlockAccessListHash = &balHash + return types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)).WithAccessListUnsafe(bal) } diff --git a/core/state_transition.go b/core/state_transition.go index 0a6994505d..51c5836892 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -608,7 +608,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) { // Execute the preparatory steps for state transition which includes: // - prepare accessList(post-berlin) - // - reset transient storage(eip 1153) + // - reset transient storage(EIP-1153) + // - enable block-level accessList construction (EIP-7928) st.state.Prepare(rules, msg.From, st.evm.Context.Coinbase, msg.To, vm.ActivePrecompiles(rules), msg.AccessList) var ( diff --git a/core/types.go b/core/types.go index 87bbfcff58..edbfc43db3 100644 --- a/core/types.go +++ b/core/types.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" ) @@ -58,4 +59,8 @@ type ProcessResult struct { Requests [][]byte Logs []*types.Log GasUsed uint64 + + // BAL is only meaningful for post-Amsterdam blocks. Please ensure + // fork validation is performed before accessing it. + Bal *bal.ConstructionBlockAccessList } diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index 9cbc1faeb9..2eb5fe93cd 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -138,10 +138,62 @@ func (b *ConstructionBlockAccessList) BalanceChange(txIdx uint32, address common // PrettyPrint returns a human-readable representation of the access list func (b *ConstructionBlockAccessList) PrettyPrint() string { - enc := b.toEncodingObj() + enc := b.ToEncodingObj() return enc.PrettyPrint() } +// Merge applies other on top of the local block access list. For colliding +// entries (a (slot, txIdx) write or a txIdx-keyed balance/nonce/code change), +// the value from other wins, matching the semantics of applying the local +// effects first and then other's. Storage reads are unioned; any slot +// written by either side is dropped from StorageReads. +// +// Typically each list covers its own tx index, so txIdx-level collisions are +// not expected; the exception is pre/post-transition system calls, which +// share a single tx index. In that case callers must pass block-accessList +// in order strictly. +// +// other is referenced (not deep copied), after the call both lists share +// inner maps and other must not be mutated. +func (b *ConstructionBlockAccessList) Merge(other *ConstructionBlockAccessList) { + if other == nil { + return + } + for addr, otherAcc := range other.Accounts { + acc, ok := b.Accounts[addr] + if !ok { + b.Accounts[addr] = otherAcc + continue + } + for key, writes := range otherAcc.StorageWrites { + existing, ok := acc.StorageWrites[key] + if !ok { + acc.StorageWrites[key] = writes + } else { + for txIdx, value := range writes { + existing[txIdx] = value + } + } + delete(acc.StorageReads, key) + } + for key := range otherAcc.StorageReads { + if _, ok := acc.StorageWrites[key]; ok { + continue + } + acc.StorageReads[key] = struct{}{} + } + for txIdx, balance := range otherAcc.BalanceChanges { + acc.BalanceChanges[txIdx] = balance + } + for txIdx, nonce := range otherAcc.NonceChanges { + acc.NonceChanges[txIdx] = nonce + } + for txIdx, code := range otherAcc.CodeChange { + acc.CodeChange[txIdx] = code + } + } +} + // Copy returns a deep copy of the access list. func (b *ConstructionBlockAccessList) Copy() *ConstructionBlockAccessList { res := NewConstructionBlockAccessList() diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 03f97f3809..399f9db7c0 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -78,17 +78,43 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate(rules params.Rules) error { +func (e *BlockAccessList) Validate(blockGasLimit uint64) error { if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } for _, entry := range *e { - if err := entry.validate(rules); err != nil { + if err := entry.validate(); err != nil { return err } } + return e.ValidateSize(blockGasLimit) +} + +// itemCount returns the number of items in the BAL for EIP-7928 size-constraint +// purposes: the count of distinct addresses plus every storage key (writes + +// reads) carried by those accounts. A storage slot is counted once regardless +// of how many transactions wrote to it. +func (e *BlockAccessList) itemCount() uint64 { + count := uint64(len(*e)) // distinct addresses + for i := range *e { + count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads)) + } + return count +} + +// ValidateSize returns an error if the BAL violates the EIP-7928 size +// constraint for the given block gas limit: +// +// itemCount() <= blockGasLimit / params.BALItemCost +func (e *BlockAccessList) ValidateSize(blockGasLimit uint64) error { + items := e.itemCount() + limit := blockGasLimit / params.BALItemCost + if items > limit { + return fmt.Errorf("block access list exceeds size constraint: items=%d, limit=%d (block gas limit %d / %d)", + items, limit, blockGasLimit, params.BALItemCost) + } return nil } @@ -159,7 +185,7 @@ type AccountAccess struct { // validate converts the account accesses out of encoding format. // If any of the keys in the encoding object are not ordered according to the // spec, an error is returned. -func (e *AccountAccess) validate(rules params.Rules) error { +func (e *AccountAccess) validate() error { // Check the storage write slots are sorted in order if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { return a.Slot.Cmp(b.Slot) @@ -200,14 +226,7 @@ func (e *AccountAccess) validate(rules params.Rules) error { return errors.New("code changes not in ascending order by tx index") } for _, change := range e.CodeChanges { - var sizeLimit int - switch { - case rules.IsAmsterdam: - sizeLimit = params.MaxCodeSizeAmsterdam - default: - sizeLimit = params.MaxCodeSize - } - if len(change.Code) > sizeLimit { + if len(change.Code) > params.MaxCodeSizeAmsterdam { return errors.New("code change contained oversized code") } } @@ -257,7 +276,7 @@ func (e *AccountAccess) Copy() AccountAccess { // EncodeRLP returns the RLP-encoded access list func (b *ConstructionBlockAccessList) EncodeRLP(wr io.Writer) error { - return b.toEncodingObj().EncodeRLP(wr) + return b.ToEncodingObj().EncodeRLP(wr) } var _ rlp.Encoder = &ConstructionBlockAccessList{} @@ -340,9 +359,9 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc return res } -// toEncodingObj returns an instance of the access list expressed as the type +// ToEncodingObj returns an instance of the access list expressed as the type // which is used as input for the encoding/decoding. -func (b *ConstructionBlockAccessList) toEncodingObj() *BlockAccessList { +func (b *ConstructionBlockAccessList) ToEncodingObj() *BlockAccessList { var addresses []common.Address for addr := range b.Accounts { addresses = append(addresses, addr) diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 32a0292f2e..2b6a3c194e 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -19,6 +19,7 @@ package bal import ( "bytes" "cmp" + "math" "reflect" "slices" "testing" @@ -98,14 +99,65 @@ func TestBALEncoding(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("decoding failed: %v\n", err) } - if dec.Hash() != bal.toEncodingObj().Hash() { + if dec.Hash() != bal.ToEncodingObj().Hash() { t.Fatalf("encoded block hash doesn't match decoded") } - if !reflect.DeepEqual(bal.toEncodingObj(), &dec) { + if !reflect.DeepEqual(bal.ToEncodingObj(), &dec) { t.Fatal("decoded BAL doesn't match") } } +func TestConstructionBALMerge(t *testing.T) { + var ( + addrA = common.BytesToAddress([]byte{0xAA}) + addrB = common.BytesToAddress([]byte{0xBB}) + slot1 = common.BytesToHash([]byte{0x01}) + slot2 = common.BytesToHash([]byte{0x02}) + slot3 = common.BytesToHash([]byte{0x03}) + ) + a := NewConstructionBlockAccessList() + a.StorageWrite(1, addrA, slot1, common.BytesToHash([]byte{0x11})) + a.StorageRead(addrA, slot2) // demoted by other's write below + a.BalanceChange(1, addrA, uint256.NewInt(100)) + a.NonceChange(addrA, 1, 7) + + b := NewConstructionBlockAccessList() + b.StorageWrite(2, addrA, slot1, common.BytesToHash([]byte{0x22})) // same slot, disjoint txIdx + b.StorageWrite(2, addrA, slot2, common.BytesToHash([]byte{0x33})) + b.StorageRead(addrA, slot3) + b.BalanceChange(2, addrA, uint256.NewInt(200)) + b.NonceChange(addrA, 2, 8) + b.CodeChange(addrB, 2, []byte{0xde, 0xad}) // account only in other + + a.Merge(b) + + accA := a.Accounts[addrA] + wantWrites := map[common.Hash]map[uint32]common.Hash{ + slot1: {1: common.BytesToHash([]byte{0x11}), 2: common.BytesToHash([]byte{0x22})}, + slot2: {2: common.BytesToHash([]byte{0x33})}, + } + if !reflect.DeepEqual(accA.StorageWrites, wantWrites) { + t.Fatalf("storage writes mismatch: got %v, want %v", accA.StorageWrites, wantWrites) + } + wantReads := map[common.Hash]struct{}{slot3: {}} + if !reflect.DeepEqual(accA.StorageReads, wantReads) { + t.Fatalf("storage reads mismatch: got %v, want %v", accA.StorageReads, wantReads) + } + if accA.BalanceChanges[1].Uint64() != 100 || accA.BalanceChanges[2].Uint64() != 200 { + t.Fatalf("balance changes mismatch: %v", accA.BalanceChanges) + } + if accA.NonceChanges[1] != 7 || accA.NonceChanges[2] != 8 { + t.Fatalf("nonce changes mismatch: %v", accA.NonceChanges) + } + accB, ok := a.Accounts[addrB] + if !ok { + t.Fatal("account only present in other was not adopted") + } + if !bytes.Equal(accB.CodeChange[2], []byte{0xde, 0xad}) { + t.Fatalf("code change for adopted account missing: %x", accB.CodeChange[2]) + } +} + func makeTestAccountAccess(sort bool) AccountAccess { var ( storageWrites []encodingSlotWrites @@ -231,10 +283,82 @@ func TestBlockAccessListCopy(t *testing.T) { } } +func TestBlockAccessListItemCount(t *testing.T) { + empty := &BlockAccessList{} + if got := empty.itemCount(); got != 0 { + t.Fatalf("empty BAL item count: got %d, want 0", got) + } + + addr1 := [20]byte(testrand.Bytes(20)) + addr2 := [20]byte(testrand.Bytes(20)) + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := &BlockAccessList{ + AccountAccess{ + Address: addr1, + StorageWrites: []encodingSlotWrites{ + {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}}, + {Slot: one()}, + }, + StorageReads: []*uint256.Int{one()}, + }, + AccountAccess{Address: addr2}, // address-only, no slots + } + // 2 addresses + 2 write-slots + 1 read-slot = 5 items. + // (Multiple TxIdx writes to the same slot count as ONE item.) + if got := bal.itemCount(); got != 5 { + t.Fatalf("item count: got %d, want 5", got) + } +} + +func TestBlockAccessListValidateSize(t *testing.T) { + // Build a BAL with exactly 30 items: 3 addresses, each with 9 storage + // slots (some writes, some reads). 3 + 9*3 = 30. + one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } + bal := make(BlockAccessList, 3) + for i := range bal { + bal[i].Address = [20]byte(testrand.Bytes(20)) + for j := 0; j < 5; j++ { + bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{ + Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}}, + }) + } + for j := 0; j < 4; j++ { + bal[i].StorageReads = append(bal[i].StorageReads, one()) + } + } + if got := bal.itemCount(); got != 30 { + t.Fatalf("setup: item count = %d, want 30", got) + } + + // limit = blockGasLimit / BALItemCost. + // 30 items requires limit >= 30, i.e. gasLimit >= 30 * 2000 = 60_000. + tests := []struct { + name string + gasLimit uint64 + expectError bool + }{ + {"exactly at limit", 30 * params.BALItemCost, false}, + {"well above limit", 60_000_000, false}, + {"one below limit", 30*params.BALItemCost - 1, true}, + {"zero gas limit", 0, true}, + } + for _, tc := range tests { + err := bal.ValidateSize(tc.gasLimit) + if (err != nil) != tc.expectError { + t.Errorf("%s: got err=%v, expectError=%v", tc.name, err, tc.expectError) + } + } + + // Empty BAL is always valid (even with 0 gas limit). + if err := (&BlockAccessList{}).ValidateSize(0); err != nil { + t.Fatalf("empty BAL must pass any limit: %v", err) + } +} + func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) - if err := enc.Validate(params.Rules{}); err != nil { + if err := enc.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -246,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(params.Rules{}); err != nil { + if err := dec.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() - listB := cBAL.toEncodingObj() - if err := listB.Validate(params.Rules{}); err != nil { + listB := cBAL.ToEncodingObj() + if err := listB.Validate(math.MaxUint64); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } diff --git a/core/types/block.go b/core/types/block.go index ea576ed232..0856845a4e 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -413,8 +413,9 @@ func (b *Block) BaseFee() *big.Int { return new(big.Int).Set(b.header.BaseFee) } -func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } -func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } +func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash } func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 diff --git a/core/types/hashes.go b/core/types/hashes.go index db8912a66f..541681e4db 100644 --- a/core/types/hashes.go +++ b/core/types/hashes.go @@ -43,6 +43,9 @@ var ( // EmptyRequestsHash is the known hash of an empty request set, sha256(""). EmptyRequestsHash = common.HexToHash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + // EmptyBlockAccessListHash is the known hash of an empty block accessList, keccak256(rlp.encode([])). + EmptyBlockAccessListHash = common.HexToHash("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + // EmptyBinaryHash is the known hash of an empty binary trie. EmptyBinaryHash = common.Hash{} ) diff --git a/core/vm/evm.go b/core/vm/evm.go index 9fe6faa3a2..832306b9a0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -709,3 +709,8 @@ func (evm *EVM) GetVMContext() *tracing.VMContext { StateDB: evm.StateDB, } } + +// GetRules returns the chain rules used throughout the EVM execution. +func (evm *EVM) GetRules() params.Rules { + return evm.chainRules +} diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 0df02388b3..88132b4b63 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -1018,7 +1018,7 @@ func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *cor // Call Prepare to clear out the statedb access list statedb.SetTxContext(txctx.TxHash, txctx.TxIndex, uint32(txctx.TxIndex+1)) - _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) + _, _, err = core.ApplyTransactionWithEVM(message, core.NewGasPool(message.GasLimit), statedb, vmctx.BlockNumber, txctx.BlockHash, vmctx.Time, tx, evm) if err != nil { return nil, fmt.Errorf("tracing failed: %w", err) } diff --git a/eth/tracers/internal/tracetest/selfdestruct_state_test.go b/eth/tracers/internal/tracetest/selfdestruct_state_test.go index 692c5eb775..39067e8efc 100644 --- a/eth/tracers/internal/tracetest/selfdestruct_state_test.go +++ b/eth/tracers/internal/tracetest/selfdestruct_state_test.go @@ -620,7 +620,7 @@ func TestSelfdestructStateTracer(t *testing.T) { } context := core.NewEVMBlockContext(block.Header(), blockchain, nil) evm := vm.NewEVM(context, hookedState, tt.genesis.Config, vm.Config{Tracer: tracer.Hooks()}) - _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) + _, _, err = core.ApplyTransactionWithEVM(msg, core.NewGasPool(msg.GasLimit), statedb, block.Number(), block.Hash(), block.Time(), tx, evm) if err != nil { t.Fatalf("failed to execute transaction: %v", err) } diff --git a/internal/ethapi/simulate.go b/internal/ethapi/simulate.go index fa2ff2c32b..8462194b1d 100644 --- a/internal/ethapi/simulate.go +++ b/internal/ethapi/simulate.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/ethapi/override" "github.com/ethereum/go-ethereum/params" @@ -292,9 +293,10 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, gp = core.NewGasPool(blockContext.GasLimit) blobGasUsed uint64 - txes = make([]*types.Transaction, len(block.Calls)) - callResults = make([]simCallResult, len(block.Calls)) - receipts = make([]*types.Receipt, len(block.Calls)) + txes = make([]*types.Transaction, len(block.Calls)) + callResults = make([]simCallResult, len(block.Calls)) + receipts = make([]*types.Receipt, len(block.Calls)) + blockAccessList = bal.NewConstructionBlockAccessList() // Block hash will be repaired after execution. tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0) @@ -313,13 +315,14 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } evm := vm.NewEVM(blockContext, tracingStateDB, sim.chainConfig, *vmConfig) defer evm.Release() + // It is possible to override precompiles with EVM bytecode, or // move them to another address. if precompiles != nil { evm.SetPrecompiles(precompiles) } // Run pre-execution system calls - core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time) + blockAccessList.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, sim.chainConfig, evm, header.Number, header.Time)) var allLogs []*types.Log for i, call := range block.Calls { @@ -350,7 +353,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, // Update the state with pending changes. var root []byte if sim.chainConfig.IsByzantium(blockContext.BlockNumber) { - tracingStateDB.Finalise(true) + blockAccessList.Merge(tracingStateDB.Finalise(true)) } else { root = sim.state.IntermediateRoot(sim.chainConfig.IsEIP158(blockContext.BlockNumber)).Bytes() } @@ -391,7 +394,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } // Process EIP-7685 requests - requests, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) + requests, bal, err := core.PostExecution(ctx, sim.chainConfig, header.Number, header.Time, allLogs, evm, uint32(len(block.Calls)+1)) if err != nil { return nil, nil, nil, err } @@ -399,6 +402,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, reqHash := types.CalcRequestsHash(requests) header.RequestsHash = &reqHash } + blockAccessList.Merge(bal) blockBody := &types.Body{ Transactions: txes, @@ -411,8 +415,11 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header, } chainHeadReader := &simChainHeadReader{ctx, sim.b} + // Apply the consensus-specific post-transaction changes + sim.b.Engine().Finalize(chainHeadReader, header, sim.state, blockBody, uint32(len(block.Calls)+1), blockAccessList) + // Assemble the block - b := core.AssembleBlock(sim.b.Engine(), chainHeadReader, header, sim.state, blockBody, receipts) + b := core.AssembleBlock(chainHeadReader, header, sim.state, blockBody, receipts, blockAccessList) repairLogs(callResults, b.Hash()) return b, callResults, senders, nil diff --git a/miner/worker.go b/miner/worker.go index 1ecee96688..21bc95cf92 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/log" @@ -71,6 +72,7 @@ type environment struct { receipts []*types.Receipt sidecars []*types.BlobTxSidecar blobs int + bal *bal.ConstructionBlockAccessList witness *stateless.Witness } @@ -208,7 +210,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, } // Collect consensus-layer requests if Prague is enabled. - requests, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) + requests, bal, err := core.PostExecution(ctx, miner.chainConfig, work.header.Number, work.header.Time, allLogs, work.evm, uint32(work.tcount+1)) if err != nil { return &newPayloadResult{err: err} } @@ -216,9 +218,14 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, reqHash := types.CalcRequestsHash(requests) work.header.RequestsHash = &reqHash } + work.bal.Merge(bal) + + // Apply the consensus-specific post-transaction changes + miner.engine.Finalize(miner.chain, work.header, work.state, &body, uint32(work.tcount+1), work.bal) + // Assemble the block for delivery. _, _, assembleSpanEnd := telemetry.StartSpan(ctx, "miner.AssembleBlock") - block := core.AssembleBlock(miner.engine, miner.chain, work.header, work.state, &body, work.receipts) + block := core.AssembleBlock(miner.chain, work.header, work.state, &body, work.receipts, work.bal) assembleSpanEnd(nil) return &newPayloadResult{ @@ -318,7 +325,7 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams, return nil, err } // Run pre-execution system calls - core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time) + env.bal.Merge(core.PreExecution(ctx, header.ParentBeaconRoot, header.ParentHash, miner.chainConfig, env.evm, header.Number, header.Time)) return env, nil } @@ -337,6 +344,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase } } state.StartPrefetcher("miner", bundle) + // Note the passed coinbase may be different with header.Coinbase. return &environment{ signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), @@ -345,6 +353,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, gasPool: core.NewGasPool(header.GasLimit), header: header, + bal: bal.NewConstructionBlockAccessList(), witness: state.Witness(), evm: vm.NewEVM(core.NewEVMBlockContext(header, miner.chain, &coinbase), state, miner.chainConfig, vm.Config{}), }, nil @@ -356,7 +365,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx if tx.Type() == types.BlobTxType { return miner.commitBlobTransaction(env, tx) } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -364,6 +373,7 @@ func (miner *Miner) commitTransaction(ctx context.Context, env *environment, tx env.receipts = append(env.receipts, receipt) env.size += tx.Size() env.tcount++ + env.bal.Merge(bal) return nil } @@ -380,7 +390,7 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio if env.blobs+len(sc.Blobs) > maxBlobs { return errors.New("max data blobs reached") } - receipt, err := miner.applyTransaction(env, tx) + receipt, bal, err := miner.applyTransaction(env, tx) if err != nil { return err } @@ -392,23 +402,24 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio env.size += txNoBlob.Size() *env.header.BlobGasUsed += receipt.BlobGasUsed env.tcount++ + env.bal.Merge(bal) return nil } // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. -func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { +func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, *bal.ConstructionBlockAccessList, error) { var ( snap = env.state.Snapshot() gp = env.gasPool.Snapshot() ) - receipt, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) + receipt, bal, err := core.ApplyTransaction(env.evm, env.gasPool, env.state, env.header, tx) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.Set(gp) - return nil, err + return nil, nil, err } env.header.GasUsed = env.gasPool.Used() - return receipt, nil + return receipt, bal, nil } func (miner *Miner) commitTransactions(ctx context.Context, env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { diff --git a/params/protocol_params.go b/params/protocol_params.go index 9da275c486..3e36b83547 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -186,6 +186,16 @@ const ( HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935. MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block + + // BALItemCost is the gas-cost divisor for the EIP-7928 block access list + // size constraint: bal_items <= block_gas_limit / BALItemCost, where + // bal_items counts every distinct address in the BAL plus every storage + // key (writes + reads) carried by those accounts. + // + // The value (2000) is set deliberately below COLD_SLOAD_COST (2100) so + // the bound has a small safety margin for system-contract accesses that + // don't consume block gas. + BALItemCost uint64 = 2000 ) // Bls12381G1MultiExpDiscountTable is the gas discount table for BLS12-381 G1 multi exponentiation operation From a484a8506dd5106f14bc28cd0fb9c4ab74d6c69d Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Tue, 19 May 2026 20:25:13 +0200 Subject: [PATCH 10/76] eth/protocols/eth: implement eth71 bal response (#34879) This PR implements the serving side of the eth71 BAL exchange messages. Until commit 4cd7092 also contained the requesting side, but since that part still needs more work, I'm splitting it out into a separate PR. The test injects BALs directly into rawdb. This can be removed once BAL generation is integrated into the chain maker. --------- Co-authored-by: Felix Lange --- core/blockchain_reader.go | 1 + eth/protocols/eth/handler.go | 21 ++++++++++ eth/protocols/eth/handler_test.go | 62 +++++++++++++++++++++++++++++ eth/protocols/eth/handlers.go | 65 +++++++++++++++++++++++++++++++ eth/protocols/eth/peer.go | 30 ++++++++++++++ eth/protocols/eth/protocol.go | 32 ++++++++++++++- 6 files changed, 209 insertions(+), 2 deletions(-) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 18afa9ce9d..a540bbc11d 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -296,6 +296,7 @@ func (bc *BlockChain) GetReceiptsRLP(hash common.Hash) rlp.RawValue { return rawdb.ReadReceiptsRLP(bc.db, hash, number) } +// GetAccessListRLP retrieves the block access list of a block in RLP encoding. func (bc *BlockChain) GetAccessListRLP(hash common.Hash) rlp.RawValue { number, ok := rawdb.ReadHeaderNumber(bc.db, hash) if !ok { diff --git a/eth/protocols/eth/handler.go b/eth/protocols/eth/handler.go index f7d25bd8ca..154b75130c 100644 --- a/eth/protocols/eth/handler.go +++ b/eth/protocols/eth/handler.go @@ -53,6 +53,9 @@ const ( // containing 200+ transactions nowadays, the practical limit will always // be softResponseLimit. maxReceiptsServe = 1024 + + // maxBALsServe is the maximum number of block access lists to serve. + maxBALsServe = 1024 ) // Handler is a callback to invoke from an outside runner after the boilerplate @@ -197,6 +200,22 @@ var eth70 = map[uint64]msgHandler{ BlockRangeUpdateMsg: handleBlockRangeUpdate, } +var eth71 = map[uint64]msgHandler{ + TransactionsMsg: handleTransactions, + NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes, + GetBlockHeadersMsg: handleGetBlockHeaders, + BlockHeadersMsg: handleBlockHeaders, + GetBlockBodiesMsg: handleGetBlockBodies, + BlockBodiesMsg: handleBlockBodies, + GetReceiptsMsg: handleGetReceipts70, + ReceiptsMsg: handleReceipts70, + GetPooledTransactionsMsg: handleGetPooledTransactions, + PooledTransactionsMsg: handlePooledTransactions, + BlockRangeUpdateMsg: handleBlockRangeUpdate, + GetBlockAccessListsMsg: handleGetBlockAccessLists, + BlockAccessListsMsg: handleBlockAccessLists, +} + // handleMessage is invoked whenever an inbound message is received from a remote // peer. The remote connection is torn down upon returning any error. func handleMessage(backend Backend, peer *Peer) error { @@ -216,6 +235,8 @@ func handleMessage(backend Backend, peer *Peer) error { handlers = eth69 case ETH70: handlers = eth70 + case ETH71: + handlers = eth71 default: return fmt.Errorf("unknown eth protocol version: %v", peer.version) } diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index d056d121d9..3f40fdb3b3 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -37,6 +37,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool/blobpool" "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/ethdb" @@ -709,6 +710,67 @@ func testGetBlockPartialReceipts(t *testing.T, protocol int) { } } +// makeTestBAL creates a BAL with a given address access and balance change, +// and returns its RLP encoding. This is used for injection into the chain DB via +// rawdb.WriteAccessListRLP. +// TODO: Should be deleted when bal is integrated with chain maker. +func makeTestBAL(t *testing.T, addr common.Address) rlp.RawValue { + cb := bal.NewConstructionBlockAccessList() + cb.AccountRead(addr) + cb.BalanceChange(0, addr, uint256.NewInt(1)) + var buf bytes.Buffer + if err := cb.EncodeRLP(&buf); err != nil { + t.Fatalf("failed to encode BAL: %v", err) + } + return buf.Bytes() +} + +// TestGetBlockAccessLists checks serving part of bal exchange +func TestGetBlockAccessLists(t *testing.T) { testGetBlockAccessLists(t, ETH71) } + +func testGetBlockAccessLists(t *testing.T, protocol uint) { + t.Parallel() + + backend := newTestBackend(5) + defer backend.close() + + peer, _ := newTestPeer("peer", protocol, backend) + defer peer.close() + + bal1 := makeTestBAL(t, common.Address{0x11}) + bal2 := makeTestBAL(t, common.Address{0x22}) + + var ( + hashes []common.Hash + expect rlp.RawList[RawBlockAccessList] + ) + for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ { + block := backend.chain.GetBlockByNumber(i) + hashes = append(hashes, block.Hash()) + switch i { + case 1: + rawdb.WriteAccessListRLP(backend.db, block.Hash(), i, bal1) + expect.AppendRaw(bal1) + case 3: + rawdb.WriteAccessListRLP(backend.db, block.Hash(), i, bal2) + expect.AppendRaw(bal2) + default: + expect.AppendRaw(rlp.EmptyString) + } + } + + p2p.Send(peer.app, GetBlockAccessListsMsg, &GetBlockAccessListsPacket{ + RequestId: 123, + GetBlockAccessListsRequest: hashes, + }) + if err := p2p.ExpectMsg(peer.app, BlockAccessListsMsg, &BlockAccessListPacket{ + RequestId: 123, + List: expect, + }); err != nil { + t.Errorf("BAL response mismatch: %v", err) + } +} + type decoder struct { msg []byte } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 3254a0abc2..71942cc9ad 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -666,3 +666,68 @@ func handleBlockRangeUpdate(backend Backend, msg Decoder, peer *Peer) error { peer.lastRange.Store(&update) return nil } + +// handleGetBlockAccessLists serves a GetBlockAccessLists request. +func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { + var query GetBlockAccessListsPacket + if err := msg.Decode(&query); err != nil { + return err + } + response := serviceGetBlockAccessListsQuery(backend.Chain(), query.GetBlockAccessListsRequest) + return peer.ReplyBlockAccessLists(query.RequestId, response) +} + +// serviceGetBlockAccessListsQuery assembles the response to a BAL query. +// Unavailable BALs are returned as empty list entries. +func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[RawBlockAccessList] { + var ( + bytes int + bals rlp.RawList[RawBlockAccessList] + ) + for _, hash := range query { + if bytes >= softResponseLimit || bals.Len() >= maxBALsServe { + break + } + data := chain.GetAccessListRLP(hash) + if len(data) == 0 { + // The signal for missing BAL is the empty string, because + // an empty list is also a valid BAL. + bals.AppendRaw(rlp.EmptyString) + continue + } + bals.AppendRaw(data) + bytes += len(data) + } + return bals +} + +// handleBlockAccessLists processes an incoming BlockAccessLists response, +// validates it against the request tracker, and dispatches it to the waiting caller. +func handleBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { + res := new(BlockAccessListPacket) + if err := msg.Decode(res); err != nil { + return err + } + tresp := tracker.Response{ID: res.RequestId, MsgCode: BlockAccessListsMsg, Size: res.List.Len()} + if err := peer.tracker.Fulfil(tresp); err != nil { + return fmt.Errorf("BlockAccessLists: %w", err) + } + bals, err := res.List.Items() + if err != nil { + return fmt.Errorf("BlockAccessLists: %w", err) + } + + metadata := func() interface{} { + hashes := make([]common.Hash, len(bals)) + for i := range bals { + hashes[i] = crypto.Keccak256Hash(bals[i].Bytes()) + } + return hashes + } + + return peer.dispatchResponse(&Response{ + id: res.RequestId, + code: BlockAccessListsMsg, + Res: (*BlockAccessListResponse)(&bals), + }, metadata) +} diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 754fd02be3..2d7079fa12 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -251,6 +251,36 @@ func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], }) } +// ReplyBlockAccessLists is the response to GetBlockAccessLists (EIP-8159). +func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[RawBlockAccessList]) error { + return p2p.Send(p.rw, BlockAccessListsMsg, &BlockAccessListPacket{ + RequestId: id, + List: list, + }) +} + +// RequestBALs fetches block access lists for the given block hashes (EIP-8159) +func (p *Peer) RequestBALs(hashes []common.Hash, sink chan *Response) (*Request, error) { + p.Log().Debug("Fetching block access lists", "count", len(hashes)) + id := rand.Uint64() + + req := &Request{ + id: id, + sink: sink, + code: GetBlockAccessListsMsg, + want: BlockAccessListsMsg, + numItems: len(hashes), + data: &GetBlockAccessListsPacket{ + RequestId: id, + GetBlockAccessListsRequest: hashes, + }, + } + if err := p.dispatchRequest(req); err != nil { + return nil, err + } + return req, nil +} + // RequestOneHeader is a wrapper around the header query functions to fetch a // single header. It is used solely by the fetcher. func (p *Peer) RequestOneHeader(hash common.Hash, sink chan *Response) (*Request, error) { diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go index 0df0776c27..a6c45f83ec 100644 --- a/eth/protocols/eth/protocol.go +++ b/eth/protocols/eth/protocol.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/forkid" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/rlp" ) @@ -31,6 +32,7 @@ import ( const ( ETH69 = 69 ETH70 = 70 + ETH71 = 71 ) // ProtocolName is the official short name of the `eth` protocol used during @@ -39,11 +41,11 @@ const ProtocolName = "eth" // ProtocolVersions are the supported versions of the `eth` protocol (first // is primary). -var ProtocolVersions = []uint{ETH70, ETH69} +var ProtocolVersions = []uint{ETH71, ETH70, ETH69} // protocolLengths are the number of implemented message corresponding to // different protocol versions. -var protocolLengths = map[uint]uint64{ETH69: 18, ETH70: 18} +var protocolLengths = map[uint]uint64{ETH71: 20, ETH69: 18, ETH70: 18} // maxMessageSize is the maximum cap on the size of a protocol message. const maxMessageSize = 10 * 1024 * 1024 @@ -66,6 +68,8 @@ const ( GetReceiptsMsg = 0x0f ReceiptsMsg = 0x10 BlockRangeUpdateMsg = 0x11 + GetBlockAccessListsMsg = 0x12 + BlockAccessListsMsg = 0x13 ) var ( @@ -288,6 +292,24 @@ type BlockRangeUpdatePacket struct { LatestBlockHash common.Hash } +type GetBlockAccessListsRequest []common.Hash + +type GetBlockAccessListsPacket struct { + RequestId uint64 + GetBlockAccessListsRequest +} + +type RawBlockAccessList struct { + rlp.RawList[bal.AccountAccess] +} + +type BlockAccessListResponse []RawBlockAccessList + +type BlockAccessListPacket struct { + RequestId uint64 + List rlp.RawList[RawBlockAccessList] +} + func (*StatusPacket) Name() string { return "Status" } func (*StatusPacket) Kind() byte { return StatusMsg } @@ -326,3 +348,9 @@ func (*ReceiptsRLPResponse) Kind() byte { return ReceiptsMsg } func (*BlockRangeUpdatePacket) Name() string { return "BlockRangeUpdate" } func (*BlockRangeUpdatePacket) Kind() byte { return BlockRangeUpdateMsg } + +func (*GetBlockAccessListsRequest) Name() string { return "GetBlockAccessLists" } +func (*GetBlockAccessListsRequest) Kind() byte { return GetBlockAccessListsMsg } + +func (*BlockAccessListResponse) Name() string { return "BlockAccessLists" } +func (*BlockAccessListResponse) Kind() byte { return BlockAccessListsMsg } From 50ae34c1d8ec2149774a9c6cf597a11498bd8d39 Mon Sep 17 00:00:00 2001 From: jwasinger Date: Tue, 19 May 2026 21:35:28 -0400 Subject: [PATCH 11/76] core/types/bal: add additional static validation for access lists (#34967) Updates the static validation logic to cover additional edge cases (reflecting the state of the latest devnet branch, except cleaned up slightly). --------- Co-authored-by: Gary Rong --- core/block_validator.go | 4 +- core/types/bal/bal_encoding.go | 114 ++++++++++++++++++++++++--------- core/types/bal/bal_test.go | 6 +- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/core/block_validator.go b/core/block_validator.go index 4086a2ead7..962fffb82a 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -125,7 +125,7 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { computed := block.AccessList().Hash() if *block.Header().BlockAccessListHash != computed { return fmt.Errorf("access list hash mismatch, computed: %x, remote: %x", computed, *block.Header().BlockAccessListHash) - } else if err := block.AccessList().Validate(block.GasLimit()); err != nil { + } else if err := block.AccessList().Validate(block.GasLimit(), len(block.Transactions())); err != nil { return fmt.Errorf("invalid block access list: %v", err) } } @@ -195,7 +195,7 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD if local != remote { return fmt.Errorf("access list hash mismatch, local: %x, remote: %x", local, remote) } - if err := enc.Validate(block.GasLimit()); err != nil { + if err := enc.Validate(block.GasLimit(), len(block.Transactions())); err != nil { return fmt.Errorf("invalid block access list: %v", err) } } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 399f9db7c0..128e8dea2c 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -78,14 +78,14 @@ func (e *BlockAccessList) DecodeRLP(s *rlp.Stream) error { // Validate returns an error if the contents of the access list are not ordered // according to the spec or any code changes are contained which exceed protocol // max code size. -func (e *BlockAccessList) Validate(blockGasLimit uint64) error { +func (e *BlockAccessList) Validate(blockGasLimit uint64, blockTxCount int) error { if !slices.IsSortedFunc(*e, func(a, b AccountAccess) int { return bytes.Compare(a.Address[:], b.Address[:]) }) { return errors.New("block access list accounts not in lexicographic order") } for _, entry := range *e { - if err := entry.validate(); err != nil { + if err := entry.validate(blockTxCount + 1); err != nil { return err } } @@ -154,15 +154,28 @@ type encodingSlotWrites struct { Accesses []encodingStorageWrite } -// validate returns an instance of the encoding-representation slot writes in -// working representation. -func (e *encodingSlotWrites) validate() error { - if slices.IsSortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) - }) { - return nil +func isStrictlySortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { + for i := 1; i < len(x); i++ { + if cmp(x[i-1], x[i]) >= 0 { + return false // includes both unsorted and duplicate + } } - return errors.New("storage write tx indices not in order") + return true +} + +// validate asserts that the encodingSlotWrites contain storage modfications +// which are ordered ascending by transaction index and contain no duplicate +// modifications for a given index. +func (e *encodingSlotWrites) validate(maxBALIndex int) error { + if !isStrictlySortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { + return cmp.Compare(a.TxIdx, b.TxIdx) + }) { + return errors.New("storage write indexes must be unique and sorted") + } + if len(e.Accesses) > 0 && int(e.Accesses[len(e.Accesses)-1].TxIdx) > maxBALIndex { + return fmt.Errorf("storage write index exceeds limit, index: %d, limit: %d", e.Accesses[len(e.Accesses)-1].TxIdx, maxBALIndex) + } + return nil } // encodingCodeChange contains the runtime bytecode deployed at an address @@ -185,46 +198,83 @@ type AccountAccess struct { // validate converts the account accesses out of encoding format. // If any of the keys in the encoding object are not ordered according to the // spec, an error is returned. -func (e *AccountAccess) validate() error { - // Check the storage write slots are sorted in order - if !slices.IsSortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { +func (e *AccountAccess) validate(maxBALIndex int) error { + // Check the storage writes are sorted in order, and unique by slot + if !isStrictlySortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { return a.Slot.Cmp(b.Slot) }) { - return errors.New("storage writes slots not in lexicographic order") + return errors.New("storage write slots must be unique and sorted") } - for _, write := range e.StorageWrites { - if err := write.validate(); err != nil { + // Check the validity of each storage slot's mutations + for _, slotWrites := range e.StorageWrites { + if err := slotWrites.validate(maxBALIndex); err != nil { return err } } - // Check the storage read slots are sorted in order - if !slices.IsSortedFunc(e.StorageReads, func(a, b *uint256.Int) int { + // Check the storage read slots are sorted in order, and unique by slot + if !isStrictlySortedFunc(e.StorageReads, func(a, b *uint256.Int) int { return a.Cmp(b) }) { - return errors.New("storage read slots not in lexicographic order") + return errors.New("storage read slots must be unique and sorted") } - // Check the balance changes are sorted in order - if !slices.IsSortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) - }) { - return errors.New("balance changes not in ascending order by tx index") + // Check that the set of written storage slots does not intersect with the + // set of read slots. + var ( + readKeys = make(map[common.Hash]struct{}, len(e.StorageReads)) + writeKeys = make(map[common.Hash]struct{}, len(e.StorageWrites)) + ) + for _, rk := range e.StorageReads { + readKey := common.BytesToHash(rk.Bytes()) + readKeys[readKey] = struct{}{} + } + for _, write := range e.StorageWrites { + writeKey := common.BytesToHash(write.Slot.Bytes()) + writeKeys[writeKey] = struct{}{} + } + for readKey := range readKeys { + if _, ok := writeKeys[readKey]; ok { + return errors.New("storage key reported in both read/write sets") + } } - // Check the nonce changes are sorted in order - if !slices.IsSortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) + // Check the balance changes are sorted in order, and unique by tx index + if !isStrictlySortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int { + return cmp.Compare(a.TxIdx, b.TxIdx) }) { - return errors.New("nonce changes not in ascending order by tx index") + return errors.New("balance changes must be unique and sorted") + } + // check that the tx index is not greater than the max allowed for the block + if len(e.BalanceChanges) > 0 && int(e.BalanceChanges[len(e.BalanceChanges)-1].TxIdx) > maxBALIndex { + return fmt.Errorf("balance change index exceeds limit, index: %d, limit: %d", e.BalanceChanges[len(e.BalanceChanges)-1].TxIdx, maxBALIndex) } - // Check the code changes are sorted in order - if !slices.IsSortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int { - return cmp.Compare[uint32](a.TxIndex, b.TxIndex) + // Check the nonce changes are sorted in order, and unique by tx index + if !isStrictlySortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int { + return cmp.Compare(a.TxIdx, b.TxIdx) }) { - return errors.New("code changes not in ascending order by tx index") + return errors.New("nonce changes must be unique and sorted") } + // check that the tx index of the highest nonce change is not greater than + // the max allowed for the block + if len(e.NonceChanges) > 0 && int(e.NonceChanges[len(e.NonceChanges)-1].TxIdx) > maxBALIndex { + return fmt.Errorf("nonce change index exceeds limit, index: %d, limit: %d", e.NonceChanges[len(e.NonceChanges)-1].TxIdx, maxBALIndex) + } + + // Check the code changes are sorted in order, and unique by tx index + if !isStrictlySortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int { + return cmp.Compare(a.TxIndex, b.TxIndex) + }) { + return errors.New("code changes must be unique and sorted") + } + // check that the tx index of the highest code changeis not greater than the + // max allowed for the block + if len(e.CodeChanges) > 0 && int(e.CodeChanges[len(e.CodeChanges)-1].TxIndex) > maxBALIndex { + return fmt.Errorf("code change index exceeds limit, index: %d, limit: %d", e.CodeChanges[len(e.CodeChanges)-1].TxIndex, maxBALIndex) + } + // Check that none of the code changes report a new code which is larger + // than the max allowed by the protocol for _, change := range e.CodeChanges { if len(change.Code) > params.MaxCodeSizeAmsterdam { return errors.New("code change contained oversized code") diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index 2b6a3c194e..fae4856ac6 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -358,7 +358,7 @@ func TestBlockAccessListValidateSize(t *testing.T) { func TestBlockAccessListValidation(t *testing.T) { // Validate the block access list after RLP decoding enc := makeTestBAL(true) - if err := enc.Validate(math.MaxUint64); err != nil { + if err := enc.Validate(math.MaxUint64, 10000); err != nil { t.Fatalf("Unexpected validation error: %v", err) } var buf bytes.Buffer @@ -370,14 +370,14 @@ func TestBlockAccessListValidation(t *testing.T) { if err := dec.DecodeRLP(rlp.NewStream(bytes.NewReader(buf.Bytes()), 0)); err != nil { t.Fatalf("Unexpected RLP-decode error: %v", err) } - if err := dec.Validate(math.MaxUint64); err != nil { + if err := dec.Validate(math.MaxUint64, 10000); err != nil { t.Fatalf("Unexpected validation error: %v", err) } // Validate the derived block access list cBAL := makeTestConstructionBAL() listB := cBAL.ToEncodingObj() - if err := listB.Validate(math.MaxUint64); err != nil { + if err := listB.Validate(math.MaxUint64, 10000); err != nil { t.Fatalf("Unexpected validation error: %v", err) } } From 918d46b942b9d9f2a13c02cf12a651ba9ccae879 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 20 May 2026 21:12:13 +0800 Subject: [PATCH 12/76] core, cmd, internal: rework BAL json marshalling to adhere EELS (#34972) It's a change to BAL json marshalling and t8n tooling to adhere the EELS definition. --- cmd/evm/internal/t8ntool/block.go | 44 ++-- cmd/evm/internal/t8ntool/execution.go | 30 ++- cmd/evm/internal/t8ntool/gen_execresult.go | 13 ++ cmd/evm/internal/t8ntool/gen_header.go | 12 + core/bal_test.go | 48 ++-- core/types/bal/bal_encoding.go | 218 ++++++++++-------- core/types/bal/bal_encoding_rlp_generated.go | 71 +++--- core/types/bal/bal_test.go | 52 ++--- core/types/bal/gen_account_access_json.go | 76 ++++++ .../bal/gen_encoding_account_nonce_json.go | 42 ++++ .../bal/gen_encoding_balance_change_json.go | 43 ++++ .../bal/gen_encoding_code_change_json.go | 42 ++++ .../bal/gen_encoding_slot_changes_json.go | 43 ++++ .../bal/gen_encoding_storage_write_json.go | 43 ++++ core/types/block.go | 2 +- core/types/gen_header_json.go | 4 +- internal/ethapi/api.go | 2 +- 17 files changed, 581 insertions(+), 204 deletions(-) create mode 100644 core/types/bal/gen_account_access_json.go create mode 100644 core/types/bal/gen_encoding_account_nonce_json.go create mode 100644 core/types/bal/gen_encoding_balance_change_json.go create mode 100644 core/types/bal/gen_encoding_code_change_json.go create mode 100644 core/types/bal/gen_encoding_slot_changes_json.go create mode 100644 core/types/bal/gen_encoding_storage_write_json.go diff --git a/cmd/evm/internal/t8ntool/block.go b/cmd/evm/internal/t8ntool/block.go index 6148e9e248..396900d5fd 100644 --- a/cmd/evm/internal/t8ntool/block.go +++ b/cmd/evm/internal/t8ntool/block.go @@ -56,6 +56,8 @@ type header struct { BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` SlotNumber *uint64 `json:"slotNumber" rlp:"optional"` } @@ -119,26 +121,28 @@ func (c *cliqueInput) UnmarshalJSON(input []byte) error { // ToBlock converts i into a *types.Block func (i *bbInput) ToBlock() *types.Block { header := &types.Header{ - ParentHash: i.Header.ParentHash, - UncleHash: types.EmptyUncleHash, - Coinbase: common.Address{}, - Root: i.Header.Root, - TxHash: types.EmptyTxsHash, - ReceiptHash: types.EmptyReceiptsHash, - Bloom: i.Header.Bloom, - Difficulty: common.Big0, - Number: i.Header.Number, - GasLimit: i.Header.GasLimit, - GasUsed: i.Header.GasUsed, - Time: i.Header.Time, - Extra: i.Header.Extra, - MixDigest: i.Header.MixDigest, - BaseFee: i.Header.BaseFee, - WithdrawalsHash: i.Header.WithdrawalsHash, - BlobGasUsed: i.Header.BlobGasUsed, - ExcessBlobGas: i.Header.ExcessBlobGas, - ParentBeaconRoot: i.Header.ParentBeaconBlockRoot, - SlotNumber: i.Header.SlotNumber, + ParentHash: i.Header.ParentHash, + UncleHash: types.EmptyUncleHash, + Coinbase: common.Address{}, + Root: i.Header.Root, + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + Bloom: i.Header.Bloom, + Difficulty: common.Big0, + Number: i.Header.Number, + GasLimit: i.Header.GasLimit, + GasUsed: i.Header.GasUsed, + Time: i.Header.Time, + Extra: i.Header.Extra, + MixDigest: i.Header.MixDigest, + BaseFee: i.Header.BaseFee, + WithdrawalsHash: i.Header.WithdrawalsHash, + BlobGasUsed: i.Header.BlobGasUsed, + ExcessBlobGas: i.Header.ExcessBlobGas, + ParentBeaconRoot: i.Header.ParentBeaconBlockRoot, + RequestsHash: i.Header.RequestsHash, + BlockAccessListHash: i.Header.BlockAccessListHash, + SlotNumber: i.Header.SlotNumber, } // Fill optional values. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 043e675494..bd089c9f55 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -76,6 +76,9 @@ type ExecutionResult struct { CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests [][]byte `json:"requests"` + + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } type executionResultMarshaling struct { @@ -153,8 +156,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, return h } var ( - isEIP4762 = chainConfig.IsUBT(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) - statedb *state.StateDB + statedb *state.StateDB + + isEIP4762 = chainConfig.IsUBT(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) + isAmsterdam = chainConfig.IsAmsterdam(big.NewInt(int64(pre.Env.Number)), pre.Env.Timestamp) ) if pre.AllocPath != "" { var err error @@ -299,9 +304,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, receipts = append(receipts, receipt) blockAccessList.Merge(bal) } - statedb.IntermediateRoot(chainConfig.IsEIP158(vmContext.BlockNumber)) + // TODO(rjl493456442) call engine.Finalize() instead // Add mining reward? (-1 means rewards are disabled) if miningReward >= 0 { // Add mining reward. The mining reward may be `0`, which only makes a difference in the cases @@ -330,11 +335,22 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, for _, w := range pre.Env.Withdrawals { // Amount is in gwei, turn into wei amount := new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), big.NewInt(params.GWei)) - statedb.AddBalance(w.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) + prev := statedb.AddBalance(w.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) if isEIP4762 { statedb.AccessEvents().AddAccount(w.Address, true, stdmath.MaxUint64) } + if isAmsterdam { + if w.Amount == 0 { + // Zero amount withdrawal, account is accessed potential + // without state changes. + blockAccessList.AccountRead(w.Address) + } else { + // Non-zero amount withdrawal, account is accessed with + // a balance change. + blockAccessList.BalanceChange(uint32(len(receipts)+1), w.Address, new(uint256.Int).Add(&prev, uint256.MustFromBig(amount))) + } + } } // Gather the execution-layer triggered requests. @@ -379,6 +395,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, execRs.RequestsHash = &h execRs.Requests = requests } + if isAmsterdam { + bal := blockAccessList.ToEncodingObj() + balHash := bal.Hash() + execRs.BlockAccessListHash = &balHash + execRs.BlockAccessList = bal + } // Re-create statedb instance with new root for MPT mode statedb, err = state.New(root, statedb.Database()) diff --git a/cmd/evm/internal/t8ntool/gen_execresult.go b/cmd/evm/internal/t8ntool/gen_execresult.go index 38310b9f2b..f678c65de2 100644 --- a/cmd/evm/internal/t8ntool/gen_execresult.go +++ b/cmd/evm/internal/t8ntool/gen_execresult.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executionResultMarshaling)(nil) @@ -32,6 +33,8 @@ func (e ExecutionResult) MarshalJSON() ([]byte, error) { CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests []hexutil.Bytes `json:"requests"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var enc ExecutionResult enc.StateRoot = e.StateRoot @@ -54,6 +57,8 @@ func (e ExecutionResult) MarshalJSON() ([]byte, error) { enc.Requests[k] = v } } + enc.BlockAccessList = e.BlockAccessList + enc.BlockAccessListHash = e.BlockAccessListHash return json.Marshal(&enc) } @@ -75,6 +80,8 @@ func (e *ExecutionResult) UnmarshalJSON(input []byte) error { CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests []hexutil.Bytes `json:"requests"` + BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var dec ExecutionResult if err := json.Unmarshal(input, &dec); err != nil { @@ -130,5 +137,11 @@ func (e *ExecutionResult) UnmarshalJSON(input []byte) error { e.Requests[k] = v } } + if dec.BlockAccessList != nil { + e.BlockAccessList = dec.BlockAccessList + } + if dec.BlockAccessListHash != nil { + e.BlockAccessListHash = dec.BlockAccessListHash + } return nil } diff --git a/cmd/evm/internal/t8ntool/gen_header.go b/cmd/evm/internal/t8ntool/gen_header.go index f430feb6d2..1c5e0065e1 100644 --- a/cmd/evm/internal/t8ntool/gen_header.go +++ b/cmd/evm/internal/t8ntool/gen_header.go @@ -38,6 +38,8 @@ func (h header) MarshalJSON() ([]byte, error) { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` SlotNumber *math.HexOrDecimal64 `json:"slotNumber" rlp:"optional"` } var enc header @@ -61,6 +63,8 @@ func (h header) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*math.HexOrDecimal64)(h.BlobGasUsed) enc.ExcessBlobGas = (*math.HexOrDecimal64)(h.ExcessBlobGas) enc.ParentBeaconBlockRoot = h.ParentBeaconBlockRoot + enc.RequestsHash = h.RequestsHash + enc.BlockAccessListHash = h.BlockAccessListHash enc.SlotNumber = (*math.HexOrDecimal64)(h.SlotNumber) return json.Marshal(&enc) } @@ -88,6 +92,8 @@ func (h *header) UnmarshalJSON(input []byte) error { BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` SlotNumber *math.HexOrDecimal64 `json:"slotNumber" rlp:"optional"` } var dec header @@ -158,6 +164,12 @@ func (h *header) UnmarshalJSON(input []byte) error { if dec.ParentBeaconBlockRoot != nil { h.ParentBeaconBlockRoot = dec.ParentBeaconBlockRoot } + if dec.RequestsHash != nil { + h.RequestsHash = dec.RequestsHash + } + if dec.BlockAccessListHash != nil { + h.BlockAccessListHash = dec.BlockAccessListHash + } if dec.SlotNumber != nil { h.SlotNumber = (*uint64)(dec.SlotNumber) } diff --git a/core/bal_test.go b/core/bal_test.go index f0b9dc6443..d20108b96c 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -121,7 +121,7 @@ func hasStorageWrite(b *bal.BlockAccessList, addr common.Address, key common.Has return false } want := new(uint256.Int).SetBytes(key[:]) - for _, w := range aa.StorageWrites { + for _, w := range aa.StorageChanges { if w.Slot.Cmp(want) == 0 { return true } @@ -147,7 +147,7 @@ func assertAbsent(t *testing.T, b *bal.BlockAccessList, addr common.Address) { func assertEmpty(t *testing.T, aa *bal.AccountAccess) { t.Helper() - if len(aa.StorageWrites) != 0 || len(aa.StorageReads) != 0 || + if len(aa.StorageChanges) != 0 || len(aa.StorageReads) != 0 || len(aa.BalanceChanges) != 0 || len(aa.NonceChanges) != 0 || len(aa.CodeChanges) != 0 { t.Fatalf("expected empty change set for %x, got %+v", aa.Address, aa) } @@ -181,14 +181,14 @@ func TestBALTxSenderAndRecipient(t *testing.T) { }) sender := assertPresent(t, b, env.from) - if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].PostNonce != 1 { t.Fatalf("sender nonce not bumped: %+v", sender.NonceChanges) } if len(sender.BalanceChanges) == 0 { t.Fatalf("sender missing balance change") } recipient := assertPresent(t, b, to) - if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].Balance.Uint64() != 1000 { + if len(recipient.BalanceChanges) != 1 || recipient.BalanceChanges[0].PostBalance.Uint64() != 1000 { t.Fatalf("recipient balance: %+v", recipient.BalanceChanges) } } @@ -236,7 +236,7 @@ func TestBALCoinbaseTipCapturesBalance(t *testing.T) { }) cb := assertPresent(t, b, coinbase) - if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].Balance.Sign() == 0 { + if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].PostBalance.Sign() == 0 { t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges) } } @@ -264,7 +264,7 @@ func TestBALSystemAddressIncludedWhenTouched(t *testing.T) { }) aa := assertPresent(t, b, sys) - if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 1000 { + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].PostBalance.Uint64() != 1000 { t.Fatalf("system-address balance change missing: %+v", aa.BalanceChanges) } } @@ -319,7 +319,7 @@ func TestBALPrecompileValueTransferRecordsBalance(t *testing.T) { }) aa := assertPresent(t, b, identity) - if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 5 { + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].PostBalance.Uint64() != 5 { t.Fatalf("precompile balance change wrong: %+v", aa.BalanceChanges) } } @@ -537,7 +537,7 @@ func TestBALSenderRecordedOnRevert(t *testing.T) { }) sender := assertPresent(t, b, env.from) - if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].Nonce != 1 { + if len(sender.NonceChanges) == 0 || sender.NonceChanges[0].PostNonce != 1 { t.Fatalf("sender nonce must be bumped even on revert: %+v", sender.NonceChanges) } if len(sender.BalanceChanges) == 0 { @@ -667,10 +667,10 @@ func TestBALStorageWriteZeroIsAWrite(t *testing.T) { if !hasStorageWrite(b, contract, slot) { t.Fatalf("SSTORE to zero must record a write\n%s", b.PrettyPrint()) } - for _, w := range aa.StorageWrites { + for _, w := range aa.StorageChanges { if w.Slot.Uint64() == 0x03 { - if len(w.Accesses) != 1 || !w.Accesses[0].ValueAfter.IsZero() { - t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.Accesses) + if len(w.SlotChanges) != 1 || !w.SlotChanges[0].PostValue.IsZero() { + t.Fatalf("expected post-value 0 for slot 0x03, got %+v", w.SlotChanges) } } } @@ -692,13 +692,13 @@ func TestBALCreateDeploysCode(t *testing.T) { created := receipts[0].ContractAddress aa := assertPresent(t, b, created) - if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 1 { t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) } - if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].Code, []byte{0x00}) { + if len(aa.CodeChanges) != 1 || !bytes.Equal(aa.CodeChanges[0].NewCode, []byte{0x00}) { t.Fatalf("expected code [0x00], got %+v", aa.CodeChanges) } - if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].Balance.Uint64() != 7 { + if len(aa.BalanceChanges) != 1 || aa.BalanceChanges[0].PostBalance.Uint64() != 7 { t.Fatalf("expected balance 7, got %+v", aa.BalanceChanges) } } @@ -716,7 +716,7 @@ func TestBALCreateEmptyRuntimeNoCodeEntry(t *testing.T) { created := receipts[0].ContractAddress aa := assertPresent(t, b, created) - if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 1 { t.Fatalf("expected nonce 0→1, got %+v", aa.NonceChanges) } if len(aa.CodeChanges) != 0 { @@ -849,7 +849,7 @@ func TestBALInEVMCreateDeploysContract(t *testing.T) { // which is the factory's genesis nonce (1). deployed := crypto.CreateAddress(factory, 1) aa := assertPresent(t, b, deployed) - if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 1 { + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 1 { t.Fatalf("deployed contract nonce: %+v", aa.NonceChanges) } } @@ -900,7 +900,7 @@ func TestBALSelfDestructBeneficiaryWithValueTransfer(t *testing.T) { }) ben := assertPresent(t, b, beneficiary) - if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 100 { + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].PostBalance.Uint64() != 100 { t.Fatalf("beneficiary balance must be credited with 100: %+v", ben.BalanceChanges) } } @@ -925,11 +925,11 @@ func TestBALSelfDestructPreExistingContract(t *testing.T) { }) aa := assertPresent(t, b, suicidal) - if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].Balance.IsZero() { + if len(aa.BalanceChanges) != 1 || !aa.BalanceChanges[0].PostBalance.IsZero() { t.Fatalf("suicidal contract balance should drop to 0: %+v", aa.BalanceChanges) } ben := assertPresent(t, b, beneficiary) - if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].Balance.Uint64() != 50 { + if len(ben.BalanceChanges) != 1 || ben.BalanceChanges[0].PostBalance.Uint64() != 50 { t.Fatalf("beneficiary should receive 50: %+v", ben.BalanceChanges) } } @@ -1041,7 +1041,7 @@ func TestBALWithdrawalNonZeroAmountRecordsBalance(t *testing.T) { }) r := assertPresent(t, b, recipient) - if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].Balance.Sign() == 0 { + if len(r.BalanceChanges) != 1 || r.BalanceChanges[0].PostBalance.Sign() == 0 { t.Fatalf("withdrawal balance change missing: %+v", r.BalanceChanges) } } @@ -1266,7 +1266,7 @@ func TestBALAuthCodeRoundTripNoCodeEntry(t *testing.T) { }) aa := assertPresent(t, b, authority) - if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 2 { t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) } if len(aa.CodeChanges) != 0 { @@ -1310,10 +1310,10 @@ func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) { t.Fatalf("expected exactly 1 code change (final), got %+v", aa.CodeChanges) } want := types.AddressToDelegation(delegateB) - if !bytes.Equal(aa.CodeChanges[0].Code, want) { - t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].Code) + if !bytes.Equal(aa.CodeChanges[0].NewCode, want) { + t.Fatalf("final code mismatch: want %x, got %x", want, aa.CodeChanges[0].NewCode) } - if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].Nonce != 2 { + if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 2 { t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges) } } diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 128e8dea2c..612d2f8777 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" @@ -99,7 +100,7 @@ func (e *BlockAccessList) Validate(blockGasLimit uint64, blockTxCount int) error func (e *BlockAccessList) itemCount() uint64 { count := uint64(len(*e)) // distinct addresses for i := range *e { - count += uint64(len((*e)[i].StorageWrites)) + uint64(len((*e)[i].StorageReads)) + count += uint64(len((*e)[i].StorageChanges)) + uint64(len((*e)[i].StorageReads)) } return count } @@ -130,28 +131,38 @@ func (e *BlockAccessList) Hash() common.Hash { return crypto.Keccak256Hash(enc.Bytes()) } -// encodingBalanceChange is the encoding format of BalanceChange. -type encodingBalanceChange struct { - TxIdx uint32 - Balance *uint256.Int -} +// EIP-7928 encoding types. Field names and JSON keys mirror the +// execution-spec-tests Pydantic models in +// `src/ethereum_test_types/block_access_list/account_changes.py`. Hex +// formatting on JSON output is supplied via the gencodec overrides +// below. -// encodingAccountNonce is the encoding format of NonceChange. -type encodingAccountNonce struct { - TxIdx uint32 - Nonce uint64 -} +//go:generate go run github.com/fjl/gencodec -type encodingStorageWrite -field-override encodingStorageWriteMarshaling -out gen_encoding_storage_write_json.go +//go:generate go run github.com/fjl/gencodec -type encodingSlotChanges -field-override encodingSlotChangesMarshaling -out gen_encoding_slot_changes_json.go +//go:generate go run github.com/fjl/gencodec -type encodingBalanceChange -field-override encodingBalanceChangeMarshaling -out gen_encoding_balance_change_json.go +//go:generate go run github.com/fjl/gencodec -type encodingAccountNonce -field-override encodingAccountNonceMarshaling -out gen_encoding_account_nonce_json.go +//go:generate go run github.com/fjl/gencodec -type encodingCodeChange -field-override encodingCodeChangeMarshaling -out gen_encoding_code_change_json.go +//go:generate go run github.com/fjl/gencodec -type AccountAccess -field-override accountAccessMarshaling -out gen_account_access_json.go -// encodingStorageWrite is the encoding format of StorageWrites. +// encodingStorageWrite is one transaction's write to a storage slot. type encodingStorageWrite struct { - TxIdx uint32 - ValueAfter *uint256.Int + BlockAccessIndex uint32 `json:"blockAccessIndex"` + PostValue *uint256.Int `json:"postValue"` } -// encodingStorageWrite is the encoding format of SlotWrites. -type encodingSlotWrites struct { - Slot *uint256.Int - Accesses []encodingStorageWrite +type encodingStorageWriteMarshaling struct { + BlockAccessIndex hexutil.Uint64 + PostValue *hexutil.U256 +} + +// encodingSlotChanges aggregates all per-tx writes to a single storage slot. +type encodingSlotChanges struct { + Slot *uint256.Int `json:"slot"` + SlotChanges []encodingStorageWrite `json:"slotChanges"` +} + +type encodingSlotChangesMarshaling struct { + Slot *hexutil.U256 } func isStrictlySortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { @@ -166,33 +177,63 @@ func isStrictlySortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { // validate asserts that the encodingSlotWrites contain storage modfications // which are ordered ascending by transaction index and contain no duplicate // modifications for a given index. -func (e *encodingSlotWrites) validate(maxBALIndex int) error { - if !isStrictlySortedFunc(e.Accesses, func(a, b encodingStorageWrite) int { - return cmp.Compare(a.TxIdx, b.TxIdx) +func (e *encodingSlotChanges) validate(maxBALIndex int) error { + if !isStrictlySortedFunc(e.SlotChanges, func(a, b encodingStorageWrite) int { + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) { return errors.New("storage write indexes must be unique and sorted") } - if len(e.Accesses) > 0 && int(e.Accesses[len(e.Accesses)-1].TxIdx) > maxBALIndex { - return fmt.Errorf("storage write index exceeds limit, index: %d, limit: %d", e.Accesses[len(e.Accesses)-1].TxIdx, maxBALIndex) + if len(e.SlotChanges) > 0 && int(e.SlotChanges[len(e.SlotChanges)-1].BlockAccessIndex) > maxBALIndex { + return fmt.Errorf("storage write index exceeds limit, index: %d, limit: %d", e.SlotChanges[len(e.SlotChanges)-1].BlockAccessIndex, maxBALIndex) } return nil } -// encodingCodeChange contains the runtime bytecode deployed at an address -// and the transaction index where the deployment took place. +// encodingBalanceChange is one transaction's post-state balance for an account. +type encodingBalanceChange struct { + BlockAccessIndex uint32 `json:"blockAccessIndex"` + PostBalance *uint256.Int `json:"postBalance"` +} + +type encodingBalanceChangeMarshaling struct { + BlockAccessIndex hexutil.Uint64 + PostBalance *hexutil.U256 +} + +// encodingAccountNonce is one transaction's post-state nonce for an account. +type encodingAccountNonce struct { + BlockAccessIndex uint32 `json:"blockAccessIndex"` + PostNonce uint64 `json:"postNonce"` +} + +type encodingAccountNonceMarshaling struct { + BlockAccessIndex hexutil.Uint64 + PostNonce hexutil.Uint64 +} + +// encodingCodeChange is one transaction's deployed runtime bytecode for an account. type encodingCodeChange struct { - TxIndex uint32 - Code []byte + BlockAccessIndex uint32 `json:"blockAccessIndex"` + NewCode []byte `json:"newCode"` +} + +type encodingCodeChangeMarshaling struct { + BlockAccessIndex hexutil.Uint64 + NewCode hexutil.Bytes } // AccountAccess is the encoding format of ConstructionAccountAccess. type AccountAccess struct { - Address [20]byte // 20-byte Ethereum address - StorageWrites []encodingSlotWrites // Storage changes (slot -> [tx_index -> new_value]) - StorageReads []*uint256.Int // Read-only storage keys - BalanceChanges []encodingBalanceChange // Balance changes ([tx_index -> post_balance]) - NonceChanges []encodingAccountNonce // Nonce changes ([tx_index -> new_nonce]) - CodeChanges []encodingCodeChange // Code changes ([tx_index -> new_code]) + Address common.Address `json:"address"` + StorageChanges []encodingSlotChanges `json:"storageChanges"` + StorageReads []*uint256.Int `json:"storageReads"` + BalanceChanges []encodingBalanceChange `json:"balanceChanges"` + NonceChanges []encodingAccountNonce `json:"nonceChanges"` + CodeChanges []encodingCodeChange `json:"codeChanges"` +} + +type accountAccessMarshaling struct { + StorageReads []*hexutil.U256 } // validate converts the account accesses out of encoding format. @@ -200,13 +241,13 @@ type AccountAccess struct { // spec, an error is returned. func (e *AccountAccess) validate(maxBALIndex int) error { // Check the storage writes are sorted in order, and unique by slot - if !isStrictlySortedFunc(e.StorageWrites, func(a, b encodingSlotWrites) int { + if !isStrictlySortedFunc(e.StorageChanges, func(a, b encodingSlotChanges) int { return a.Slot.Cmp(b.Slot) }) { return errors.New("storage write slots must be unique and sorted") } // Check the validity of each storage slot's mutations - for _, slotWrites := range e.StorageWrites { + for _, slotWrites := range e.StorageChanges { if err := slotWrites.validate(maxBALIndex); err != nil { return err } @@ -223,13 +264,13 @@ func (e *AccountAccess) validate(maxBALIndex int) error { // set of read slots. var ( readKeys = make(map[common.Hash]struct{}, len(e.StorageReads)) - writeKeys = make(map[common.Hash]struct{}, len(e.StorageWrites)) + writeKeys = make(map[common.Hash]struct{}, len(e.StorageChanges)) ) for _, rk := range e.StorageReads { readKey := common.BytesToHash(rk.Bytes()) readKeys[readKey] = struct{}{} } - for _, write := range e.StorageWrites { + for _, write := range e.StorageChanges { writeKey := common.BytesToHash(write.Slot.Bytes()) writeKeys[writeKey] = struct{}{} } @@ -241,42 +282,42 @@ func (e *AccountAccess) validate(maxBALIndex int) error { // Check the balance changes are sorted in order, and unique by tx index if !isStrictlySortedFunc(e.BalanceChanges, func(a, b encodingBalanceChange) int { - return cmp.Compare(a.TxIdx, b.TxIdx) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) { return errors.New("balance changes must be unique and sorted") } // check that the tx index is not greater than the max allowed for the block - if len(e.BalanceChanges) > 0 && int(e.BalanceChanges[len(e.BalanceChanges)-1].TxIdx) > maxBALIndex { - return fmt.Errorf("balance change index exceeds limit, index: %d, limit: %d", e.BalanceChanges[len(e.BalanceChanges)-1].TxIdx, maxBALIndex) + if len(e.BalanceChanges) > 0 && int(e.BalanceChanges[len(e.BalanceChanges)-1].BlockAccessIndex) > maxBALIndex { + return fmt.Errorf("balance change index exceeds limit, index: %d, limit: %d", e.BalanceChanges[len(e.BalanceChanges)-1].BlockAccessIndex, maxBALIndex) } // Check the nonce changes are sorted in order, and unique by tx index if !isStrictlySortedFunc(e.NonceChanges, func(a, b encodingAccountNonce) int { - return cmp.Compare(a.TxIdx, b.TxIdx) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) { return errors.New("nonce changes must be unique and sorted") } // check that the tx index of the highest nonce change is not greater than // the max allowed for the block - if len(e.NonceChanges) > 0 && int(e.NonceChanges[len(e.NonceChanges)-1].TxIdx) > maxBALIndex { - return fmt.Errorf("nonce change index exceeds limit, index: %d, limit: %d", e.NonceChanges[len(e.NonceChanges)-1].TxIdx, maxBALIndex) + if len(e.NonceChanges) > 0 && int(e.NonceChanges[len(e.NonceChanges)-1].BlockAccessIndex) > maxBALIndex { + return fmt.Errorf("nonce change index exceeds limit, index: %d, limit: %d", e.NonceChanges[len(e.NonceChanges)-1].BlockAccessIndex, maxBALIndex) } // Check the code changes are sorted in order, and unique by tx index if !isStrictlySortedFunc(e.CodeChanges, func(a, b encodingCodeChange) int { - return cmp.Compare(a.TxIndex, b.TxIndex) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) { return errors.New("code changes must be unique and sorted") } // check that the tx index of the highest code changeis not greater than the // max allowed for the block - if len(e.CodeChanges) > 0 && int(e.CodeChanges[len(e.CodeChanges)-1].TxIndex) > maxBALIndex { - return fmt.Errorf("code change index exceeds limit, index: %d, limit: %d", e.CodeChanges[len(e.CodeChanges)-1].TxIndex, maxBALIndex) + if len(e.CodeChanges) > 0 && int(e.CodeChanges[len(e.CodeChanges)-1].BlockAccessIndex) > maxBALIndex { + return fmt.Errorf("code change index exceeds limit, index: %d, limit: %d", e.CodeChanges[len(e.CodeChanges)-1].BlockAccessIndex, maxBALIndex) } // Check that none of the code changes report a new code which is larger // than the max allowed by the protocol for _, change := range e.CodeChanges { - if len(change.Code) > params.MaxCodeSizeAmsterdam { + if len(change.NewCode) > params.MaxCodeSizeAmsterdam { return errors.New("code change contained oversized code") } } @@ -290,7 +331,7 @@ func (e *AccountAccess) Copy() AccountAccess { StorageReads: make([]*uint256.Int, 0, len(e.StorageReads)), BalanceChanges: make([]encodingBalanceChange, 0, len(e.BalanceChanges)), NonceChanges: slices.Clone(e.NonceChanges), - StorageWrites: make([]encodingSlotWrites, 0, len(e.StorageWrites)), + StorageChanges: make([]encodingSlotChanges, 0, len(e.StorageChanges)), CodeChanges: make([]encodingCodeChange, 0, len(e.CodeChanges)), } for _, slot := range e.StorageReads { @@ -298,27 +339,27 @@ func (e *AccountAccess) Copy() AccountAccess { } for _, change := range e.BalanceChanges { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ - TxIdx: change.TxIdx, - Balance: change.Balance.Clone(), + BlockAccessIndex: change.BlockAccessIndex, + PostBalance: change.PostBalance.Clone(), }) } - for _, storageWrite := range e.StorageWrites { - accesses := make([]encodingStorageWrite, 0, len(storageWrite.Accesses)) - for _, w := range storageWrite.Accesses { - accesses = append(accesses, encodingStorageWrite{ - TxIdx: w.TxIdx, - ValueAfter: w.ValueAfter.Clone(), + for _, slot := range e.StorageChanges { + writes := make([]encodingStorageWrite, 0, len(slot.SlotChanges)) + for _, w := range slot.SlotChanges { + writes = append(writes, encodingStorageWrite{ + BlockAccessIndex: w.BlockAccessIndex, + PostValue: w.PostValue.Clone(), }) } - res.StorageWrites = append(res.StorageWrites, encodingSlotWrites{ - Slot: storageWrite.Slot.Clone(), - Accesses: accesses, + res.StorageChanges = append(res.StorageChanges, encodingSlotChanges{ + Slot: slot.Slot.Clone(), + SlotChanges: writes, }) } for _, codeChange := range e.CodeChanges { res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ - TxIndex: codeChange.TxIndex, - Code: bytes.Clone(codeChange.Code), + BlockAccessIndex: codeChange.BlockAccessIndex, + NewCode: bytes.Clone(codeChange.NewCode), }) } return res @@ -336,7 +377,7 @@ var _ rlp.Encoder = &ConstructionBlockAccessList{} func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAccess { res := AccountAccess{ Address: addr, - StorageWrites: make([]encodingSlotWrites, 0, len(a.StorageWrites)), + StorageChanges: make([]encodingSlotChanges, 0, len(a.StorageWrites)), StorageReads: make([]*uint256.Int, 0, len(a.StorageReads)), BalanceChanges: make([]encodingBalanceChange, 0, len(a.BalanceChanges)), NonceChanges: make([]encodingAccountNonce, 0, len(a.NonceChanges)), @@ -347,22 +388,22 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc writeSlots := slices.Collect(maps.Keys(a.StorageWrites)) slices.SortFunc(writeSlots, common.Hash.Cmp) for _, slot := range writeSlots { - obj := encodingSlotWrites{ + obj := encodingSlotChanges{ Slot: new(uint256.Int).SetBytes(slot[:]), } slotWrites := a.StorageWrites[slot] - obj.Accesses = make([]encodingStorageWrite, 0, len(slotWrites)) + obj.SlotChanges = make([]encodingStorageWrite, 0, len(slotWrites)) indices := slices.Collect(maps.Keys(slotWrites)) - slices.SortFunc(indices, cmp.Compare[uint32]) + slices.SortFunc(indices, cmp.Compare) for _, index := range indices { val := slotWrites[index] - obj.Accesses = append(obj.Accesses, encodingStorageWrite{ - TxIdx: index, - ValueAfter: new(uint256.Int).SetBytes(val[:]), + obj.SlotChanges = append(obj.SlotChanges, encodingStorageWrite{ + BlockAccessIndex: index, + PostValue: new(uint256.Int).SetBytes(val[:]), }) } - res.StorageWrites = append(res.StorageWrites, obj) + res.StorageChanges = append(res.StorageChanges, obj) } // Convert read slots @@ -374,36 +415,36 @@ func (a *ConstructionAccountAccess) toEncodingObj(addr common.Address) AccountAc // Convert balance changes balanceIndices := slices.Collect(maps.Keys(a.BalanceChanges)) - slices.SortFunc(balanceIndices, cmp.Compare[uint32]) + slices.SortFunc(balanceIndices, cmp.Compare) for _, idx := range balanceIndices { res.BalanceChanges = append(res.BalanceChanges, encodingBalanceChange{ - TxIdx: idx, - Balance: a.BalanceChanges[idx].Clone(), + BlockAccessIndex: idx, + PostBalance: a.BalanceChanges[idx].Clone(), }) } // Convert nonce changes nonceIndices := slices.Collect(maps.Keys(a.NonceChanges)) - slices.SortFunc(nonceIndices, cmp.Compare[uint32]) + slices.SortFunc(nonceIndices, cmp.Compare) for _, idx := range nonceIndices { res.NonceChanges = append(res.NonceChanges, encodingAccountNonce{ - TxIdx: idx, - Nonce: a.NonceChanges[idx], + BlockAccessIndex: idx, + PostNonce: a.NonceChanges[idx], }) } // Convert code change codeIndices := slices.Collect(maps.Keys(a.CodeChange)) - slices.SortFunc(codeIndices, cmp.Compare[uint32]) + slices.SortFunc(codeIndices, cmp.Compare) for _, idx := range codeIndices { res.CodeChanges = append(res.CodeChanges, encodingCodeChange{ - TxIndex: idx, + BlockAccessIndex: idx, // TODO(rjl493456442) the contract code is not deep-copied. // In theory the deep-copy is unnecessary, the semantics of // the function should be probably changed that the returned // AccessList is unsafe for modification. - Code: a.CodeChange[idx], + NewCode: a.CodeChange[idx], }) } return res @@ -432,33 +473,28 @@ func (e *BlockAccessList) PrettyPrint() string { } for _, accountDiff := range *e { printWithIndent(0, fmt.Sprintf("%x:", accountDiff.Address)) - - printWithIndent(1, "storage writes:") - for _, sWrite := range accountDiff.StorageWrites { - printWithIndent(2, fmt.Sprintf("%s:", sWrite.Slot.Hex())) - for _, access := range sWrite.Accesses { - printWithIndent(3, fmt.Sprintf("%d: %s", access.TxIdx, access.ValueAfter.Hex())) + printWithIndent(1, "storage changes:") + for _, slot := range accountDiff.StorageChanges { + printWithIndent(2, fmt.Sprintf("%s:", slot.Slot.Hex())) + for _, access := range slot.SlotChanges { + printWithIndent(3, fmt.Sprintf("%d: %s", access.BlockAccessIndex, access.PostValue.Hex())) } } - printWithIndent(1, "storage reads:") for _, slot := range accountDiff.StorageReads { printWithIndent(2, slot.Hex()) } - printWithIndent(1, "balance changes:") for _, change := range accountDiff.BalanceChanges { - printWithIndent(2, fmt.Sprintf("%d: %s", change.TxIdx, change.Balance)) + printWithIndent(2, fmt.Sprintf("%d: %s", change.BlockAccessIndex, change.PostBalance)) } - printWithIndent(1, "nonce changes:") for _, change := range accountDiff.NonceChanges { - printWithIndent(2, fmt.Sprintf("%d: %d", change.TxIdx, change.Nonce)) + printWithIndent(2, fmt.Sprintf("%d: %d", change.BlockAccessIndex, change.PostNonce)) } - printWithIndent(1, "code changes:") for _, change := range accountDiff.CodeChanges { - printWithIndent(2, fmt.Sprintf("%d: %x", change.TxIndex, change.Code)) + printWithIndent(2, fmt.Sprintf("%d: %x", change.BlockAccessIndex, change.NewCode)) } } return res.String() diff --git a/core/types/bal/bal_encoding_rlp_generated.go b/core/types/bal/bal_encoding_rlp_generated.go index 540987c076..49f53197cf 100644 --- a/core/types/bal/bal_encoding_rlp_generated.go +++ b/core/types/bal/bal_encoding_rlp_generated.go @@ -2,6 +2,7 @@ package bal +import "github.com/ethereum/go-ethereum/common" import "github.com/ethereum/go-ethereum/rlp" import "github.com/holiman/uint256" import "io" @@ -11,7 +12,7 @@ func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { _tmp0 := w.List() w.WriteBytes(obj.Address[:]) _tmp1 := w.List() - for _, _tmp2 := range obj.StorageWrites { + for _, _tmp2 := range obj.StorageChanges { _tmp3 := w.List() if _tmp2.Slot == nil { w.Write(rlp.EmptyString) @@ -19,13 +20,13 @@ func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { w.WriteUint256(_tmp2.Slot) } _tmp4 := w.List() - for _, _tmp5 := range _tmp2.Accesses { + for _, _tmp5 := range _tmp2.SlotChanges { _tmp6 := w.List() - w.WriteUint64(uint64(_tmp5.TxIdx)) - if _tmp5.ValueAfter == nil { + w.WriteUint64(uint64(_tmp5.BlockAccessIndex)) + if _tmp5.PostValue == nil { w.Write(rlp.EmptyString) } else { - w.WriteUint256(_tmp5.ValueAfter) + w.WriteUint256(_tmp5.PostValue) } w.ListEnd(_tmp6) } @@ -45,11 +46,11 @@ func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { _tmp9 := w.List() for _, _tmp10 := range obj.BalanceChanges { _tmp11 := w.List() - w.WriteUint64(uint64(_tmp10.TxIdx)) - if _tmp10.Balance == nil { + w.WriteUint64(uint64(_tmp10.BlockAccessIndex)) + if _tmp10.PostBalance == nil { w.Write(rlp.EmptyString) } else { - w.WriteUint256(_tmp10.Balance) + w.WriteUint256(_tmp10.PostBalance) } w.ListEnd(_tmp11) } @@ -57,16 +58,16 @@ func (obj *AccountAccess) EncodeRLP(_w io.Writer) error { _tmp12 := w.List() for _, _tmp13 := range obj.NonceChanges { _tmp14 := w.List() - w.WriteUint64(uint64(_tmp13.TxIdx)) - w.WriteUint64(_tmp13.Nonce) + w.WriteUint64(uint64(_tmp13.BlockAccessIndex)) + w.WriteUint64(_tmp13.PostNonce) w.ListEnd(_tmp14) } w.ListEnd(_tmp12) _tmp15 := w.List() for _, _tmp16 := range obj.CodeChanges { _tmp17 := w.List() - w.WriteUint64(uint64(_tmp16.TxIndex)) - w.WriteBytes(_tmp16.Code) + w.WriteUint64(uint64(_tmp16.BlockAccessIndex)) + w.WriteBytes(_tmp16.NewCode) w.ListEnd(_tmp17) } w.ListEnd(_tmp15) @@ -81,18 +82,18 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { return err } // Address: - var _tmp1 [20]byte + var _tmp1 common.Address if err := dec.ReadBytes(_tmp1[:]); err != nil { return err } _tmp0.Address = _tmp1 - // StorageWrites: - var _tmp2 []encodingSlotWrites + // StorageChanges: + var _tmp2 []encodingSlotChanges if _, err := dec.List(); err != nil { return err } for dec.MoreDataInList() { - var _tmp3 encodingSlotWrites + var _tmp3 encodingSlotChanges { if _, err := dec.List(); err != nil { return err @@ -103,7 +104,7 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { return err } _tmp3.Slot = &_tmp4 - // Accesses: + // SlotChanges: var _tmp5 []encodingStorageWrite if _, err := dec.List(); err != nil { return err @@ -114,18 +115,18 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if _, err := dec.List(); err != nil { return err } - // TxIdx: + // BlockAccessIndex: _tmp7, err := dec.Uint32() if err != nil { return err } - _tmp6.TxIdx = _tmp7 - // ValueAfter: + _tmp6.BlockAccessIndex = _tmp7 + // PostValue: var _tmp8 uint256.Int if err := dec.ReadUint256(&_tmp8); err != nil { return err } - _tmp6.ValueAfter = &_tmp8 + _tmp6.PostValue = &_tmp8 if err := dec.ListEnd(); err != nil { return err } @@ -135,7 +136,7 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if err := dec.ListEnd(); err != nil { return err } - _tmp3.Accesses = _tmp5 + _tmp3.SlotChanges = _tmp5 if err := dec.ListEnd(); err != nil { return err } @@ -145,7 +146,7 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if err := dec.ListEnd(); err != nil { return err } - _tmp0.StorageWrites = _tmp2 + _tmp0.StorageChanges = _tmp2 // StorageReads: var _tmp9 []*uint256.Int if _, err := dec.List(); err != nil { @@ -173,18 +174,18 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if _, err := dec.List(); err != nil { return err } - // TxIdx: + // BlockAccessIndex: _tmp13, err := dec.Uint32() if err != nil { return err } - _tmp12.TxIdx = _tmp13 - // Balance: + _tmp12.BlockAccessIndex = _tmp13 + // PostBalance: var _tmp14 uint256.Int if err := dec.ReadUint256(&_tmp14); err != nil { return err } - _tmp12.Balance = &_tmp14 + _tmp12.PostBalance = &_tmp14 if err := dec.ListEnd(); err != nil { return err } @@ -206,18 +207,18 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if _, err := dec.List(); err != nil { return err } - // TxIdx: + // BlockAccessIndex: _tmp17, err := dec.Uint32() if err != nil { return err } - _tmp16.TxIdx = _tmp17 - // Nonce: + _tmp16.BlockAccessIndex = _tmp17 + // PostNonce: _tmp18, err := dec.Uint64() if err != nil { return err } - _tmp16.Nonce = _tmp18 + _tmp16.PostNonce = _tmp18 if err := dec.ListEnd(); err != nil { return err } @@ -239,18 +240,18 @@ func (obj *AccountAccess) DecodeRLP(dec *rlp.Stream) error { if _, err := dec.List(); err != nil { return err } - // TxIndex: + // BlockAccessIndex: _tmp21, err := dec.Uint32() if err != nil { return err } - _tmp20.TxIndex = _tmp21 - // Code: + _tmp20.BlockAccessIndex = _tmp21 + // NewCode: _tmp22, err := dec.Bytes() if err != nil { return err } - _tmp20.Code = _tmp22 + _tmp20.NewCode = _tmp22 if err := dec.ListEnd(); err != nil { return err } diff --git a/core/types/bal/bal_test.go b/core/types/bal/bal_test.go index fae4856ac6..a798337c19 100644 --- a/core/types/bal/bal_test.go +++ b/core/types/bal/bal_test.go @@ -160,7 +160,7 @@ func TestConstructionBALMerge(t *testing.T) { func makeTestAccountAccess(sort bool) AccountAccess { var ( - storageWrites []encodingSlotWrites + storageWrites []encodingSlotChanges storageReads []*uint256.Int balances []encodingBalanceChange nonces []encodingAccountNonce @@ -170,24 +170,24 @@ func makeTestAccountAccess(sort bool) AccountAccess { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } for i := 0; i < 5; i++ { - slot := encodingSlotWrites{ + slot := encodingSlotChanges{ Slot: randSlot(), } for j := 0; j < 3; j++ { - slot.Accesses = append(slot.Accesses, encodingStorageWrite{ - TxIdx: uint32(2 * j), - ValueAfter: randSlot(), + slot.SlotChanges = append(slot.SlotChanges, encodingStorageWrite{ + BlockAccessIndex: uint32(2 * j), + PostValue: randSlot(), }) } if sort { - slices.SortFunc(slot.Accesses, func(a, b encodingStorageWrite) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) + slices.SortFunc(slot.SlotChanges, func(a, b encodingStorageWrite) int { + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) } storageWrites = append(storageWrites, slot) } if sort { - slices.SortFunc(storageWrites, func(a, b encodingSlotWrites) int { + slices.SortFunc(storageWrites, func(a, b encodingSlotChanges) int { return a.Slot.Cmp(b.Slot) }) } @@ -203,43 +203,43 @@ func makeTestAccountAccess(sort bool) AccountAccess { for i := 0; i < 5; i++ { balances = append(balances, encodingBalanceChange{ - TxIdx: uint32(2 * i), - Balance: new(uint256.Int).SetBytes(testrand.Bytes(16)), + BlockAccessIndex: uint32(2 * i), + PostBalance: new(uint256.Int).SetBytes(testrand.Bytes(16)), }) } if sort { slices.SortFunc(balances, func(a, b encodingBalanceChange) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) } for i := 0; i < 5; i++ { nonces = append(nonces, encodingAccountNonce{ - TxIdx: uint32(2 * i), - Nonce: uint64(i + 100), + BlockAccessIndex: uint32(2 * i), + PostNonce: uint64(i + 100), }) } if sort { slices.SortFunc(nonces, func(a, b encodingAccountNonce) int { - return cmp.Compare[uint32](a.TxIdx, b.TxIdx) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) } for i := 0; i < 5; i++ { codes = append(codes, encodingCodeChange{ - TxIndex: uint32(2 * i), - Code: testrand.Bytes(256), + BlockAccessIndex: uint32(2 * i), + NewCode: testrand.Bytes(256), }) } if sort { slices.SortFunc(codes, func(a, b encodingCodeChange) int { - return cmp.Compare[uint32](a.TxIndex, b.TxIndex) + return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) } return AccountAccess{ - Address: [20]byte(testrand.Bytes(20)), - StorageWrites: storageWrites, + Address: common.Address(testrand.Bytes(20)), + StorageChanges: storageWrites, StorageReads: storageReads, BalanceChanges: balances, NonceChanges: nonces, @@ -289,14 +289,14 @@ func TestBlockAccessListItemCount(t *testing.T) { t.Fatalf("empty BAL item count: got %d, want 0", got) } - addr1 := [20]byte(testrand.Bytes(20)) - addr2 := [20]byte(testrand.Bytes(20)) + addr1 := common.Address(testrand.Bytes(20)) + addr2 := common.Address(testrand.Bytes(20)) one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } bal := &BlockAccessList{ AccountAccess{ Address: addr1, - StorageWrites: []encodingSlotWrites{ - {Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}, {TxIdx: 1, ValueAfter: one()}}}, + StorageChanges: []encodingSlotChanges{ + {Slot: one(), SlotChanges: []encodingStorageWrite{{BlockAccessIndex: 0, PostValue: one()}, {BlockAccessIndex: 1, PostValue: one()}}}, {Slot: one()}, }, StorageReads: []*uint256.Int{one()}, @@ -316,10 +316,10 @@ func TestBlockAccessListValidateSize(t *testing.T) { one := func() *uint256.Int { return new(uint256.Int).SetBytes(testrand.Bytes(32)) } bal := make(BlockAccessList, 3) for i := range bal { - bal[i].Address = [20]byte(testrand.Bytes(20)) + bal[i].Address = common.Address(testrand.Bytes(20)) for j := 0; j < 5; j++ { - bal[i].StorageWrites = append(bal[i].StorageWrites, encodingSlotWrites{ - Slot: one(), Accesses: []encodingStorageWrite{{TxIdx: 0, ValueAfter: one()}}, + bal[i].StorageChanges = append(bal[i].StorageChanges, encodingSlotChanges{ + Slot: one(), SlotChanges: []encodingStorageWrite{{BlockAccessIndex: 0, PostValue: one()}}, }) } for j := 0; j < 4; j++ { diff --git a/core/types/bal/gen_account_access_json.go b/core/types/bal/gen_account_access_json.go new file mode 100644 index 0000000000..754a875e39 --- /dev/null +++ b/core/types/bal/gen_account_access_json.go @@ -0,0 +1,76 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" +) + +var _ = (*accountAccessMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (a AccountAccess) MarshalJSON() ([]byte, error) { + type AccountAccess struct { + Address common.Address `json:"address"` + StorageChanges []encodingSlotChanges `json:"storageChanges"` + StorageReads []*hexutil.U256 `json:"storageReads"` + BalanceChanges []encodingBalanceChange `json:"balanceChanges"` + NonceChanges []encodingAccountNonce `json:"nonceChanges"` + CodeChanges []encodingCodeChange `json:"codeChanges"` + } + var enc AccountAccess + enc.Address = a.Address + enc.StorageChanges = a.StorageChanges + if a.StorageReads != nil { + enc.StorageReads = make([]*hexutil.U256, len(a.StorageReads)) + for k, v := range a.StorageReads { + enc.StorageReads[k] = (*hexutil.U256)(v) + } + } + enc.BalanceChanges = a.BalanceChanges + enc.NonceChanges = a.NonceChanges + enc.CodeChanges = a.CodeChanges + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (a *AccountAccess) UnmarshalJSON(input []byte) error { + type AccountAccess struct { + Address *common.Address `json:"address"` + StorageChanges []encodingSlotChanges `json:"storageChanges"` + StorageReads []*hexutil.U256 `json:"storageReads"` + BalanceChanges []encodingBalanceChange `json:"balanceChanges"` + NonceChanges []encodingAccountNonce `json:"nonceChanges"` + CodeChanges []encodingCodeChange `json:"codeChanges"` + } + var dec AccountAccess + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Address != nil { + a.Address = *dec.Address + } + if dec.StorageChanges != nil { + a.StorageChanges = dec.StorageChanges + } + if dec.StorageReads != nil { + a.StorageReads = make([]*uint256.Int, len(dec.StorageReads)) + for k, v := range dec.StorageReads { + a.StorageReads[k] = (*uint256.Int)(v) + } + } + if dec.BalanceChanges != nil { + a.BalanceChanges = dec.BalanceChanges + } + if dec.NonceChanges != nil { + a.NonceChanges = dec.NonceChanges + } + if dec.CodeChanges != nil { + a.CodeChanges = dec.CodeChanges + } + return nil +} diff --git a/core/types/bal/gen_encoding_account_nonce_json.go b/core/types/bal/gen_encoding_account_nonce_json.go new file mode 100644 index 0000000000..62087b3d8a --- /dev/null +++ b/core/types/bal/gen_encoding_account_nonce_json.go @@ -0,0 +1,42 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*encodingAccountNonceMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e encodingAccountNonce) MarshalJSON() ([]byte, error) { + type encodingAccountNonce struct { + BlockAccessIndex hexutil.Uint64 `json:"blockAccessIndex"` + PostNonce hexutil.Uint64 `json:"postNonce"` + } + var enc encodingAccountNonce + enc.BlockAccessIndex = hexutil.Uint64(e.BlockAccessIndex) + enc.PostNonce = hexutil.Uint64(e.PostNonce) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *encodingAccountNonce) UnmarshalJSON(input []byte) error { + type encodingAccountNonce struct { + BlockAccessIndex *hexutil.Uint64 `json:"blockAccessIndex"` + PostNonce *hexutil.Uint64 `json:"postNonce"` + } + var dec encodingAccountNonce + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.BlockAccessIndex != nil { + e.BlockAccessIndex = uint32(*dec.BlockAccessIndex) + } + if dec.PostNonce != nil { + e.PostNonce = uint64(*dec.PostNonce) + } + return nil +} diff --git a/core/types/bal/gen_encoding_balance_change_json.go b/core/types/bal/gen_encoding_balance_change_json.go new file mode 100644 index 0000000000..630f81496c --- /dev/null +++ b/core/types/bal/gen_encoding_balance_change_json.go @@ -0,0 +1,43 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" +) + +var _ = (*encodingBalanceChangeMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e encodingBalanceChange) MarshalJSON() ([]byte, error) { + type encodingBalanceChange struct { + BlockAccessIndex hexutil.Uint64 `json:"blockAccessIndex"` + PostBalance *hexutil.U256 `json:"postBalance"` + } + var enc encodingBalanceChange + enc.BlockAccessIndex = hexutil.Uint64(e.BlockAccessIndex) + enc.PostBalance = (*hexutil.U256)(e.PostBalance) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *encodingBalanceChange) UnmarshalJSON(input []byte) error { + type encodingBalanceChange struct { + BlockAccessIndex *hexutil.Uint64 `json:"blockAccessIndex"` + PostBalance *hexutil.U256 `json:"postBalance"` + } + var dec encodingBalanceChange + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.BlockAccessIndex != nil { + e.BlockAccessIndex = uint32(*dec.BlockAccessIndex) + } + if dec.PostBalance != nil { + e.PostBalance = (*uint256.Int)(dec.PostBalance) + } + return nil +} diff --git a/core/types/bal/gen_encoding_code_change_json.go b/core/types/bal/gen_encoding_code_change_json.go new file mode 100644 index 0000000000..c725a47da4 --- /dev/null +++ b/core/types/bal/gen_encoding_code_change_json.go @@ -0,0 +1,42 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*encodingCodeChangeMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e encodingCodeChange) MarshalJSON() ([]byte, error) { + type encodingCodeChange struct { + BlockAccessIndex hexutil.Uint64 `json:"blockAccessIndex"` + NewCode hexutil.Bytes `json:"newCode"` + } + var enc encodingCodeChange + enc.BlockAccessIndex = hexutil.Uint64(e.BlockAccessIndex) + enc.NewCode = e.NewCode + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *encodingCodeChange) UnmarshalJSON(input []byte) error { + type encodingCodeChange struct { + BlockAccessIndex *hexutil.Uint64 `json:"blockAccessIndex"` + NewCode *hexutil.Bytes `json:"newCode"` + } + var dec encodingCodeChange + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.BlockAccessIndex != nil { + e.BlockAccessIndex = uint32(*dec.BlockAccessIndex) + } + if dec.NewCode != nil { + e.NewCode = *dec.NewCode + } + return nil +} diff --git a/core/types/bal/gen_encoding_slot_changes_json.go b/core/types/bal/gen_encoding_slot_changes_json.go new file mode 100644 index 0000000000..0cc318f3d6 --- /dev/null +++ b/core/types/bal/gen_encoding_slot_changes_json.go @@ -0,0 +1,43 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" +) + +var _ = (*encodingSlotChangesMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e encodingSlotChanges) MarshalJSON() ([]byte, error) { + type encodingSlotChanges struct { + Slot *hexutil.U256 `json:"slot"` + SlotChanges []encodingStorageWrite `json:"slotChanges"` + } + var enc encodingSlotChanges + enc.Slot = (*hexutil.U256)(e.Slot) + enc.SlotChanges = e.SlotChanges + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *encodingSlotChanges) UnmarshalJSON(input []byte) error { + type encodingSlotChanges struct { + Slot *hexutil.U256 `json:"slot"` + SlotChanges []encodingStorageWrite `json:"slotChanges"` + } + var dec encodingSlotChanges + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Slot != nil { + e.Slot = (*uint256.Int)(dec.Slot) + } + if dec.SlotChanges != nil { + e.SlotChanges = dec.SlotChanges + } + return nil +} diff --git a/core/types/bal/gen_encoding_storage_write_json.go b/core/types/bal/gen_encoding_storage_write_json.go new file mode 100644 index 0000000000..c5075c1336 --- /dev/null +++ b/core/types/bal/gen_encoding_storage_write_json.go @@ -0,0 +1,43 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package bal + +import ( + "encoding/json" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" +) + +var _ = (*encodingStorageWriteMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e encodingStorageWrite) MarshalJSON() ([]byte, error) { + type encodingStorageWrite struct { + BlockAccessIndex hexutil.Uint64 `json:"blockAccessIndex"` + PostValue *hexutil.U256 `json:"postValue"` + } + var enc encodingStorageWrite + enc.BlockAccessIndex = hexutil.Uint64(e.BlockAccessIndex) + enc.PostValue = (*hexutil.U256)(e.PostValue) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *encodingStorageWrite) UnmarshalJSON(input []byte) error { + type encodingStorageWrite struct { + BlockAccessIndex *hexutil.Uint64 `json:"blockAccessIndex"` + PostValue *hexutil.U256 `json:"postValue"` + } + var dec encodingStorageWrite + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.BlockAccessIndex != nil { + e.BlockAccessIndex = uint32(*dec.BlockAccessIndex) + } + if dec.PostValue != nil { + e.PostValue = (*uint256.Int)(dec.PostValue) + } + return nil +} diff --git a/core/types/block.go b/core/types/block.go index 0856845a4e..eab458e88a 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -101,7 +101,7 @@ type Header struct { RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` // BlockAccessListHash was added by EIP-7928 and is ignored in legacy headers. - BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` // SlotNumber was added by EIP-7843 and is ignored in legacy headers. SlotNumber *uint64 `json:"slotNumber" rlp:"optional"` diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 2e2f1cdca5..6856562215 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -37,7 +37,7 @@ func (h Header) MarshalJSON() ([]byte, error) { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` Hash common.Hash `json:"hash"` } @@ -93,7 +93,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` RequestsHash *common.Hash `json:"requestsHash" rlp:"optional"` - BlockAccessListHash *common.Hash `json:"balHash" rlp:"optional"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash" rlp:"optional"` SlotNumber *hexutil.Uint64 `json:"slotNumber" rlp:"optional"` } var dec Header diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 109169e0b0..6452fcf37c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1002,7 +1002,7 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { result["requestsHash"] = head.RequestsHash } if head.BlockAccessListHash != nil { - result["balHash"] = head.BlockAccessListHash + result["blockAccessListHash"] = head.BlockAccessListHash } if head.SlotNumber != nil { result["slotNumber"] = hexutil.Uint64(*head.SlotNumber) From efe58eac003940e736717ab2207f6db22fdbd2e6 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Wed, 20 May 2026 13:25:56 -0500 Subject: [PATCH 13/76] beacon/engine, rpc: optimize JSON encoding for large blob payloads (#33969) Adds a fast path for ExecutionPayloadEnvelope and BlobAndProofListV* that bypasses encoding/json's reflection and re-validation, which are expensive for large payloads with many blobs. Also hand-rolls the jsonrpcMessage wire encoding in the RPC codec to avoid a second re-validation pass when writing responses to the connection. Resolves #33814 --------- Co-authored-by: Marius van der Wijden Co-authored-by: Felix Lange --- beacon/engine/bapl_encode.go | 69 ++ beacon/engine/{gen_ed.go => ed_codec.go} | 0 beacon/engine/{gen_epe.go => epe_decode.go} | 25 - beacon/engine/epe_encode.go | 98 +++ beacon/engine/epe_test.go | 128 +++ .../{gen_blockparams.go => pa_codec.go} | 0 beacon/engine/types.go | 24 +- eth/catalyst/api.go | 12 +- eth/catalyst/api_benchmark_test.go | 739 ++++++++++++++++++ eth/catalyst/api_test.go | 6 +- go.mod | 3 +- go.sum | 6 +- rpc/client.go | 41 +- rpc/handler.go | 19 +- rpc/http.go | 88 ++- rpc/json.go | 154 +++- rpc/server_test.go | 42 + rpc/subscription.go | 14 +- rpc/subscription_test.go | 23 +- rpc/types.go | 7 +- rpc/websocket.go | 23 +- 21 files changed, 1366 insertions(+), 155 deletions(-) create mode 100644 beacon/engine/bapl_encode.go rename beacon/engine/{gen_ed.go => ed_codec.go} (100%) rename beacon/engine/{gen_epe.go => epe_decode.go} (62%) create mode 100644 beacon/engine/epe_encode.go create mode 100644 beacon/engine/epe_test.go rename beacon/engine/{gen_blockparams.go => pa_codec.go} (100%) create mode 100644 eth/catalyst/api_benchmark_test.go diff --git a/beacon/engine/bapl_encode.go b/beacon/engine/bapl_encode.go new file mode 100644 index 0000000000..b9f46ebf26 --- /dev/null +++ b/beacon/engine/bapl_encode.go @@ -0,0 +1,69 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package engine + +import ( + "github.com/fjl/jsonw" +) + +// MarshalJSON implements json.Marshaler. +func (list BlobAndProofListV1) MarshalJSON() ([]byte, error) { + var b jsonw.Buffer + b.Array(func() { + for _, item := range list { + marshalBlobAndProofV1(&b, item) + } + }) + return b.Output(), nil +} + +func marshalBlobAndProofV1(b *jsonw.Buffer, item *BlobAndProofV1) { + if item == nil { + b.Null() + } else { + b.Object(func() { + b.Key("blob") + b.HexBytes(item.Blob) + b.Key("proof") + b.HexBytes(item.Proof) + }) + } +} + +// MarshalJSON implements json.Marshaler. +func (list BlobAndProofListV2) MarshalJSON() ([]byte, error) { + var b jsonw.Buffer + b.Array(func() { + for _, item := range list { + marshalBlobAndProofV2(&b, item) + } + }) + return b.Output(), nil +} + +func marshalBlobAndProofV2(b *jsonw.Buffer, item *BlobAndProofV2) { + if item == nil { + b.Null() + } else { + b.Object(func() { + b.Key("blob") + b.HexBytes(item.Blob) + b.Key("proofs") + appendHexBytesArray(b, item.CellProofs) + }) + } +} diff --git a/beacon/engine/gen_ed.go b/beacon/engine/ed_codec.go similarity index 100% rename from beacon/engine/gen_ed.go rename to beacon/engine/ed_codec.go diff --git a/beacon/engine/gen_epe.go b/beacon/engine/epe_decode.go similarity index 62% rename from beacon/engine/gen_epe.go rename to beacon/engine/epe_decode.go index cf7bd9ee3f..a125daa030 100644 --- a/beacon/engine/gen_epe.go +++ b/beacon/engine/epe_decode.go @@ -12,31 +12,6 @@ import ( var _ = (*executionPayloadEnvelopeMarshaling)(nil) -// MarshalJSON marshals as JSON. -func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) { - type ExecutionPayloadEnvelope struct { - ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"` - BlockValue *hexutil.Big `json:"blockValue" gencodec:"required"` - BlobsBundle *BlobsBundle `json:"blobsBundle"` - Requests []hexutil.Bytes `json:"executionRequests"` - Override bool `json:"shouldOverrideBuilder"` - Witness *hexutil.Bytes `json:"witness,omitempty"` - } - var enc ExecutionPayloadEnvelope - enc.ExecutionPayload = e.ExecutionPayload - enc.BlockValue = (*hexutil.Big)(e.BlockValue) - enc.BlobsBundle = e.BlobsBundle - if e.Requests != nil { - enc.Requests = make([]hexutil.Bytes, len(e.Requests)) - for k, v := range e.Requests { - enc.Requests[k] = v - } - } - enc.Override = e.Override - enc.Witness = e.Witness - return json.Marshal(&enc) -} - // UnmarshalJSON unmarshals from JSON. func (e *ExecutionPayloadEnvelope) UnmarshalJSON(input []byte) error { type ExecutionPayloadEnvelope struct { diff --git a/beacon/engine/epe_encode.go b/beacon/engine/epe_encode.go new file mode 100644 index 0000000000..73deb8f0c6 --- /dev/null +++ b/beacon/engine/epe_encode.go @@ -0,0 +1,98 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package engine + +import ( + "encoding/json" + "errors" + + "github.com/fjl/jsonw" +) + +// marshalBlobsBundle writes BlobsBundle as JSON and appends it to buf. +func marshalBlobsBundle(b *jsonw.Buffer, bundle *BlobsBundle) { + if bundle == nil { + b.Null() + return + } + b.Object(func() { + b.Key("commitments") + appendHexBytesArray(b, bundle.Commitments) + b.Key("proofs") + appendHexBytesArray(b, bundle.Proofs) + b.Key("blobs") + appendHexBytesArray(b, bundle.Blobs) + }) +} + +// MarshalJSON implements json.Marshaler. +func (e ExecutionPayloadEnvelope) MarshalJSON() ([]byte, error) { + if e.ExecutionPayload == nil { + return nil, errors.New("missing required field 'executionPayload' for ExecutionPayloadEnvelope") + } + + // Pre-marshal the execution payload using its gencodec MarshalJSON. + payload, err := e.ExecutionPayload.MarshalJSON() + if err != nil { + return nil, err + } + // Pre-marshal the witness. + var witness []byte + if e.Witness != nil { + witness, err = json.Marshal(e.Witness) + if err != nil { + return nil, err + } + } + + // Write the execution payload to the buffer + var b jsonw.Buffer + b.Object(func() { + b.Key("executionPayload") + b.RawValue(payload) + + b.Key("blockValue") + b.HexBigInt(e.BlockValue) + + b.Key("blobsBundle") + marshalBlobsBundle(&b, e.BlobsBundle) + + b.Key("executionRequests") + if e.Requests == nil { + b.Null() + } else { + appendHexBytesArray(&b, e.Requests) + } + + b.Key("shouldOverrideBuilder") + b.Bool(e.Override) + + if e.Witness != nil { + b.Key("witness") + b.RawValue(witness) + } + }) + return b.Output(), nil +} + +func appendHexBytesArray[T ~[]byte](b *jsonw.Buffer, slice []T) { + b.Array(func() { + for _, elem := range slice { + b.HexBytes(elem) + } + }) +} diff --git a/beacon/engine/epe_test.go b/beacon/engine/epe_test.go new file mode 100644 index 0000000000..e4ed5b6578 --- /dev/null +++ b/beacon/engine/epe_test.go @@ -0,0 +1,128 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package engine + +import ( + "bytes" + "encoding/json" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +func makeTestPayload() *ExecutableData { + return &ExecutableData{ + ParentHash: common.HexToHash("0x01"), + FeeRecipient: common.HexToAddress("0x02"), + StateRoot: common.HexToHash("0x03"), + ReceiptsRoot: common.HexToHash("0x04"), + LogsBloom: make([]byte, 256), + Random: common.HexToHash("0x05"), + Number: 100, + GasLimit: 1000000, + GasUsed: 500000, + Timestamp: 1234567890, + ExtraData: []byte("extra"), + BaseFeePerGas: big.NewInt(7), + BlockHash: common.HexToHash("0x08"), + Transactions: [][]byte{{0xaa, 0xbb}}, + } +} + +func TestMarshalJSONRoundtrip(t *testing.T) { + witness := hexutil.Bytes{0xde, 0xad} + original := ExecutionPayloadEnvelope{ + ExecutionPayload: makeTestPayload(), + BlockValue: big.NewInt(12345), + BlobsBundle: &BlobsBundle{ + Commitments: []hexutil.Bytes{{0x01, 0x02}}, + Proofs: []hexutil.Bytes{{0x03, 0x04}}, + Blobs: []hexutil.Bytes{{0x05, 0x06}}, + }, + Requests: [][]byte{{0xaa}, {0xbb, 0xcc}}, + Override: true, + Witness: &witness, + } + + data, err := original.MarshalJSON() + if err != nil { + t.Fatalf("MarshalJSON error: %v", err) + } + + var decoded ExecutionPayloadEnvelope + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("UnmarshalJSON error: %v", err) + } + + if decoded.ExecutionPayload.Number != original.ExecutionPayload.Number { + t.Error("ExecutionPayload.Number mismatch") + } + if decoded.BlockValue.Cmp(original.BlockValue) != 0 { + t.Errorf("BlockValue mismatch: got %v, want %v", decoded.BlockValue, original.BlockValue) + } + if len(decoded.BlobsBundle.Blobs) != len(original.BlobsBundle.Blobs) { + t.Error("BlobsBundle.Blobs length mismatch") + } + if len(decoded.Requests) != len(original.Requests) { + t.Error("Requests length mismatch") + } + if decoded.Override != original.Override { + t.Error("Override mismatch") + } + if !bytes.Equal(*decoded.Witness, *original.Witness) { + t.Error("Witness mismatch") + } +} + +func TestMarshalJSONNilPayload(t *testing.T) { + env := ExecutionPayloadEnvelope{ + ExecutionPayload: nil, + BlockValue: big.NewInt(1), + } + _, err := env.MarshalJSON() + if err == nil { + t.Fatal("expected error for nil ExecutionPayload") + } +} + +// TestExecutionPayloadEnvelopeFieldCoverage guards against structural drift. +// If a field is added to or removed from ExecutionPayloadEnvelope, this test +// fails, reminding the developer to update MarshalJSON in marshal_epe.go. +func TestExecutionPayloadEnvelopeFieldCoverage(t *testing.T) { + expected := []string{ + "ExecutionPayload", + "BlockValue", + "BlobsBundle", + "Requests", + "Override", + "Witness", + } + typ := reflect.TypeOf(ExecutionPayloadEnvelope{}) + if typ.NumField() != len(expected) { + t.Fatalf("ExecutionPayloadEnvelope has %d fields, expected %d — update MarshalJSON in marshal_epe.go", + typ.NumField(), len(expected)) + } + for i, name := range expected { + if typ.Field(i).Name != name { + t.Errorf("field %d: got %q, want %q — update MarshalJSON in marshal_epe.go", + i, typ.Field(i).Name, name) + } + } +} diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/pa_codec.go similarity index 100% rename from beacon/engine/gen_blockparams.go rename to beacon/engine/pa_codec.go diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 5c31ee4e98..60d564b877 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -60,7 +60,7 @@ var ( PayloadV4 PayloadVersion = 0x4 ) -//go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go +//go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out pa_codec.go // PayloadAttributes describes the environment context in which a block should // be built. @@ -79,7 +79,7 @@ type payloadAttributesMarshaling struct { SlotNumber *hexutil.Uint64 } -//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go +//go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out ed_codec.go // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { @@ -127,7 +127,7 @@ type StatelessPayloadStatusV1 struct { ValidationError *string `json:"validationError"` } -//go:generate go run github.com/fjl/gencodec -type ExecutionPayloadEnvelope -field-override executionPayloadEnvelopeMarshaling -out gen_epe.go +//go:generate go run github.com/fjl/gencodec -enc=false -type ExecutionPayloadEnvelope -field-override executionPayloadEnvelopeMarshaling -out epe_decode.go type ExecutionPayloadEnvelope struct { ExecutionPayload *ExecutableData `json:"executionPayload" gencodec:"required"` @@ -138,6 +138,12 @@ type ExecutionPayloadEnvelope struct { Witness *hexutil.Bytes `json:"witness,omitempty"` } +// JSON type overrides for ExecutionPayloadEnvelope. +type executionPayloadEnvelopeMarshaling struct { + BlockValue *hexutil.Big + Requests []hexutil.Bytes +} + // BlobsBundle includes the marshalled sidecar data. Note this structure is // shared by BlobsBundleV1 and BlobsBundleV2 for the sake of simplicity. // @@ -154,16 +160,18 @@ type BlobAndProofV1 struct { Proof hexutil.Bytes `json:"proof"` } +// BlobAndProofListV1 is a list of BlobAndProofV1 with a hand-rolled JSON marshaler +// that avoids the overhead of encoding/json for large blob payloads. +type BlobAndProofListV1 []*BlobAndProofV1 + type BlobAndProofV2 struct { Blob hexutil.Bytes `json:"blob"` CellProofs []hexutil.Bytes `json:"proofs"` // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs. } -// JSON type overrides for ExecutionPayloadEnvelope. -type executionPayloadEnvelopeMarshaling struct { - BlockValue *hexutil.Big - Requests []hexutil.Bytes -} +// BlobAndProofListV2 is a list of BlobAndProofV2 with a hand-rolled JSON marshaler +// that avoids the overhead of encoding/json for large blob payloads. +type BlobAndProofListV2 []*BlobAndProofV2 type PayloadStatusV1 struct { Status string `json:"status"` diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 1def169ae0..b31185a40f 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -552,7 +552,7 @@ func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool, versi // // Client software MAY return an array of all null entries if syncing or otherwise // unable to serve blob pool data. -func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProofV1, error) { +func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofListV1, error) { // Reject the request if Osaka has been activated. // follow https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#cancun-api head := api.eth.BlockChain().CurrentHeader() @@ -566,7 +566,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo if err != nil { return nil, engine.InvalidParams.With(err) } - res := make([]*engine.BlobAndProofV1, len(hashes)) + res := make(engine.BlobAndProofListV1, len(hashes)) for i := 0; i < len(blobs); i++ { // Skip the non-existing blob if blobs[i] == nil { @@ -605,7 +605,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo // // Client software MUST return null if syncing or otherwise unable to serve // blob pool data. -func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) { +func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) (engine.BlobAndProofListV2, error) { head := api.eth.BlockChain().CurrentHeader() if api.config().LatestFork(head.Time) < forks.Osaka { return nil, nil @@ -616,7 +616,7 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo // GetBlobsV3 returns a set of blobs from the transaction pool. Same as // GetBlobsV2, except will return partial responses in case there is a missing // blob. -func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) { +func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) (engine.BlobAndProofListV2, error) { head := api.eth.BlockChain().CurrentHeader() if api.config().LatestFork(head.Time) < forks.Osaka { return nil, nil @@ -626,7 +626,7 @@ func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) ([]*engine.BlobAndProo // getBlobs returns all available blobs. In v2, partial responses are not allowed, // while v3 supports partial responses. -func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.BlobAndProofV2, error) { +func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAndProofListV2, error) { if len(hashes) > 128 { return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes))) } @@ -647,7 +647,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) ([]*engine.Blob } // Validate the blobs from the pool and assemble the response filled := 0 - res := make([]*engine.BlobAndProofV2, len(hashes)) + res := make(engine.BlobAndProofListV2, len(hashes)) for i := range blobs { // The blob has been evicted since the last AvailableBlobs call. // Return null if partial response is not allowed. diff --git a/eth/catalyst/api_benchmark_test.go b/eth/catalyst/api_benchmark_test.go new file mode 100644 index 0000000000..377e5caa43 --- /dev/null +++ b/eth/catalyst/api_benchmark_test.go @@ -0,0 +1,739 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . +package catalyst + +import ( + "context" + "crypto/ecdsa" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "math/big" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/holiman/uint256" +) + +// encodingType specifies which encoding to use in benchmarks +type encodingType int + +const ( + encNone encodingType = iota + encJSON + encJSONCustom + encRLP +) + +func (e encodingType) String() string { + switch e { + case encNone: + return "none" + case encJSON: + return "json" + case encJSONCustom: + return "json_custom" + case encRLP: + return "rlp" + default: + return "unknown" + } +} + +var encodingTypes = []encodingType{encNone, encJSON, encJSONCustom, encRLP} + +// benchEncode encodes the value using the specified encoding type. +// It fails the benchmark if encoding fails. +func benchEncode(b *testing.B, enc encodingType, v any) { + var err error + switch enc { + case encJSON: + _, err = json.Marshal(v) + if err != nil { + b.Fatalf("JSON marshal failed: %v", err) + } + case encJSONCustom: + if m, ok := v.(json.Marshaler); ok { + _, err = m.MarshalJSON() + } else { + _, err = json.Marshal(v) + } + if err != nil { + b.Fatalf("JSON MarshalJSON failed: %v", err) + } + case encRLP: + _, err = rlp.EncodeToBytes(v) + if err != nil { + b.Fatalf("RLP encode failed: %v", err) + } + } +} + +// benchmarkBlobCounts defines the blob counts for benchmarks +var benchmarkBlobCounts = []int{21, 72} + +// maxBenchmarkBlobs is the maximum number of blobs we need for benchmarks +var maxBenchmarkBlobs = benchmarkBlobCounts[len(benchmarkBlobCounts)-1] + +var ( + // Pre-computed blobs for benchmarks + benchBlobs []*kzg4844.Blob + benchBlobCommits []kzg4844.Commitment + benchBlobProofs []kzg4844.Proof + benchBlobCellProofs [][]kzg4844.Proof + benchBlobVHashes []common.Hash +) + +func init() { + // Pre-compute blobs for benchmarks + for i := 0; i < maxBenchmarkBlobs; i++ { + blob := &kzg4844.Blob{byte(i), byte(i >> 8)} + benchBlobs = append(benchBlobs, blob) + + commit, _ := kzg4844.BlobToCommitment(blob) + benchBlobCommits = append(benchBlobCommits, commit) + + proof, _ := kzg4844.ComputeBlobProof(blob, commit) + benchBlobProofs = append(benchBlobProofs, proof) + + cellProofs, _ := kzg4844.ComputeCellProofs(blob) + benchBlobCellProofs = append(benchBlobCellProofs, cellProofs) + + vhash := kzg4844.CalcBlobHashV1(sha256.New(), &commit) + benchBlobVHashes = append(benchBlobVHashes, vhash) + } +} + +// benchFork specifies which fork to use in benchmark environments +type benchFork int + +const ( + forkCancun benchFork = iota + forkPrague + forkOsaka +) + +// benchmarkBlobEnv holds the environment for blob benchmarks +type benchmarkBlobEnv struct { + node *node.Node + eth *eth.Ethereum + api *ConsensusAPI + config *params.ChainConfig + keys []*ecdsa.PrivateKey + vhashes []common.Hash + version byte + blobCount int + nonces []uint64 // current nonce for each key +} + +// makeBenchBlobTx creates a blob transaction with the specified number of blobs. +// blobOffset indicates which pre-computed blobs to use. +func makeBenchBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction { + var ( + blobs []kzg4844.Blob + blobHashes []common.Hash + commitments []kzg4844.Commitment + proofs []kzg4844.Proof + ) + for i := 0; i < blobCount; i++ { + idx := blobOffset + i + blobs = append(blobs, *benchBlobs[idx]) + commitments = append(commitments, benchBlobCommits[idx]) + if version == types.BlobSidecarVersion0 { + proofs = append(proofs, benchBlobProofs[idx]) + } else { + proofs = append(proofs, benchBlobCellProofs[idx]...) + } + blobHashes = append(blobHashes, benchBlobVHashes[idx]) + } + blobtx := &types.BlobTx{ + ChainID: uint256.MustFromBig(chainConfig.ChainID), + Nonce: nonce, + GasTipCap: uint256.NewInt(params.GWei), + GasFeeCap: uint256.NewInt(10 * params.GWei), + Gas: 21000, + BlobFeeCap: uint256.NewInt(params.GWei), + BlobHashes: blobHashes, + Value: uint256.NewInt(100), + Sidecar: types.NewBlobTxSidecar(version, blobs, commitments, proofs), + } + return types.MustSignNewTx(key, types.LatestSigner(chainConfig), blobtx) +} + +// newBenchmarkBlobEnv creates an environment for blob benchmarks. +// It creates multiple keys and fills the pool with blob transactions totaling the specified blob count. +// version: 0 = BlobSidecarVersion0 (pre-Osaka), 1 = BlobSidecarVersion1 (Osaka+) +// fork: which fork to enable +func newBenchmarkBlobEnv(b *testing.B, blobCount int, version byte, fork benchFork) *benchmarkBlobEnv { + // Create a configuration that allows enough blobs + config := *params.MergedTestChainConfig + // Set blob schedule to allow for large blob counts (up to 128 blobs per block) + config.BlobScheduleConfig = ¶ms.BlobScheduleConfig{ + Cancun: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 3338477}, + Prague: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, + Osaka: ¶ms.BlobConfig{Target: 6, Max: 128, UpdateFraction: 5007716}, + } + // Configure fork times based on requested fork + switch fork { + case forkCancun: + config.PragueTime = nil + config.OsakaTime = nil + case forkPrague: + config.OsakaTime = nil + case forkOsaka: + // All forks enabled (default) + } + + // Generate enough keys for all the blob transactions + // Each tx can have up to 6 blobs, so we need ceil(blobCount/6) keys + numTxs := (blobCount + 5) / 6 + keys := make([]*ecdsa.PrivateKey, numTxs) + addrs := make([]common.Address, numTxs) + alloc := make(types.GenesisAlloc) + alloc[testAddr] = types.Account{Balance: testBalance} + + for i := 0; i < numTxs; i++ { + key, _ := crypto.GenerateKey() + keys[i] = key + addrs[i] = crypto.PubkeyToAddress(key.PublicKey) + // Give each account enough balance for many transactions + alloc[addrs[i]] = types.Account{Balance: new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000))} + } + + gspec := &core.Genesis{ + Config: &config, + Alloc: alloc, + Difficulty: common.Big0, + } + n, ethServ := startEthService(b, gspec, nil) + + // Collect versioned hashes for the blobs we'll use + var vhashes []common.Hash + for i := 0; i < blobCount; i++ { + vhashes = append(vhashes, benchBlobVHashes[i]) + } + + // Fill initial blob txs into the pool + env := &benchmarkBlobEnv{ + node: n, + eth: ethServ, + api: newConsensusAPIWithoutHeartbeat(ethServ), + config: &config, + keys: keys, + vhashes: vhashes, + version: version, + blobCount: blobCount, + nonces: make([]uint64, numTxs), + } + env.addBlobTxs(b) + return env +} + +// addBlobTxs adds blob transactions to the pool using the stored blobCount and per-key nonces. +// It increments each key's nonce after adding transactions. +func (env *benchmarkBlobEnv) addBlobTxs(b *testing.B) { + numTxs := (env.blobCount + 5) / 6 + var txs []*types.Transaction + blobsRemaining := env.blobCount + blobOffset := 0 + + for i := 0; i < numTxs && blobsRemaining > 0; i++ { + // Each tx gets up to 6 blobs + txBlobCount := 6 + if blobsRemaining < 6 { + txBlobCount = blobsRemaining + } + tx := makeBenchBlobTx(env.config, env.nonces[i], txBlobCount, blobOffset, env.keys[i], env.version) + txs = append(txs, tx) + blobOffset += txBlobCount + blobsRemaining -= txBlobCount + } + errs := env.eth.TxPool().Add(txs, true) + for i, err := range errs { + if err != nil { + b.Fatalf("Failed to add blob tx %d to pool: %v", i, err) + } + } + // Increment nonce for each key used + for i := 0; i < numTxs; i++ { + env.nonces[i]++ + } +} + +// Close closes the environment +func (env *benchmarkBlobEnv) Close() { + env.node.Close() +} + +// BenchmarkGetBlobsV1 benchmarks the GetBlobsV1 method with various blob counts. +// GetBlobsV1 is available at Cancun/Prague (pre-Osaka). +func BenchmarkGetBlobsV1(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 0, forkPrague) + defer env.Close() + + b.ResetTimer() + for b.Loop() { + result, err := env.api.GetBlobsV1(env.vhashes) + if err != nil { + b.Fatalf("GetBlobsV1 failed: %v", err) + } + // Verify we got the expected number of blobs + if len(result) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(result)) + } + benchEncode(b, enc, result) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkGetBlobsV2Extended benchmarks the GetBlobsV2 method with various blob counts. +// GetBlobsV2 is available at Osaka+. +func BenchmarkGetBlobsV2Extended(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + b.ResetTimer() + for b.Loop() { + result, err := env.api.GetBlobsV2(env.vhashes) + if err != nil { + b.Fatalf("GetBlobsV2 failed: %v", err) + } + // Verify we got the expected number of blobs + if len(result) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(result)) + } + benchEncode(b, enc, result) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkGetBlobsV3 benchmarks the GetBlobsV3 method with various blob counts. +// GetBlobsV3 is available at Osaka+. +func BenchmarkGetBlobsV3(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + b.ResetTimer() + for b.Loop() { + result, err := env.api.GetBlobsV3(env.vhashes) + if err != nil { + b.Fatalf("GetBlobsV3 failed: %v", err) + } + // Verify we got the expected number of blobs + if len(result) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(result)) + } + benchEncode(b, enc, result) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkGetPayloadV5WithBlobs benchmarks GetPayloadV5 (Osaka fork) with blobs. +// Note: Measures single iteration performance due to NewPayload complexity at Osaka. +func BenchmarkGetPayloadV5WithBlobs(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + parent := env.api.eth.BlockChain().CurrentHeader() + beaconRoot := common.Hash{0x42} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Note: We don't call addBlobTxs here because we can't advance the chain + // (NewPayloadV5 requires execution requests). The same transactions are + // reused for each iteration, which still benchmarks the GetPayload performance. + timestamp := parent.Time + 12 + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: parent.Hash(), + SafeBlockHash: parent.Hash(), + FinalizedBlockHash: parent.Hash(), + } + payloadAttr := &engine.PayloadAttributes{ + Timestamp: timestamp, + Random: common.Hash{byte(i)}, + SuggestedFeeRecipient: testAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + } + resp, err := env.api.ForkchoiceUpdatedV3(context.Background(), fcState, payloadAttr) + if err != nil { + b.Fatalf("ForkchoiceUpdatedV3 failed: %v", err) + } + if resp.PayloadID == nil { + b.Fatalf("ForkchoiceUpdatedV3 returned nil PayloadID") + } + // Wait for the payload to be built with transactions + time.Sleep(100 * time.Millisecond) + + envelope, err := env.api.GetPayloadV5(*resp.PayloadID) + if err != nil { + b.Fatalf("GetPayloadV5 failed: %v", err) + } + if envelope.BlobsBundle == nil { + b.Fatalf("BlobsBundle is nil") + } + // Verify we got the expected number of blobs + if len(envelope.BlobsBundle.Blobs) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(envelope.BlobsBundle.Blobs)) + } + benchEncode(b, enc, envelope) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkNewPayloadV3WithBlobs benchmarks the NewPayloadV3 method with various blob counts. +// Each iteration processes a payload with the full blob count. +func BenchmarkNewPayloadV3WithBlobs(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 0, forkCancun) + defer env.Close() + + parent := env.api.eth.BlockChain().CurrentHeader() + beaconRoot := common.Hash{0x42} + + // Build a payload first to get valid executable data + timestamp := parent.Time + 12 + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: parent.Hash(), + SafeBlockHash: parent.Hash(), + FinalizedBlockHash: parent.Hash(), + } + payloadAttr := &engine.PayloadAttributes{ + Timestamp: timestamp, + Random: common.Hash{0x01}, + SuggestedFeeRecipient: testAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + } + resp, err := env.api.ForkchoiceUpdatedV3(context.Background(), fcState, payloadAttr) + if err != nil { + b.Fatalf("ForkchoiceUpdatedV3 failed: %v", err) + } + if resp.PayloadID == nil { + b.Fatalf("ForkchoiceUpdatedV3 returned nil PayloadID") + } + // Wait for the payload to be built with transactions + time.Sleep(100 * time.Millisecond) + + // Get the payload + envelope, err := env.api.GetPayloadV3(*resp.PayloadID) + if err != nil { + b.Fatalf("GetPayloadV3 failed: %v", err) + } + // Verify we got the expected number of blobs + if len(envelope.BlobsBundle.Blobs) != blobCount { + b.Fatalf("expected %d blobs in setup, got %d", blobCount, len(envelope.BlobsBundle.Blobs)) + } + + execData := envelope.ExecutionPayload + // Collect versioned hashes from blobs bundle + vhashes := make([]common.Hash, len(envelope.BlobsBundle.Commitments)) + for j, commitment := range envelope.BlobsBundle.Commitments { + var commit kzg4844.Commitment + copy(commit[:], commitment) + vhashes[j] = kzg4844.CalcBlobHashV1(sha256.New(), &commit) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // NewPayload is idempotent, calling it multiple times with the same data + // should return the same result. The payload contains blobCount blobs. + result, err := env.api.NewPayloadV3(context.Background(), *execData, vhashes, &beaconRoot) + if err != nil { + b.Fatalf("NewPayloadV3 failed: %v", err) + } + benchEncode(b, enc, result) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkForkchoiceUpdatedWithBlobPayload benchmarks forkchoice updates that trigger +// payload building with blob transactions. +// Note: Measures ForkchoiceUpdated performance with blob transactions in the pool. +func BenchmarkForkchoiceUpdatedWithBlobPayload(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 0, forkCancun) + defer env.Close() + + parent := env.api.eth.BlockChain().CurrentHeader() + beaconRoot := common.Hash{0x42} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Note: We don't call addBlobTxs here because the blob pool has + // a per-account limit of 16 transactions. The same transactions are + // reused for each iteration, which still benchmarks the ForkchoiceUpdated + // performance with blob transactions in the pool. + timestamp := parent.Time + 12 + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: parent.Hash(), + SafeBlockHash: parent.Hash(), + FinalizedBlockHash: parent.Hash(), + } + payloadAttr := &engine.PayloadAttributes{ + Timestamp: timestamp, + Random: common.Hash{byte(i)}, + SuggestedFeeRecipient: testAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + } + resp, err := env.api.ForkchoiceUpdatedV3(context.Background(), fcState, payloadAttr) + if err != nil { + b.Fatalf("ForkchoiceUpdatedV3 failed: %v", err) + } + if resp.PayloadID == nil { + b.Fatalf("ForkchoiceUpdatedV3 returned nil PayloadID") + } + benchEncode(b, enc, resp) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// BenchmarkFullBlobWorkflowOsaka benchmarks the complete blob workflow at Osaka: +// ForkchoiceUpdated -> GetPayload +// Note: Measures single iteration performance due to NewPayload complexity at Osaka. +func BenchmarkFullBlobWorkflowOsaka(b *testing.B) { + for _, blobCount := range benchmarkBlobCounts { + for _, enc := range encodingTypes { + b.Run(fmt.Sprintf("blobs=%d/enc=%s", blobCount, enc), func(b *testing.B) { + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + parent := env.api.eth.BlockChain().CurrentHeader() + beaconRoot := common.Hash{0x42} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // Note: We don't call addBlobTxs here because we can't advance the chain + // (NewPayloadV5 requires execution requests). The same transactions are + // reused for each iteration, which still benchmarks the workflow performance. + + // 1. ForkchoiceUpdated to build payload + timestamp := parent.Time + 12 + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: parent.Hash(), + SafeBlockHash: parent.Hash(), + FinalizedBlockHash: parent.Hash(), + } + payloadAttr := &engine.PayloadAttributes{ + Timestamp: timestamp, + Random: common.Hash{byte(i)}, + SuggestedFeeRecipient: testAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + } + resp, err := env.api.ForkchoiceUpdatedV3(context.Background(), fcState, payloadAttr) + if err != nil { + b.Fatalf("ForkchoiceUpdatedV3 failed: %v", err) + } + if resp.PayloadID == nil { + b.Fatalf("ForkchoiceUpdatedV3 returned nil PayloadID") + } + // Encode ForkchoiceUpdated response + benchEncode(b, enc, resp) + + // Wait for the payload to be built with transactions + time.Sleep(100 * time.Millisecond) + + // 2. GetPayload + envelope, err := env.api.GetPayloadV5(*resp.PayloadID) + if err != nil { + b.Fatalf("GetPayloadV5 failed: %v", err) + } + if envelope.BlobsBundle == nil { + b.Fatalf("BlobsBundle is nil") + } + // Verify we got the expected number of blobs + if len(envelope.BlobsBundle.Blobs) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(envelope.BlobsBundle.Blobs)) + } + // Encode GetPayload response + benchEncode(b, enc, envelope) + } + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") + }) + } + } +} + +// discardConn is a net.Conn-like writer that discards all output. +// Used to measure server-side RPC cost without client-side decoding. +type discardConn struct { + io.Reader + io.Writer +} + +func (discardConn) Close() error { return nil } +func (discardConn) SetWriteDeadline(time.Time) error { return nil } + +// BenchmarkGetPayloadV5RPCServerOnly benchmarks only the EL server-side cost of +// engine_getPayloadV5: method dispatch, JSON serialization, and wire encoding. +// Client-side decoding is excluded by writing to io.Discard. +func BenchmarkGetPayloadV5RPCServerOnly(b *testing.B) { + blobCount := 72 + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + // Register the engine API on the running node's in-process RPC server. + rpcServer, err := env.node.RPCHandler() + if err != nil { + b.Fatalf("RPCHandler failed: %v", err) + } + rpcServer.RegisterName("engine", env.api) + + parent := env.api.eth.BlockChain().CurrentHeader() + beaconRoot := common.Hash{0x42} + + // Build one payload to get a valid payloadID. + fcState := engine.ForkchoiceStateV1{ + HeadBlockHash: parent.Hash(), + SafeBlockHash: parent.Hash(), + FinalizedBlockHash: parent.Hash(), + } + payloadAttr := &engine.PayloadAttributes{ + Timestamp: parent.Time + 12, + Random: common.Hash{0x01}, + SuggestedFeeRecipient: testAddr, + Withdrawals: []*types.Withdrawal{}, + BeaconRoot: &beaconRoot, + } + resp, err := env.api.ForkchoiceUpdatedV3(context.Background(), fcState, payloadAttr) + if err != nil { + b.Fatalf("ForkchoiceUpdatedV3 failed: %v", err) + } + if resp.PayloadID == nil { + b.Fatalf("ForkchoiceUpdatedV3 returned nil PayloadID") + } + time.Sleep(100 * time.Millisecond) + + // Verify the payload has the expected blobs via the direct API first. + envelope, err := env.api.GetPayloadV5(*resp.PayloadID) + if err != nil { + b.Fatalf("GetPayloadV5 failed: %v", err) + } + if len(envelope.BlobsBundle.Blobs) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(envelope.BlobsBundle.Blobs)) + } + b.Logf("payload size: %d blobs, %d txs", len(envelope.BlobsBundle.Blobs), len(envelope.ExecutionPayload.Transactions)) + + // Build the JSON-RPC request bytes once. + reqJSON := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"engine_getPayloadV5","params":["%s"]}`, resp.PayloadID.String()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + conn := discardConn{ + Reader: strings.NewReader(reqJSON), + Writer: io.Discard, + } + codec := rpc.NewCodec(conn) + rpcServer.ServeCodec(codec, 0) + } + b.StopTimer() + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") +} + +// BenchmarkGetBlobsV3RPCServerOnly benchmarks only the EL server-side cost of +// engine_getBlobsV3: method dispatch, JSON serialization, and wire encoding. +// Client-side decoding is excluded by writing to io.Discard. +func BenchmarkGetBlobsV3RPCServerOnly(b *testing.B) { + blobCount := 72 + env := newBenchmarkBlobEnv(b, blobCount, 1, forkOsaka) + defer env.Close() + + // Register the engine API on the running node's in-process RPC server. + rpcServer, err := env.node.RPCHandler() + if err != nil { + b.Fatalf("RPCHandler failed: %v", err) + } + rpcServer.RegisterName("engine", env.api) + + // Verify the blobs are available via the direct API first. + result, err := env.api.GetBlobsV3(env.vhashes) + if err != nil { + b.Fatalf("GetBlobsV3 failed: %v", err) + } + if len(result) != blobCount { + b.Fatalf("expected %d blobs, got %d", blobCount, len(result)) + } + b.Logf("blob count: %d", blobCount) + + // Build the JSON-RPC request bytes once. + // Format the versioned hashes as a JSON array of hex strings. + var hashStrs []string + for _, h := range env.vhashes { + hashStrs = append(hashStrs, fmt.Sprintf(`"%s"`, h.Hex())) + } + reqJSON := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"engine_getBlobsV3","params":[[%s]]}`, strings.Join(hashStrs, ",")) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + conn := discardConn{ + Reader: strings.NewReader(reqJSON), + Writer: io.Discard, + } + codec := rpc.NewCodec(conn) + rpcServer.ServeCodec(codec, 0) + } + b.StopTimer() + b.ReportMetric(float64(b.Elapsed().Milliseconds())/float64(b.N), "ms/op") +} diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 1f38c4dd8a..65d78d84ee 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -1961,7 +1961,7 @@ func TestGetBlobsV1(t *testing.T) { // Fill the request for retrieving blobs var ( vhashes []common.Hash - expect []*engine.BlobAndProofV1 + expect engine.BlobAndProofListV1 ) // fill missing blob at the beginning if suite.fillRandom { @@ -2072,13 +2072,13 @@ func BenchmarkGetBlobsV2(b *testing.B) { } } -type getBlobsFn func(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) +type getBlobsFn func(hashes []common.Hash) (engine.BlobAndProofListV2, error) func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom bool, expectPartialResponse bool, name string) { // Fill the request for retrieving blobs var ( vhashes []common.Hash - expect []*engine.BlobAndProofV2 + expect engine.BlobAndProofListV2 ) for j := start; j < limit; j++ { vhashes = append(vhashes, testBlobVHashes[j]) diff --git a/go.mod b/go.mod index 17897a62c0..56869d255d 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/ethereum/hid v1.0.1-0.20260421154323-c2ab8d9bf68a github.com/fatih/color v1.16.0 github.com/ferranbt/fastssz v0.1.4 + github.com/fjl/jsonw v0.1.0 github.com/fsnotify/fsnotify v1.6.0 github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff github.com/gofrs/flock v0.12.1 @@ -121,7 +122,7 @@ require ( github.com/deepmap/oapi-codegen v1.6.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect - github.com/fjl/gencodec v0.1.0 // indirect + github.com/fjl/gencodec v0.1.2 // indirect github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect diff --git a/go.sum b/go.sum index bad8a44cfd..6335fb5698 100644 --- a/go.sum +++ b/go.sum @@ -123,8 +123,10 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fjl/gencodec v0.1.0 h1:B3K0xPfc52cw52BBgUbSPxYo+HlLfAgWMVKRWXUXBcs= -github.com/fjl/gencodec v0.1.0/go.mod h1:Um1dFHPONZGTHog1qD1NaWjXJW/SPB38wPv0O8uZ2fI= +github.com/fjl/gencodec v0.1.2 h1:nf+MMsmuii5ZQMbS6/xjZoe5LRkN0415FOJOSwmnuW8= +github.com/fjl/gencodec v0.1.2/go.mod h1:chDHL3wKXuBgauP8x3XNZkl5EIAR5SoCTmmmDTZRzmw= +github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= +github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= diff --git a/rpc/client.go b/rpc/client.go index 8d81503d59..9175626241 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -364,7 +364,7 @@ func (c *Client) CallContext(ctx context.Context, result interface{}, method str resp := batchresp[0] switch { case resp.Error != nil: - return resp.Error + return resp.decodeError() case len(resp.Result) == 0: return ErrNoResult default: @@ -419,7 +419,7 @@ func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error { if c.isHTTP { err = c.sendBatchHTTP(ctx, op, msgs) } else { - err = c.send(ctx, op, msgs) + err = c.sendBatch(ctx, op, msgs) } if err != nil { return err @@ -449,7 +449,7 @@ func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error { elem := &b[index] switch { case resp.Error != nil: - elem.Error = resp.Error + elem.Error = resp.decodeError() case resp.Result == nil: elem.Error = ErrNoResult default: @@ -552,7 +552,7 @@ func (c *Client) newMessage(method string, paramsIn ...interface{}) (*jsonrpcMes // send registers op with the dispatch loop, then sends msg on the connection. // if sending fails, op is deregistered. -func (c *Client) send(ctx context.Context, op *requestOp, msg interface{}) error { +func (c *Client) send(ctx context.Context, op *requestOp, msg *jsonrpcMessage) error { select { case c.reqInit <- op: err := c.write(ctx, msg, false) @@ -567,7 +567,22 @@ func (c *Client) send(ctx context.Context, op *requestOp, msg interface{}) error } } -func (c *Client) write(ctx context.Context, msg interface{}, retry bool) error { +// sendBatch registers op with the dispatch loop, then sends a batch of messages +// on the connection. If sending fails, op is deregistered. +func (c *Client) sendBatch(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error { + select { + case c.reqInit <- op: + err := c.writeBatch(ctx, msgs, false) + c.reqSent <- err + return err + case <-ctx.Done(): + return ctx.Err() + case <-c.closing: + return ErrClientQuit + } +} + +func (c *Client) write(ctx context.Context, msg *jsonrpcMessage, retry bool) error { if c.writeConn == nil { // The previous write failed. Try to establish a new connection. if err := c.reconnect(ctx); err != nil { @@ -584,6 +599,22 @@ func (c *Client) write(ctx context.Context, msg interface{}, retry bool) error { return err } +func (c *Client) writeBatch(ctx context.Context, msgs []*jsonrpcMessage, retry bool) error { + if c.writeConn == nil { + if err := c.reconnect(ctx); err != nil { + return err + } + } + err := c.writeConn.writeJSONBatch(ctx, msgs, false) + if err != nil { + c.writeConn = nil + if !retry { + return c.writeBatch(ctx, msgs, true) + } + } + return err +} + func (c *Client) reconnect(ctx context.Context) error { if c.reconnectFunc == nil { return errDead diff --git a/rpc/handler.go b/rpc/handler.go index c0af162f13..89fc78236c 100644 --- a/rpc/handler.go +++ b/rpc/handler.go @@ -169,7 +169,7 @@ func (b *batchCallBuffer) doWrite(ctx context.Context, conn jsonWriter, isErrorR } b.wrote = true // can only write once if len(b.resp) > 0 { - conn.writeJSON(ctx, b.resp, isErrorResponse) + conn.writeJSONBatch(ctx, b.resp, isErrorResponse) } } @@ -237,7 +237,7 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) { resp := h.handleCallMsg(cp, msg) callBuffer.pushResponse(resp) if resp != nil && h.batchResponseMaxSize != 0 { - responseBytes += len(resp.Result) + responseBytes += len(resp.Result) + len(resp.Error) if responseBytes > h.batchResponseMaxSize { err := &internalServerError{errcodeResponseTooLarge, errMsgResponseTooLarge} callBuffer.respondWithError(cp.ctx, h.conn, err) @@ -268,7 +268,7 @@ func (h *handler) respondWithBatchTooLarge(cp *callProc, batch []*jsonrpcMessage break } } - h.conn.writeJSON(cp.ctx, []*jsonrpcMessage{resp}, true) + h.conn.writeJSONBatch(cp.ctx, []*jsonrpcMessage{resp}, true) } // handleMsg handles a single non-batch message. @@ -415,7 +415,7 @@ func (h *handler) handleResponses(batch []*jsonrpcMessage, handleCall func(*json // the op.resp channel. if op.sub != nil { if msg.Error != nil { - op.err = msg.Error + op.err = msg.decodeError() } else { op.err = json.Unmarshal(msg.Result, &op.sub.subid) if op.err == nil { @@ -481,9 +481,10 @@ func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMess var logctx []any logctx = append(logctx, "reqid", idForLog{msg.ID}, "duration", time.Since(start)) if resp.Error != nil { - logctx = append(logctx, "err", resp.Error.Message) - if resp.Error.Data != nil { - logctx = append(logctx, "errdata", formatErrorData(resp.Error.Data)) + je := resp.decodeError() + logctx = append(logctx, "err", je.Message) + if je.Data != nil { + logctx = append(logctx, "errdata", formatErrorData(je.Data)) } h.log.Warn("Served "+msg.Method, logctx...) } else { @@ -550,7 +551,7 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage answer := h.runMethod(rctx, msg, callb, args) var rErr error if answer.Error != nil { - rErr = errors.New(answer.Error.Message) + rErr = errors.New(answer.decodeError().Message) } rSpanEnd(&rErr) @@ -623,7 +624,7 @@ func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *cal _, _, spanEnd := telemetry.StartSpanWithTracer(ctx, h.tracer(), "rpc.encodeJSONResponse", attributes...) response := msg.response(result) if response.Error != nil { - err = errors.New(response.Error.Message) + err = errors.New(response.decodeError().Message) } spanEnd(&err) return response diff --git a/rpc/http.go b/rpc/http.go index 55f0abfa72..49618244df 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -57,10 +57,14 @@ type httpConn struct { // and some methods don't work. The panic() stubs here exist to ensure // this special treatment is correct. -func (hc *httpConn) writeJSON(context.Context, interface{}, bool) error { +func (hc *httpConn) writeJSON(context.Context, *jsonrpcMessage, bool) error { panic("writeJSON called on httpConn") } +func (hc *httpConn) writeJSONBatch(context.Context, []*jsonrpcMessage, bool) error { + panic("writeJSONBatch called on httpConn") +} + func (hc *httpConn) peerInfo() PeerInfo { panic("peerInfo called on httpConn") } @@ -179,9 +183,9 @@ func cleanlyCloseBody(body io.ReadCloser) error { return body.Close() } -func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error { +func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg *jsonrpcMessage) error { hc := c.writeConn.(*httpConn) - respBody, err := hc.doRequest(ctx, msg) + respBody, err := hc.doRequest(ctx, appendMessage(nil, msg)) if err != nil { return err } @@ -198,7 +202,7 @@ func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) e func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error { hc := c.writeConn.(*httpConn) - respBody, err := hc.doRequest(ctx, msgs) + respBody, err := hc.doRequest(ctx, appendBatch(nil, msgs)) if err != nil { return err } @@ -212,11 +216,7 @@ func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonr return nil } -func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadCloser, error) { - body, err := json.Marshal(msg) - if err != nil { - return nil, err - } +func (hc *httpConn) doRequest(ctx context.Context, body []byte) (io.ReadCloser, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, hc.url, io.NopCloser(bytes.NewReader(body))) if err != nil { return nil, err @@ -268,41 +268,51 @@ func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) Serve body := io.LimitReader(r.Body, int64(s.httpBodyLimit)) conn := &httpServerConn{Reader: body, Writer: w, r: r} - encoder := func(v any, isErrorResponse bool) error { - if !isErrorResponse { - return json.NewEncoder(conn).Encode(v) - } - - // It's an error response and requires special treatment. - // - // In case of a timeout error, the response must be written before the HTTP - // server's write timeout occurs. So we need to flush the response. The - // Content-Length header also needs to be set to ensure the client knows - // when it has the full response. - encdata, err := json.Marshal(v) - if err != nil { - return err - } - w.Header().Set("content-length", strconv.Itoa(len(encdata))) - - // If this request is wrapped in a handler that might remove Content-Length (such - // as the automatic gzip we do in package node), we need to ensure the HTTP server - // doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked - // encoding might not be finished correctly, and some clients do not like it when - // the final chunk is missing. - w.Header().Set("transfer-encoding", "identity") - - _, err = w.Write(encdata) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - return err + var buf []byte + encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + buf = appendMessage(buf[:0], msg) + return httpWriteResult(w, buf, isError) + } + encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + buf = appendBatch(buf[:0], msgs) + return httpWriteResult(w, buf, isError) } dec := json.NewDecoder(conn) dec.UseNumber() - return NewFuncCodec(conn, encoder, dec.Decode) + return NewFuncCodec(conn, encodeMsg, encodeBatch, dec.Decode) +} + +// httpWriteResult writes pre-encoded response data over HTTP. +// For error responses, it sets Content-Length and flushes to ensure the response +// is fully written before any HTTP server write timeout occurs. +func httpWriteResult(w http.ResponseWriter, data []byte, isError bool) error { + if !isError { + _, err := w.Write(data) + return err + } + + // It's an error response and requires special treatment. + // + // In case of a timeout error, the response must be written before the HTTP + // server's write timeout occurs. So we need to flush the response. The + // Content-Length header also needs to be set to ensure the client knows + // when it has the full response. + w.Header().Set("content-length", strconv.Itoa(len(data))) + + // If this request is wrapped in a handler that might remove Content-Length (such + // as the automatic gzip we do in package node), we need to ensure the HTTP server + // doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked + // encoding might not be finished correctly, and some clients do not like it when + // the final chunk is missing. + w.Header().Set("transfer-encoding", "identity") + + _, err := w.Write(data) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + return err } // Close does nothing and always returns nil. diff --git a/rpc/json.go b/rpc/json.go index fcd801fc95..9813acae73 100644 --- a/rpc/json.go +++ b/rpc/json.go @@ -27,6 +27,8 @@ import ( "strings" "sync" "time" + + "github.com/fjl/jsonw" ) const ( @@ -52,12 +54,6 @@ type subscriptionResultEnc struct { Result any `json:"result"` } -type jsonrpcSubscriptionNotification struct { - Version string `json:"jsonrpc"` - Method string `json:"method"` - Params subscriptionResultEnc `json:"params"` -} - // A value of this type can a JSON-RPC request, notification, successful response or // error response. Which one it is depends on the fields. type jsonrpcMessage struct { @@ -65,7 +61,7 @@ type jsonrpcMessage struct { ID json.RawMessage `json:"id,omitempty"` Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` - Error *jsonError `json:"error,omitempty"` + Error json.RawMessage `json:"error,omitempty"` Result json.RawMessage `json:"result,omitempty"` } @@ -113,8 +109,29 @@ func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage { return resp } +// decodeError decodes the Error field into a jsonError struct. +func (msg *jsonrpcMessage) decodeError() *jsonError { + if msg.Error == nil { + return nil + } + je := new(jsonError) + json.Unmarshal(msg.Error, je) + return je +} + func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage { - enc, err := json.Marshal(result) + var ( + enc []byte + err error + ) + // Call MarshalJSON directly for types that implement it. This avoids the + // expensive validation/compaction pass that json.Marshal performs on + // encoder output. + if m, ok := result.(json.Marshaler); ok { + enc, err = m.MarshalJSON() + } else { + enc, err = json.Marshal(result) + } if err != nil { return msg.errorResponse(&internalServerError{errcodeMarshalError, err.Error()}) } @@ -122,19 +139,18 @@ func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage { } func errorMessage(err error) *jsonrpcMessage { - msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{ + je := &jsonError{ Code: errcodeDefault, Message: err.Error(), - }} - ec, ok := err.(Error) - if ok { - msg.Error.Code = ec.ErrorCode() } - de, ok := err.(DataError) - if ok { - msg.Error.Data = de.ErrorData() + if ec, ok := err.(Error); ok { + je.Code = ec.ErrorCode() } - return msg + if de, ok := err.(DataError); ok { + je.Data = de.ErrorData() + } + enc, _ := json.Marshal(je) + return &jsonrpcMessage{Version: vsn, ID: null, Error: enc} } type jsonError struct { @@ -179,28 +195,32 @@ type ConnRemoteAddr interface { // jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has // support for parsing arguments and serializing (result) objects. type jsonCodec struct { - remote string - closer sync.Once // close closed channel once - closeCh chan interface{} // closed on Close - decode decodeFunc // decoder to allow multiple transports - encMu sync.Mutex // guards the encoder - encode encodeFunc // encoder to allow multiple transports - conn deadlineCloser + remote string + closer sync.Once // close closed channel once + closeCh chan interface{} // closed on Close + decode decodeFunc // decoder to allow multiple transports + encMu sync.Mutex // guards the encoder + encodeMsg encodeMsgFunc // single-message encoder + encodeBatch encodeBatchFunc // batch encoder + conn deadlineCloser } -type encodeFunc = func(v interface{}, isErrorResponse bool) error +type encodeMsgFunc = func(msg *jsonrpcMessage, isError bool) error + +type encodeBatchFunc = func(msgs []*jsonrpcMessage, isError bool) error type decodeFunc = func(v interface{}) error // NewFuncCodec creates a codec which uses the given functions to read and write. If conn // implements ConnRemoteAddr, log messages will use it to include the remote address of // the connection. -func NewFuncCodec(conn deadlineCloser, encode encodeFunc, decode decodeFunc) ServerCodec { +func NewFuncCodec(conn deadlineCloser, encodeMsg encodeMsgFunc, encodeBatch encodeBatchFunc, decode decodeFunc) ServerCodec { codec := &jsonCodec{ - closeCh: make(chan interface{}), - encode: encode, - decode: decode, - conn: conn, + closeCh: make(chan interface{}), + encodeMsg: encodeMsg, + encodeBatch: encodeBatch, + decode: decode, + conn: conn, } if ra, ok := conn.(ConnRemoteAddr); ok { codec.remote = ra.RemoteAddr() @@ -211,14 +231,62 @@ func NewFuncCodec(conn deadlineCloser, encode encodeFunc, decode decodeFunc) Ser // NewCodec creates a codec on the given connection. If conn implements ConnRemoteAddr, log // messages will use it to include the remote address of the connection. func NewCodec(conn Conn) ServerCodec { - enc := json.NewEncoder(conn) dec := json.NewDecoder(conn) dec.UseNumber() - - encode := func(v interface{}, isErrorResponse bool) error { - return enc.Encode(v) + var buf []byte + encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + buf = appendMessage(buf[:0], msg) + buf = append(buf, '\n') + _, err := conn.Write(buf) + return err } - return NewFuncCodec(conn, encode, dec.Decode) + encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + buf = appendBatch(buf[:0], msgs) + buf = append(buf, '\n') + _, err := conn.Write(buf) + return err + } + return NewFuncCodec(conn, encodeMsg, encodeBatch, dec.Decode) +} + +// appendMessage appends the JSON-RPC encoding of msg to buf. +func appendMessage(buf []byte, msg *jsonrpcMessage) []byte { + buf = append(buf, `{"jsonrpc":"2.0"`...) + if msg.ID != nil { + buf = append(buf, `,"id":`...) + buf = append(buf, msg.ID...) + } + if msg.Method != "" { + buf = append(buf, `,"method":`...) + buf = jsonw.AppendQuotedString(buf, msg.Method) + } + if msg.Params != nil { + buf = append(buf, `,"params":`...) + buf = append(buf, msg.Params...) + } + if msg.Error != nil { + buf = append(buf, `,"error":`...) + buf = append(buf, msg.Error...) + } + if msg.Result != nil { + buf = append(buf, `,"result":`...) + buf = append(buf, msg.Result...) + } + buf = append(buf, '}') + return buf +} + +// appendBatch appends the JSON-RPC encoding of a message batch to buf. +func appendBatch(buf []byte, msgs []*jsonrpcMessage) []byte { + buf = append(buf, '[') + for i, msg := range msgs { + if i > 0 { + buf = append(buf, ',') + } + buf = appendMessage(buf, msg) + } + buf = append(buf, ']') + return buf } func (c *jsonCodec) peerInfo() PeerInfo { @@ -248,7 +316,7 @@ func (c *jsonCodec) readBatch() (messages []*jsonrpcMessage, batch bool, err err return messages, batch, nil } -func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}, isErrorResponse bool) error { +func (c *jsonCodec) writeJSON(ctx context.Context, msg *jsonrpcMessage, isError bool) error { c.encMu.Lock() defer c.encMu.Unlock() @@ -257,7 +325,19 @@ func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}, isErrorRespons deadline = time.Now().Add(defaultWriteTimeout) } c.conn.SetWriteDeadline(deadline) - return c.encode(v, isErrorResponse) + return c.encodeMsg(msg, isError) +} + +func (c *jsonCodec) writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { + c.encMu.Lock() + defer c.encMu.Unlock() + + deadline, ok := ctx.Deadline() + if !ok { + deadline = time.Now().Add(defaultWriteTimeout) + } + c.conn.SetWriteDeadline(deadline) + return c.encodeBatch(msgs, isError) } func (c *jsonCodec) close() { diff --git a/rpc/server_test.go b/rpc/server_test.go index 8334d4e80d..a2b8af2b7f 100644 --- a/rpc/server_test.go +++ b/rpc/server_test.go @@ -208,6 +208,48 @@ func TestServerBatchResponseSizeLimit(t *testing.T) { } } +// TestServerBatchResponseSizeLimit_errorResponses verifies that error responses +// are counted toward BatchResponseMaxSize. +func TestServerBatchResponseSizeLimit_errorResponses(t *testing.T) { + t.Parallel() + + server := newTestServer() + defer server.Stop() + // Each error response for test_returnError is ~58 bytes of JSON in the Error field. + // Set limit to 100 so 1 response fits (58 bytes) but the 2nd (116 bytes) exceeds it. + server.SetBatchLimits(100, 100) + var ( + batch []BatchElem + client = DialInProc(server) + ) + for i := 0; i < 5; i++ { + batch = append(batch, BatchElem{ + Method: "test_returnError", + Result: new(int), + }) + } + if err := client.BatchCall(batch); err != nil { + t.Fatal("error sending batch:", err) + } + for i := range batch { + re, ok := batch[i].Error.(Error) + if !ok { + t.Fatalf("batch elem %d has wrong error type: %v", i, batch[i].Error) + } + if i < 2 { + // First two: elem 0 fits under limit, elem 1 pushes over but is already processed. + if re.ErrorCode() != 444 { + t.Errorf("batch elem %d wrong error code, have %d want 444", i, re.ErrorCode()) + } + } else { + // Remaining should be the response-too-large error. + if re.ErrorCode() != errcodeResponseTooLarge { + t.Errorf("batch elem %d wrong error code, have %d want %d", i, re.ErrorCode(), errcodeResponseTooLarge) + } + } + } +} + func TestServerWebsocketReadLimit(t *testing.T) { t.Parallel() diff --git a/rpc/subscription.go b/rpc/subscription.go index 9e400c8b60..6b90bd4a3b 100644 --- a/rpc/subscription.go +++ b/rpc/subscription.go @@ -171,13 +171,17 @@ func (n *Notifier) activate() error { } func (n *Notifier) send(sub *Subscription, data any) error { - msg := jsonrpcSubscriptionNotification{ + params, err := json.Marshal(subscriptionResultEnc{ + ID: string(sub.ID), + Result: data, + }) + if err != nil { + return err + } + msg := jsonrpcMessage{ Version: vsn, Method: n.namespace + notificationMethodSuffix, - Params: subscriptionResultEnc{ - ID: string(sub.ID), - Result: data, - }, + Params: params, } return n.h.conn.writeJSON(context.Background(), &msg, false) } diff --git a/rpc/subscription_test.go b/rpc/subscription_test.go index cd44d219de..623b496124 100644 --- a/rpc/subscription_test.go +++ b/rpc/subscription_test.go @@ -220,7 +220,7 @@ func readAndValidateMessage(in *json.Decoder) (*subConfirmation, *subscriptionRe case msg.isResponse(): var c subConfirmation if msg.Error != nil { - return nil, nil, msg.Error + return nil, nil, msg.decodeError() } else if err := json.Unmarshal(msg.Result, &c.subid); err != nil { return nil, nil, fmt.Errorf("invalid response: %v", err) } else { @@ -233,12 +233,21 @@ func readAndValidateMessage(in *json.Decoder) (*subConfirmation, *subscriptionRe } type mockConn struct { - enc *json.Encoder + w io.Writer } -// writeJSON writes a message to the connection. -func (c *mockConn) writeJSON(ctx context.Context, msg interface{}, isError bool) error { - return c.enc.Encode(msg) +func (c *mockConn) writeJSON(ctx context.Context, msg *jsonrpcMessage, isError bool) error { + buf := appendMessage(nil, msg) + buf = append(buf, '\n') + _, err := c.w.Write(buf) + return err +} + +func (c *mockConn) writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { + buf := appendBatch(nil, msgs) + buf = append(buf, '\n') + _, err := c.w.Write(buf) + return err } // closed returns a channel which is closed when the connection is closed. @@ -251,7 +260,7 @@ func (c *mockConn) remoteAddr() string { return "" } func BenchmarkNotify(b *testing.B) { id := ID("test") notifier := &Notifier{ - h: &handler{conn: &mockConn{json.NewEncoder(io.Discard)}}, + h: &handler{conn: &mockConn{io.Discard}}, sub: &Subscription{ID: id}, activated: true, } @@ -271,7 +280,7 @@ func TestNotify(t *testing.T) { out := new(bytes.Buffer) id := ID("test") notifier := &Notifier{ - h: &handler{conn: &mockConn{json.NewEncoder(out)}}, + h: &handler{conn: &mockConn{out}}, sub: &Subscription{ID: id}, activated: true, } diff --git a/rpc/types.go b/rpc/types.go index 85f15344e8..578d3f86dd 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -51,9 +51,10 @@ type ServerCodec interface { // jsonWriter can write JSON messages to its underlying connection. // Implementations must be safe for concurrent use. type jsonWriter interface { - // writeJSON writes a message to the connection. - writeJSON(ctx context.Context, msg interface{}, isError bool) error - + // writeJSON writes a single JSON-RPC message to the connection. + writeJSON(ctx context.Context, msg *jsonrpcMessage, isError bool) error + // writeJSONBatch writes a batch of JSON-RPC messages to the connection. + writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error // Closed returns a channel which is closed when the connection is closed. closed() <-chan interface{} // RemoteAddr returns the peer address of the connection. diff --git a/rpc/websocket.go b/rpc/websocket.go index ec676b9caf..e70498873a 100644 --- a/rpc/websocket.go +++ b/rpc/websocket.go @@ -293,11 +293,17 @@ type websocketCodec struct { func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, readLimit int64) ServerCodec { conn.SetReadLimit(readLimit) - encode := func(v interface{}, isErrorResponse bool) error { - return conn.WriteJSON(v) + var buf []byte + encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + buf = appendMessage(buf[:0], msg) + return conn.WriteMessage(websocket.TextMessage, buf) + } + encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + buf = appendBatch(buf[:0], msgs) + return conn.WriteMessage(websocket.TextMessage, buf) } wc := &websocketCodec{ - jsonCodec: NewFuncCodec(conn, encode, conn.ReadJSON).(*jsonCodec), + jsonCodec: NewFuncCodec(conn, encodeMsg, encodeBatch, conn.ReadJSON).(*jsonCodec), conn: conn, pingReset: make(chan struct{}, 1), pongReceived: make(chan struct{}), @@ -342,8 +348,15 @@ func (wc *websocketCodec) peerInfo() PeerInfo { return wc.info } -func (wc *websocketCodec) writeJSON(ctx context.Context, v interface{}, isError bool) error { - err := wc.jsonCodec.writeJSON(ctx, v, isError) +func (wc *websocketCodec) writeJSON(ctx context.Context, msg *jsonrpcMessage, isError bool) error { + return wc.writeAndResetPing(wc.jsonCodec.writeJSON(ctx, msg, isError)) +} + +func (wc *websocketCodec) writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { + return wc.writeAndResetPing(wc.jsonCodec.writeJSONBatch(ctx, msgs, isError)) +} + +func (wc *websocketCodec) writeAndResetPing(err error) error { if err == nil { // Notify pingLoop to delay the next idle ping. select { From ef5041ef4dc827da91adf946952358e4ef1781e9 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Thu, 21 May 2026 09:54:32 +0200 Subject: [PATCH 14/76] eth/catalyst: engine_hasBlobs (#34859) Co-authored-by: healthykim Co-authored-by: Felix Lange --- core/txpool/blobpool/blobpool.go | 18 +++++++----------- eth/catalyst/api.go | 7 ++++++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index d33629365f..da54952674 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1695,18 +1695,14 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo return blobs, commitments, proofs, nil } -// AvailableBlobs returns the number of blobs that are available in the subpool. -func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int { - available := 0 - for _, vhash := range vhashes { - // Retrieve the datastore item (in a short lock) - p.lock.RLock() - _, exists := p.lookup.storeidOfBlob(vhash) - p.lock.RUnlock() - if exists { - available++ - } +// AvailableBlobs returns whether the blobs are available in the subpool. +func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) []bool { + available := make([]bool, len(vhashes)) + p.lock.RLock() + for i, vhash := range vhashes { + _, available[i] = p.lookup.storeidOfBlob(vhash) } + p.lock.RUnlock() return available } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index b31185a40f..a1f9673de8 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -630,7 +630,7 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd if len(hashes) > 128 { return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes))) } - available := api.eth.BlobTxPool().AvailableBlobs(hashes) + available := len(api.eth.BlobTxPool().AvailableBlobs(hashes)) getBlobsRequestedCounter.Inc(int64(len(hashes))) getBlobsAvailableCounter.Inc(int64(available)) @@ -679,6 +679,11 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd return res, nil } +// HasBlobs reports availability for the requested blob-versioned-hashes. +func (api *ConsensusAPI) HasBlobs(hashes []common.Hash) []bool { + return api.eth.BlobTxPool().AvailableBlobs(hashes) +} + // Helper for NewPayload* methods. var invalidStatus = engine.PayloadStatusV1{Status: engine.INVALID} From dc07433d878edd49c376ed62a9f5749cc5ad31f9 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 21 May 2026 09:56:15 +0200 Subject: [PATCH 15/76] beacon/engine: preserve nil blob list JSON (#35019) Fixes a regression where nil results from getBlobs were encoded as an empty array instead of null. --------- Co-authored-by: Felix Lange --- beacon/engine/bapl_encode.go | 6 ++++++ eth/catalyst/api_test.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/beacon/engine/bapl_encode.go b/beacon/engine/bapl_encode.go index b9f46ebf26..5a1ce47658 100644 --- a/beacon/engine/bapl_encode.go +++ b/beacon/engine/bapl_encode.go @@ -22,6 +22,9 @@ import ( // MarshalJSON implements json.Marshaler. func (list BlobAndProofListV1) MarshalJSON() ([]byte, error) { + if list == nil { + return []byte("null"), nil + } var b jsonw.Buffer b.Array(func() { for _, item := range list { @@ -46,6 +49,9 @@ func marshalBlobAndProofV1(b *jsonw.Buffer, item *BlobAndProofV1) { // MarshalJSON implements json.Marshaler. func (list BlobAndProofListV2) MarshalJSON() ([]byte, error) { + if list == nil { + return []byte("null"), nil + } var b jsonw.Buffer b.Array(func() { for _, item := range list { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 65d78d84ee..05d688ed9f 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -22,6 +22,7 @@ import ( "crypto/ecdsa" crand "crypto/rand" "crypto/sha256" + "encoding/json" "errors" "fmt" "math/big" @@ -2105,6 +2106,13 @@ func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom } else { // Nil is expected if getBlobs can not return a partial response expect = nil + enc, err := json.Marshal(result) + if err != nil { + t.Fatalf("Failed to encode result for case %s: %v", name, err) + } + if string(enc) != "null" { + t.Fatalf("Unexpected JSON result for case %s: got %s, want null", name, enc) + } } } if !reflect.DeepEqual(result, expect) { From 36520d8199a871a88f0d12db44ed729e1e4a3fb6 Mon Sep 17 00:00:00 2001 From: DeFi Junkie Date: Thu, 21 May 2026 16:31:26 +0300 Subject: [PATCH 16/76] accounts/usbwallet: add support for blob and setcode transactions (#33797) Adds Ledger signing support for BlobTxType (EIP-4844) and SetCodeTxType (EIP-7702) transactions. --------- Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- accounts/usbwallet/ledger.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go index 80e63f1864..79ff5929ba 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger.go @@ -334,26 +334,41 @@ func (w *ledgerDriver) ledgerSign(derivationPath []uint32, tx *types.Transaction err error ) if chainID == nil { - if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil { + if txrlp, err = rlp.EncodeToBytes([]any{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil { return common.Address{}, nil, err } } else { - if tx.Type() == types.DynamicFeeTxType { - if txrlp, err = rlp.EncodeToBytes([]interface{}{chainID, tx.Nonce(), tx.GasTipCap(), tx.GasFeeCap(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList()}); err != nil { + switch tx.Type() { + case types.SetCodeTxType: + if txrlp, err = rlp.EncodeToBytes([]any{chainID, tx.Nonce(), tx.GasTipCap(), tx.GasFeeCap(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations()}); err != nil { return common.Address{}, nil, err } // append type to transaction txrlp = append([]byte{tx.Type()}, txrlp...) - } else if tx.Type() == types.AccessListTxType { - if txrlp, err = rlp.EncodeToBytes([]interface{}{chainID, tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList()}); err != nil { + case types.BlobTxType: + if txrlp, err = rlp.EncodeToBytes([]any{chainID, tx.Nonce(), tx.GasTipCap(), tx.GasFeeCap(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList(), tx.BlobGasFeeCap(), tx.BlobHashes()}); err != nil { return common.Address{}, nil, err } // append type to transaction txrlp = append([]byte{tx.Type()}, txrlp...) - } else if tx.Type() == types.LegacyTxType { - if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil { + case types.DynamicFeeTxType: + if txrlp, err = rlp.EncodeToBytes([]any{chainID, tx.Nonce(), tx.GasTipCap(), tx.GasFeeCap(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList()}); err != nil { return common.Address{}, nil, err } + // append type to transaction + txrlp = append([]byte{tx.Type()}, txrlp...) + case types.AccessListTxType: + if txrlp, err = rlp.EncodeToBytes([]any{chainID, tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), tx.AccessList()}); err != nil { + return common.Address{}, nil, err + } + // append type to transaction + txrlp = append([]byte{tx.Type()}, txrlp...) + case types.LegacyTxType: + if txrlp, err = rlp.EncodeToBytes([]any{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil { + return common.Address{}, nil, err + } + default: + return common.Address{}, nil, fmt.Errorf("unsupported transaction type: %d", tx.Type()) } } payload := append(path, txrlp...) From 4daaaadfc4706b0a49d4dfde3559de7be968c28a Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Thu, 21 May 2026 16:00:57 -0300 Subject: [PATCH 17/76] eth/catalyst: implement engine_newPayloadWithWitnessV5 and use witness field spec ordering (#35009) This PR: - Adds `engine_newPayloadWithWitnessV5`. The codebase already supports the previous `VX`, so only `V5` was missing. - Make the consensus witness format use the field [ordering defined in the spec](https://github.com/ethereum/execution-specs/blob/8d7e68f4b7fa58107647164d1749853a6ed00eb3/src/ethereum/forks/amsterdam/stateless_host_exec_witness.py#L175-L176) to make it canonical. cc @gballet --------- Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- core/stateless/encoding.go | 20 ++++- core/stateless/encoding_test.go | 130 ++++++++++++++++++++++++++++++++ eth/catalyst/witness.go | 30 ++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 core/stateless/encoding_test.go diff --git a/core/stateless/encoding.go b/core/stateless/encoding.go index 1b20c4cb2a..a34cba6ebd 100644 --- a/core/stateless/encoding.go +++ b/core/stateless/encoding.go @@ -17,8 +17,10 @@ package stateless import ( + "bytes" "errors" "io" + "slices" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" @@ -28,16 +30,25 @@ import ( // ToExtWitness converts our internal witness representation to the consensus one. func (w *Witness) ToExtWitness() *ExtWitness { ext := &ExtWitness{ - Headers: w.Headers, + Headers: slices.Clone(w.Headers), } + slices.Reverse(ext.Headers) + ext.Codes = make([]hexutil.Bytes, 0, len(w.Codes)) for code := range w.Codes { ext.Codes = append(ext.Codes, []byte(code)) } + slices.SortFunc(ext.Codes, func(a, b hexutil.Bytes) int { + return bytes.Compare(a, b) + }) + ext.State = make([]hexutil.Bytes, 0, len(w.State)) for node := range w.State { ext.State = append(ext.State, []byte(node)) } + slices.SortFunc(ext.State, func(a, b hexutil.Bytes) int { + return bytes.Compare(a, b) + }) return ext } @@ -46,7 +57,12 @@ func (w *Witness) FromExtWitness(ext *ExtWitness) error { if len(ext.Headers) == 0 { return errors.New("witness must contain at least one header") } - w.Headers = ext.Headers + w.Headers = slices.Clone(ext.Headers) + // don't trust the input and sort headers in reverse order + // this is only useful for calling `Root` + slices.SortFunc(w.Headers, func(a, b *types.Header) int { + return b.Number.Cmp(a.Number) + }) w.Codes = make(map[string]struct{}, len(ext.Codes)) for _, code := range ext.Codes { diff --git a/core/stateless/encoding_test.go b/core/stateless/encoding_test.go new file mode 100644 index 0000000000..e90c450137 --- /dev/null +++ b/core/stateless/encoding_test.go @@ -0,0 +1,130 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package stateless + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +func TestWitnessToExtWitnessOrdersFields(t *testing.T) { + witness := &Witness{ + Headers: []*types.Header{testHeader(3), testHeader(2), testHeader(1)}, + Codes: map[string]struct{}{ + string([]byte{0x02}): {}, + string([]byte{0x01, 0xff}): {}, + string([]byte{0x01}): {}, + }, + State: map[string]struct{}{ + string([]byte{0xff}): {}, + string([]byte{0x00}): {}, + string([]byte{0x7f}): {}, + }, + } + ext := witness.ToExtWitness() + + checkHeaderNumbers(t, ext.Headers, []uint64{1, 2, 3}) + checkBytes(t, "codes", ext.Codes, [][]byte{ + {0x01}, + {0x01, 0xff}, + {0x02}, + }) + checkBytes(t, "state", ext.State, [][]byte{ + {0x00}, + {0x7f}, + {0xff}, + }) +} + +func TestWitnessFromExtWitnessNormalizesHeaderOrder(t *testing.T) { + tests := []struct { + name string + headers []*types.Header + }{ + { + name: "spec ordered", + headers: []*types.Header{testHeader(1), testHeader(2), testHeader(3)}, + }, + { + name: "not ordered", + headers: []*types.Header{testHeader(2), testHeader(3), testHeader(1)}, + }, + { + name: "internal ordered", + headers: []*types.Header{testHeader(3), testHeader(2), testHeader(1)}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var witness Witness + if err := witness.FromExtWitness(&ExtWitness{Headers: tt.headers}); err != nil { + t.Fatalf("FromExtWitness returned error: %v", err) + } + checkHeaderNumbers(t, witness.Headers, []uint64{3, 2, 1}) + if root := witness.Root(); root != testHeaderRoot(3) { + t.Fatalf("root mismatch: have %s, want %s", root, testHeaderRoot(3)) + } + }) + } +} + +func TestWitnessFromExtWitnessRejectsEmptyHeaders(t *testing.T) { + var witness Witness + if err := witness.FromExtWitness(&ExtWitness{}); err == nil { + t.Fatal("expected empty witness error") + } +} + +func testHeader(number uint64) *types.Header { + return &types.Header{ + Number: new(big.Int).SetUint64(number), + Root: testHeaderRoot(number), + } +} + +func testHeaderRoot(number uint64) common.Hash { + return common.Hash{byte(number)} +} + +func checkHeaderNumbers(t *testing.T, headers []*types.Header, want []uint64) { + t.Helper() + if len(headers) != len(want) { + t.Fatalf("header count mismatch: have %d, want %d", len(headers), len(want)) + } + for i, header := range headers { + if header.Number.Uint64() != want[i] { + t.Fatalf("header %d number mismatch: have %d, want %d", i, header.Number.Uint64(), want[i]) + } + } +} + +func checkBytes(t *testing.T, name string, got []hexutil.Bytes, want [][]byte) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("%s count mismatch: have %d, want %d", name, len(got), len(want)) + } + for i := range got { + if !bytes.Equal(got[i], want[i]) { + t.Fatalf("%s %d mismatch: have %x, want %x", name, i, got[i], want[i]) + } + } +} diff --git a/eth/catalyst/witness.go b/eth/catalyst/witness.go index fe75c66908..05bcbbd81f 100644 --- a/eth/catalyst/witness.go +++ b/eth/catalyst/witness.go @@ -162,6 +162,36 @@ func (api *ConsensusAPI) NewPayloadWithWitnessV4(ctx context.Context, params eng return api.newPayload(ctx, params, versionedHashes, beaconRoot, requests, true) } +// NewPayloadWithWitnessV5 is analogous to NewPayloadV5, only it also generates +// and returns a stateless witness after running the payload. +func (api *ConsensusAPI) NewPayloadWithWitnessV5(ctx context.Context, params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, executionRequests []hexutil.Bytes) (engine.PayloadStatusV1, error) { + switch { + case params.Withdrawals == nil: + return invalidStatus, paramsErr("nil withdrawals post-shanghai") + case params.ExcessBlobGas == nil: + return invalidStatus, paramsErr("nil excessBlobGas post-cancun") + case params.BlobGasUsed == nil: + return invalidStatus, paramsErr("nil blobGasUsed post-cancun") + case versionedHashes == nil: + return invalidStatus, paramsErr("nil versionedHashes post-cancun") + case beaconRoot == nil: + return invalidStatus, paramsErr("nil beaconRoot post-cancun") + case executionRequests == nil: + return invalidStatus, paramsErr("nil executionRequests post-prague") + case params.BlockAccessList == nil: + return invalidStatus, paramsErr("nil block access list post-amsterdam") + case params.SlotNumber == nil: + return invalidStatus, paramsErr("nil slotnumber post-amsterdam") + case !api.checkFork(params.Timestamp, forks.Amsterdam): + return invalidStatus, unsupportedForkErr("newPayloadV5 must only be called for amsterdam payloads") + } + requests := convertRequests(executionRequests) + if err := validateRequests(requests); err != nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(err) + } + return api.newPayload(ctx, params, versionedHashes, beaconRoot, requests, true) +} + // ExecuteStatelessPayloadV1 is analogous to NewPayloadV1, only it operates in // a stateless mode on top of a provided witness instead of the local database. func (api *ConsensusAPI) ExecuteStatelessPayloadV1(params engine.ExecutableData, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { From 2522b716f4af8202ed019aadc13b8e2fe17d9b92 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Thu, 21 May 2026 22:24:14 -0500 Subject: [PATCH 18/76] eth/catalyst, core/txpool/blobpool: add tracing to GetBlobs endpoints (#35026) - Adds tracing to the `GetBlobsV1/V2/V3` - Adds `blobs.requested` and `blobs.filled` attributes to `GetBlobsV1/V2/V3` spans. - Adds tracing to `BlobTxPool().GetBlobs()` --- core/txpool/blobpool/blobpool.go | 7 ++++- core/txpool/blobpool/blobpool_test.go | 7 +++-- eth/catalyst/api.go | 42 +++++++++++++++++++++------ eth/catalyst/api_benchmark_test.go | 8 ++--- eth/catalyst/api_test.go | 8 ++--- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index da54952674..15f4430cc6 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -19,6 +19,7 @@ package blobpool import ( "container/heap" + "context" "errors" "fmt" "math" @@ -39,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/telemetry" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" @@ -1620,7 +1622,10 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata { // The version argument specifies the type of proofs to return, either the // blob proofs (version 0) or the cell proofs (version 1). Proofs conversion is // CPU intensive and prohibited in the blobpool explicitly. -func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blob, []kzg4844.Commitment, [][]kzg4844.Proof, error) { +func (p *BlobPool) GetBlobs(ctx context.Context, vhashes []common.Hash, version byte) (_ []*kzg4844.Blob, _ []kzg4844.Commitment, _ [][]kzg4844.Proof, err error) { + _, _, spanEnd := telemetry.StartSpan(ctx, "blobpool.GetBlobs") + defer spanEnd(&err) + var ( blobs = make([]*kzg4844.Blob, len(vhashes)) commitments = make([]kzg4844.Commitment, len(vhashes)) diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 8032e21e8a..49ff4bfe1c 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -18,6 +18,7 @@ package blobpool import ( "bytes" + "context" "crypto/ecdsa" "crypto/sha256" "errors" @@ -440,11 +441,11 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) { hashes = append(hashes, tx.vhashes...) } } - blobs1, _, proofs1, err := pool.GetBlobs(hashes, types.BlobSidecarVersion0) + blobs1, _, proofs1, err := pool.GetBlobs(context.Background(), hashes, types.BlobSidecarVersion0) if err != nil { t.Fatal(err) } - blobs2, _, proofs2, err := pool.GetBlobs(hashes, types.BlobSidecarVersion1) + blobs2, _, proofs2, err := pool.GetBlobs(context.Background(), hashes, types.BlobSidecarVersion1) if err != nil { t.Fatal(err) } @@ -2087,7 +2088,7 @@ func TestGetBlobs(t *testing.T) { filled[len(vhashes)] = struct{}{} vhashes = append(vhashes, testrand.Hash()) } - blobs, _, proofs, err := pool.GetBlobs(vhashes, c.version) + blobs, _, proofs, err := pool.GetBlobs(context.Background(), vhashes, c.version) if err != nil { t.Errorf("Unexpected error for case %d, %v", i, err) } diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index a1f9673de8..4109971dc8 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -552,7 +552,19 @@ func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool, versi // // Client software MAY return an array of all null entries if syncing or otherwise // unable to serve blob pool data. -func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofListV1, error) { +func (api *ConsensusAPI) GetBlobsV1(ctx context.Context, hashes []common.Hash) (result engine.BlobAndProofListV1, err error) { + var ( + filled int + attrs = []telemetry.Attribute{ + telemetry.Int64Attribute("blobs.requested", int64(len(hashes))), + } + ) + ctx, span, spanEnd := telemetry.StartSpan(ctx, "engine.getBlobsV1", attrs...) + defer func() { + span.SetAttributes(telemetry.Int64Attribute("blobs.filled", int64(filled))) + spanEnd(&err) + }() + // Reject the request if Osaka has been activated. // follow https://github.com/ethereum/execution-apis/blob/main/src/engine/osaka.md#cancun-api head := api.eth.BlockChain().CurrentHeader() @@ -562,7 +574,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofLi if len(hashes) > 128 { return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes))) } - blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(hashes, types.BlobSidecarVersion0) + blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(ctx, hashes, types.BlobSidecarVersion0) if err != nil { return nil, engine.InvalidParams.With(err) } @@ -576,6 +588,7 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofLi Blob: blobs[i][:], Proof: proofs[i][0][:], } + filled++ } return res, nil } @@ -605,28 +618,40 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) (engine.BlobAndProofLi // // Client software MUST return null if syncing or otherwise unable to serve // blob pool data. -func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) (engine.BlobAndProofListV2, error) { +func (api *ConsensusAPI) GetBlobsV2(ctx context.Context, hashes []common.Hash) (engine.BlobAndProofListV2, error) { head := api.eth.BlockChain().CurrentHeader() if api.config().LatestFork(head.Time) < forks.Osaka { return nil, nil } - return api.getBlobs(hashes, true) + return api.getBlobs(ctx, hashes, true) } // GetBlobsV3 returns a set of blobs from the transaction pool. Same as // GetBlobsV2, except will return partial responses in case there is a missing // blob. -func (api *ConsensusAPI) GetBlobsV3(hashes []common.Hash) (engine.BlobAndProofListV2, error) { +func (api *ConsensusAPI) GetBlobsV3(ctx context.Context, hashes []common.Hash) (engine.BlobAndProofListV2, error) { head := api.eth.BlockChain().CurrentHeader() if api.config().LatestFork(head.Time) < forks.Osaka { return nil, nil } - return api.getBlobs(hashes, false) + return api.getBlobs(ctx, hashes, false) } // getBlobs returns all available blobs. In v2, partial responses are not allowed, // while v3 supports partial responses. -func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAndProofListV2, error) { +func (api *ConsensusAPI) getBlobs(ctx context.Context, hashes []common.Hash, v2 bool) (result engine.BlobAndProofListV2, err error) { + var ( + filled int + attrs = []telemetry.Attribute{ + telemetry.Int64Attribute("blobs.requested", int64(len(hashes))), + } + ) + ctx, span, spanEnd := telemetry.StartSpan(ctx, "engine.getBlobs", attrs...) + defer func() { + span.SetAttributes(telemetry.Int64Attribute("blobs.filled", int64(filled))) + spanEnd(&err) + }() + if len(hashes) > 128 { return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes))) } @@ -641,12 +666,11 @@ func (api *ConsensusAPI) getBlobs(hashes []common.Hash, v2 bool) (engine.BlobAnd } // Retrieve blobs from the pool. This operation is expensive and may involve // heavy disk I/O. - blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(hashes, types.BlobSidecarVersion1) + blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(ctx, hashes, types.BlobSidecarVersion1) if err != nil { return nil, engine.InvalidParams.With(err) } // Validate the blobs from the pool and assemble the response - filled := 0 res := make(engine.BlobAndProofListV2, len(hashes)) for i := range blobs { // The blob has been evicted since the last AvailableBlobs call. diff --git a/eth/catalyst/api_benchmark_test.go b/eth/catalyst/api_benchmark_test.go index 377e5caa43..ee0a0a4888 100644 --- a/eth/catalyst/api_benchmark_test.go +++ b/eth/catalyst/api_benchmark_test.go @@ -302,7 +302,7 @@ func BenchmarkGetBlobsV1(b *testing.B) { b.ResetTimer() for b.Loop() { - result, err := env.api.GetBlobsV1(env.vhashes) + result, err := env.api.GetBlobsV1(context.Background(), env.vhashes) if err != nil { b.Fatalf("GetBlobsV1 failed: %v", err) } @@ -329,7 +329,7 @@ func BenchmarkGetBlobsV2Extended(b *testing.B) { b.ResetTimer() for b.Loop() { - result, err := env.api.GetBlobsV2(env.vhashes) + result, err := env.api.GetBlobsV2(context.Background(), env.vhashes) if err != nil { b.Fatalf("GetBlobsV2 failed: %v", err) } @@ -356,7 +356,7 @@ func BenchmarkGetBlobsV3(b *testing.B) { b.ResetTimer() for b.Loop() { - result, err := env.api.GetBlobsV3(env.vhashes) + result, err := env.api.GetBlobsV3(context.Background(), env.vhashes) if err != nil { b.Fatalf("GetBlobsV3 failed: %v", err) } @@ -708,7 +708,7 @@ func BenchmarkGetBlobsV3RPCServerOnly(b *testing.B) { rpcServer.RegisterName("engine", env.api) // Verify the blobs are available via the direct API first. - result, err := env.api.GetBlobsV3(env.vhashes) + result, err := env.api.GetBlobsV3(context.Background(), env.vhashes) if err != nil { b.Fatalf("GetBlobsV3 failed: %v", err) } diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 05d688ed9f..0cf2c5c8e6 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -1987,7 +1987,7 @@ func TestGetBlobsV1(t *testing.T) { vhashes = append(vhashes, testrand.Hash()) expect = append(expect, nil) } - result, err := api.GetBlobsV1(vhashes) + result, err := api.GetBlobsV1(context.Background(), vhashes) if err != nil { t.Errorf("Unexpected error for case %d, %v", i, err) } @@ -2009,7 +2009,7 @@ func TestGetBlobsV1AfterOsakaFork(t *testing.T) { var engineErr *engine.EngineAPIError api := newConsensusAPIWithoutHeartbeat(ethServ) - _, err := api.GetBlobsV1([]common.Hash{testrand.Hash()}) + _, err := api.GetBlobsV1(context.Background(), []common.Hash{testrand.Hash()}) if !errors.As(err, &engineErr) { t.Fatalf("Unexpected error: %T", err) } else { @@ -2073,7 +2073,7 @@ func BenchmarkGetBlobsV2(b *testing.B) { } } -type getBlobsFn func(hashes []common.Hash) (engine.BlobAndProofListV2, error) +type getBlobsFn func(ctx context.Context, hashes []common.Hash) (engine.BlobAndProofListV2, error) func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom bool, expectPartialResponse bool, name string) { // Fill the request for retrieving blobs @@ -2096,7 +2096,7 @@ func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom if fillRandom { vhashes = append(vhashes, testrand.Hash()) } - result, err := getBlobs(vhashes) + result, err := getBlobs(context.Background(), vhashes) if err != nil { t.Errorf("Unexpected error for case %s, %v", name, err) } From 92cd26cae052cc3952f9b006ffab91eea620b96e Mon Sep 17 00:00:00 2001 From: Miki Noir Date: Fri, 22 May 2026 05:33:21 +0200 Subject: [PATCH 19/76] core: add code cache hit/miss meters (#34821) --- core/blockchain.go | 4 ++++ core/blockchain_stats.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/core/blockchain.go b/core/blockchain.go index 7b5a910b7a..23b4169372 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -84,11 +84,15 @@ var ( accountCacheMissMeter = metrics.NewRegisteredMeter("chain/account/reads/cache/process/miss", nil) storageCacheHitMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/process/hit", nil) storageCacheMissMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/process/miss", nil) + codeCacheHitMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/process/hit", nil) + codeCacheMissMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/process/miss", nil) accountCacheHitPrefetchMeter = metrics.NewRegisteredMeter("chain/account/reads/cache/prefetch/hit", nil) accountCacheMissPrefetchMeter = metrics.NewRegisteredMeter("chain/account/reads/cache/prefetch/miss", nil) storageCacheHitPrefetchMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/prefetch/hit", nil) storageCacheMissPrefetchMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/prefetch/miss", nil) + codeCacheHitPrefetchMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/prefetch/hit", nil) + codeCacheMissPrefetchMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/prefetch/miss", nil) accountReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/account/single/reads", nil) storageReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/storage/single/reads", nil) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index d753b0b700..4f518a34d8 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -96,11 +96,15 @@ func (s *ExecuteStats) reportMetrics() { accountCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.AccountCacheMiss) storageCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheHit) storageCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheMiss) + codeCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.CodeStats.CacheHit) + codeCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.CodeStats.CacheMiss) accountCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheHit) accountCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheMiss) storageCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheHit) storageCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheMiss) + codeCacheHitMeter.Mark(s.StateReadCacheStats.CodeStats.CacheHit) + codeCacheMissMeter.Mark(s.StateReadCacheStats.CodeStats.CacheMiss) } // slowBlockLog represents the JSON structure for slow block logging. From 12eabbd76d04f104c89693a2f379173380144de1 Mon Sep 17 00:00:00 2001 From: felipe Date: Thu, 21 May 2026 21:40:09 -0600 Subject: [PATCH 20/76] cmd/evm/internal/t8ntool: Amsterdam t8n updates; adds BAL and slotNum (#35025) The changes here enable us to fill tests with Amsterdam using geth EVM bin. This will be useful for block builder tests using `testing_buildBlockV1` endpoint and for filling benchmarking compute and stateful tests as Python is too slow for benchmark tests. Tested in [ethereum/execution-specs](https://github.com/ethereum/execution-specs) with: ``` uv run fill --clean --fork=Amsterdam tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py --evm-bin=$GETH_EVM_PATH ``` --- cmd/evm/internal/t8ntool/execution.go | 17 ++++++++++++----- cmd/evm/internal/t8ntool/gen_execresult.go | 7 +++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index bd089c9f55..39dfbf772b 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -77,8 +77,8 @@ type ExecutionResult struct { RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests [][]byte `json:"requests"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` - BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` + BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } type executionResultMarshaling struct { @@ -192,6 +192,9 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, GasLimit: pre.Env.GasLimit, GetHash: getHash, } + if pre.Env.SlotNumber != nil { + vmContext.SlotNum = *pre.Env.SlotNumber + } // If currentBaseFee is defined, add it to the vmContext. if pre.Env.BaseFee != nil { vmContext.BaseFee = new(big.Int).Set(pre.Env.BaseFee) @@ -396,10 +399,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, execRs.Requests = requests } if isAmsterdam { - bal := blockAccessList.ToEncodingObj() - balHash := bal.Hash() + encoded := blockAccessList.ToEncodingObj() + balRLP, err := rlp.EncodeToBytes(encoded) + if err != nil { + return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not encode BAL: %v", err)) + } + balHash := encoded.Hash() execRs.BlockAccessListHash = &balHash - execRs.BlockAccessList = bal + execRs.BlockAccessList = balRLP } // Re-create statedb instance with new root for MPT mode diff --git a/cmd/evm/internal/t8ntool/gen_execresult.go b/cmd/evm/internal/t8ntool/gen_execresult.go index f678c65de2..2fc6bd134f 100644 --- a/cmd/evm/internal/t8ntool/gen_execresult.go +++ b/cmd/evm/internal/t8ntool/gen_execresult.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/types/bal" ) var _ = (*executionResultMarshaling)(nil) @@ -33,7 +32,7 @@ func (e ExecutionResult) MarshalJSON() ([]byte, error) { CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests []hexutil.Bytes `json:"requests"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList hexutil.Bytes `json:"blockAccessList,omitempty"` BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var enc ExecutionResult @@ -80,7 +79,7 @@ func (e *ExecutionResult) UnmarshalJSON(input []byte) error { CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` RequestsHash *common.Hash `json:"requestsHash,omitempty"` Requests []hexutil.Bytes `json:"requests"` - BlockAccessList *bal.BlockAccessList `json:"blockAccessList,omitempty"` + BlockAccessList *hexutil.Bytes `json:"blockAccessList,omitempty"` BlockAccessListHash *common.Hash `json:"blockAccessListHash,omitempty"` } var dec ExecutionResult @@ -138,7 +137,7 @@ func (e *ExecutionResult) UnmarshalJSON(input []byte) error { } } if dec.BlockAccessList != nil { - e.BlockAccessList = dec.BlockAccessList + e.BlockAccessList = *dec.BlockAccessList } if dec.BlockAccessListHash != nil { e.BlockAccessListHash = dec.BlockAccessListHash From a059a357d14514bcf1b8020cdf5f38cbf3f59911 Mon Sep 17 00:00:00 2001 From: rayoo Date: Fri, 22 May 2026 13:58:31 +0800 Subject: [PATCH 21/76] eth/catalyst: count actually-available blobs in getBlobs (#35028) --- eth/catalyst/api.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 4109971dc8..5dc0deb324 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -655,7 +655,12 @@ func (api *ConsensusAPI) getBlobs(ctx context.Context, hashes []common.Hash, v2 if len(hashes) > 128 { return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes))) } - available := len(api.eth.BlobTxPool().AvailableBlobs(hashes)) + available := 0 + for _, ok := range api.eth.BlobTxPool().AvailableBlobs(hashes) { + if ok { + available++ + } + } getBlobsRequestedCounter.Inc(int64(len(hashes))) getBlobsAvailableCounter.Inc(int64(available)) From d3edc58ef74184f1a19fb82d79f83de64733cd84 Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Fri, 22 May 2026 06:59:02 +0100 Subject: [PATCH 22/76] graphql: handle missing block body in Raw resolver (#35027) Return empty raw bytes when the GraphQL `Block.raw` resolver cannot load the block body. This matches the nil handling used by the other block-body-backed resolvers and avoids exposing RLP empty-list bytes as raw block data. --- graphql/graphql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/graphql.go b/graphql/graphql.go index dadc91fac0..b25683cb0f 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -929,7 +929,7 @@ func (b *Block) RawHeader(ctx context.Context) (hexutil.Bytes, error) { func (b *Block) Raw(ctx context.Context) (hexutil.Bytes, error) { block, err := b.resolve(ctx) - if err != nil { + if err != nil || block == nil { return hexutil.Bytes{}, err } return rlp.EncodeToBytes(block) From c0fc5e0bda7f14f6c29231b5f1b1a1be2245f9b7 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Tue, 26 May 2026 09:36:28 +0200 Subject: [PATCH 23/76] internal/ethapi: fix base fee too low error code in eth_simulateV1 (#34951) Fixes the issue discovered in https://github.com/NethermindEth/nethermind/issues/11412. --- internal/ethapi/errors.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/ethapi/errors.go b/internal/ethapi/errors.go index cc79af6f3c..392bf86cf4 100644 --- a/internal/ethapi/errors.go +++ b/internal/ethapi/errors.go @@ -102,6 +102,7 @@ func (e *invalidTxError) ErrorCode() int { return e.Code } const ( errCodeNonceTooHigh = -38011 errCodeNonceTooLow = -38010 + errCodeBaseFeeTooLow = -38012 errCodeIntrinsicGas = -38013 errCodeInsufficientFunds = -38014 errCodeBlockGasLimitReached = -38015 @@ -134,7 +135,7 @@ func txValidationError(err error) *invalidTxError { case errors.Is(err, core.ErrTipAboveFeeCap): return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams} case errors.Is(err, core.ErrFeeCapTooLow): - return &invalidTxError{Message: err.Error(), Code: errCodeInvalidParams} + return &invalidTxError{Message: err.Error(), Code: errCodeBaseFeeTooLow} case errors.Is(err, core.ErrInsufficientFunds): return &invalidTxError{Message: err.Error(), Code: errCodeInsufficientFunds} case errors.Is(err, core.ErrIntrinsicGas): From 8209b9cb052cf526f363d8185c988926773621e4 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 26 May 2026 16:29:00 +0800 Subject: [PATCH 24/76] accounts/abi: forEachUnpack ABI error message arguments swapped (#35046) --- accounts/abi/unpack.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/abi/unpack.go b/accounts/abi/unpack.go index 905b5ce629..96e5d5d936 100644 --- a/accounts/abi/unpack.go +++ b/accounts/abi/unpack.go @@ -154,7 +154,7 @@ func forEachUnpack(t Type, output []byte, start, size int) (interface{}, error) return nil, fmt.Errorf("cannot marshal input to array, size is negative (%d)", size) } if start+32*size > len(output) { - return nil, fmt.Errorf("abi: cannot marshal into go array: offset %d would go over slice boundary (len=%d)", len(output), start+32*size) + return nil, fmt.Errorf("abi: cannot marshal into go array: offset %d would go over slice boundary (len=%d)", start+32*size, len(output)) } // this value will become our slice or our array, depending on the type From ca1a027fae93cc721ae76932a0296a41c62de181 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 26 May 2026 18:27:07 +0800 Subject: [PATCH 25/76] core: add slot number (#35036) --- core/chain_makers.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/chain_makers.go b/core/chain_makers.go index 2e856b5161..d93ce80dca 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -528,6 +528,13 @@ func (cm *chainMaker) makeHeader(parent *types.Block, state *state.StateDB, engi header.BlobGasUsed = new(uint64) header.ParentBeaconRoot = new(common.Hash) } + if cm.config.IsAmsterdam(header.Number, header.Time) { + var slot uint64 + if parentHeader.SlotNumber != nil { + slot = *parentHeader.SlotNumber + 1 + } + header.SlotNumber = &slot + } return header } From 9429725d2d6468ba1cfa50e4a9a81cdc67fab990 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 26 May 2026 20:18:47 +0800 Subject: [PATCH 26/76] p2p/discover: waitForNodes hangs on RespCount=0 from peer (#35043) The first NODES response sets total = min(int(response.RespCount), totalNodesResponseLimit), With RespCount=0, total=0 but receive become 1; receive == count is never satisfied. --- p2p/discover/v5_udp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go index c13032e1af..5f14784511 100644 --- a/p2p/discover/v5_udp.go +++ b/p2p/discover/v5_udp.go @@ -464,7 +464,7 @@ func (t *UDPv5) waitForNodes(c *callV5, distances []uint) ([]*enode.Node, error) if total == -1 { total = min(int(response.RespCount), totalNodesResponseLimit) } - if received++; received == total { + if received++; received >= total { return nodes, nil } case err := <-c.err: From c6b2a27f85a982228c8e8f3da945bc8c2eaa416d Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 26 May 2026 20:21:07 +0800 Subject: [PATCH 27/76] graphql: end == 0 and begin > 0 should be reject (#35032) --- graphql/graphql.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/graphql.go b/graphql/graphql.go index b25683cb0f..9b9fffb041 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -1433,7 +1433,7 @@ func (r *Resolver) Logs(ctx context.Context, args struct{ Filter FilterCriteria if args.Filter.ToBlock != nil { end = int64(*args.Filter.ToBlock) } - if begin > 0 && end > 0 && begin > end { + if begin >= 0 && end >= 0 && begin > end { return nil, errInvalidBlockRange } var addresses []common.Address From cae4c5f93c6fbde2963f35631ac90e96f695bc74 Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Tue, 26 May 2026 13:44:40 +0100 Subject: [PATCH 28/76] cmd/utils: respect --state.size-tracking=false (#35011) Passing `--state.size-tracking=false` currently cannot disable state size tracking when it was enabled by the config file because the CLI path only turns the config value on. --------- Co-authored-by: Jared Wasinger --- cmd/utils/flags.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c41cf4ee40..a48d9ccef2 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1936,8 +1936,8 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } - if ctx.Bool(StateSizeTrackingFlag.Name) { - cfg.EnableStateSizeTracking = true + if ctx.IsSet(StateSizeTrackingFlag.Name) { + cfg.EnableStateSizeTracking = ctx.Bool(StateSizeTrackingFlag.Name) } // Override any default configs for hard coded networks. switch { From 5933fa4bbf3d8a42f575cafcdc9fe9b80b038b0e Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Tue, 26 May 2026 14:44:56 +0200 Subject: [PATCH 29/76] .github: cancel CI run for stale PR commits (#34964) Each commit on a PR kicks off a CI run. Those CI jobs run to the finish regardless, even when new commits have been pushed which make them stale and useless. This change attempts to cancel any previously running job for the same PR. --- .github/workflows/go.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3e811072ff..f86990b5a7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,6 +7,11 @@ on: - master workflow_dispatch: +# Free runner capacity by cancelling superseded PR runs. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: lint: name: Lint From 622cef2d062c81c5e4b341cd9d5cdf3601a9f35f Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 26 May 2026 20:45:14 +0800 Subject: [PATCH 30/76] eth/protocols/snap: fix error message (#34976) --- eth/protocols/snap/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go index 5a5733bdb4..89c6c52f7f 100644 --- a/eth/protocols/snap/handlers.go +++ b/eth/protocols/snap/handlers.go @@ -308,11 +308,11 @@ func handleStorageRanges(backend Backend, msg Decoder, peer *Peer) error { // Decode. slotLists, err := res.Slots.Items() if err != nil { - return fmt.Errorf("AccountRange: invalid accounts list: %v", err) + return fmt.Errorf("StorageRanges: invalid storages list: %v", err) } proof, err := res.Proof.Items() if err != nil { - return fmt.Errorf("AccountRange: invalid proof: %v", err) + return fmt.Errorf("StorageRanges: invalid proof: %v", err) } // Ensure the ranges are monotonically increasing From d9028372560e6e9486c0930eec1a53d8b763aa27 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Wed, 27 May 2026 09:01:05 +0200 Subject: [PATCH 31/76] core/vm: global cache for jumpdest bitmaps (#34850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` ● Global JUMPDEST Cache - engine_newPayload benchmark ============================================================ Commit before: a06558042 (master) Commit after: faef2454f (core/vm: global cache for jumpdest bitmaps) Blocks: 1k mainnet (24950066 → 24951065) Runs: 3 each, clean ZFS clone per run Before (avg) With Falcon (avg) Δ Throughput 176.0 MGas/s 190.7 MGas/s +8.3% Mean NP 172.3ms 159.0ms -7.7% p50 162.8ms 150.7ms -7.4% p95 282.4ms 259.8ms -8.0% p99 391.0ms 371.6ms -5.0% Machine: Intel Ultra 7 255H, 62GB DDR5, NVMe (ZFS), governor=performance, turbo=off ``` --------- Co-authored-by: Felix Lange --- core/blockchain.go | 6 ++-- core/blockchain_test.go | 2 +- core/jumpdest.go | 76 ++++++++++++++++++++++++++++++++++++++++ core/state_prefetcher.go | 5 ++- core/state_processor.go | 5 ++- core/stateless.go | 2 +- core/types.go | 4 +-- eth/state_accessor.go | 2 +- 8 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 core/jumpdest.go diff --git a/core/blockchain.go b/core/blockchain.go index 23b4169372..8d4943532c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -330,6 +330,7 @@ type BlockChain struct { flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state triedb *triedb.Database // The database handler for maintaining trie nodes. codedb *state.CodeDB // The database handler for maintaining contract codes. + jumpDestCache vm.JumpDestCache // Shared JUMPDEST analysis cache for block processing txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain @@ -412,6 +413,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, db: db, triedb: triedb, codedb: state.NewCodeDB(db), + jumpDestCache: NewJumpDestCache(), triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit), @@ -2183,7 +2185,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // Disable tracing for prefetcher executions. vmCfg := bc.cfg.VmConfig vmCfg.Tracer = nil - bc.prefetcher.Prefetch(block, throwaway, vmCfg, &interrupt) + bc.prefetcher.Prefetch(block, throwaway, bc.jumpDestCache, vmCfg, &interrupt) blockPrefetchExecuteTimer.Update(time.Since(start)) if interrupt.Load() { @@ -2229,7 +2231,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, // Process block using the parent state as reference point pstart := time.Now() pctx, _, spanEnd := telemetry.StartSpan(ctx, "bc.processor.Process") - res, err := bc.processor.Process(pctx, block, statedb, bc.cfg.VmConfig) + res, err := bc.processor.Process(pctx, block, statedb, bc.jumpDestCache, bc.cfg.VmConfig) spanEnd(&err) if err != nil { bc.reportBadBlock(block, res, err) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 1a2ee45291..a8ddf5caa8 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -160,7 +160,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { if err != nil { return err } - res, err := blockchain.processor.Process(context.Background(), block, statedb, vm.Config{}) + res, err := blockchain.processor.Process(context.Background(), block, statedb, nil, vm.Config{}) if err != nil { blockchain.reportBadBlock(block, res, err) return err diff --git a/core/jumpdest.go b/core/jumpdest.go new file mode 100644 index 0000000000..d2c861b70f --- /dev/null +++ b/core/jumpdest.go @@ -0,0 +1,76 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + jumpDestHitMeter = metrics.NewRegisteredMeter("chain/cache/jumpdest/hit", nil) + jumpDestMissMeter = metrics.NewRegisteredMeter("chain/cache/jumpdest/miss", nil) +) + +const ( + // jumpDestBuckets is the number of independent LRU shards. Code hashes + // are dispatched by the low bits of the first byte to spread load across + // shards and reduce mutex contention from the parallel prefetcher. + jumpDestBuckets = 8 + + // jumpDestBucketSize is the per-shard byte budget. + jumpDestBucketSize = 8 * 1024 * 1024 +) + +// shardedJumpDestCache is a thread-safe, byte-bounded LRU of JUMPDEST analysis +// bitmaps, sharded into independent buckets to reduce lock contention. It is +// owned by BlockChain and shared across block processing and prefetching, +// keyed by the immutable contract code hash. +type shardedJumpDestCache struct { + buckets [jumpDestBuckets]struct { + dest *lru.SizeConstrainedCache[common.Hash, vm.BitVec] + } +} + +// NewJumpDestCache constructs the analysis cache. +func NewJumpDestCache() vm.JumpDestCache { + c := new(shardedJumpDestCache) + for i := range c.buckets { + c.buckets[i].dest = lru.NewSizeConstrainedCache[common.Hash, vm.BitVec](jumpDestBucketSize) + } + return c +} + +// Load retrieves the cached jumpdest analysis for the given code hash. +func (c *shardedJumpDestCache) Load(hash common.Hash) (vm.BitVec, bool) { + bucket := &c.buckets[hash[0]&(jumpDestBuckets-1)] + v, ok := bucket.dest.Get(hash) + if ok { + jumpDestHitMeter.Mark(1) + } else { + jumpDestMissMeter.Mark(1) + } + return v, ok +} + +// Store saves the jumpdest analysis for the given code hash. +func (c *shardedJumpDestCache) Store(hash common.Hash, b vm.BitVec) { + bucket := &c.buckets[hash[0]&(jumpDestBuckets-1)] + bucket.dest.Add(hash, b) +} diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index d99611ff2c..c635481730 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -49,7 +49,7 @@ func newStatePrefetcher(config *params.ChainConfig, chain *HeaderChain) *statePr // Prefetch processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb, but any changes are discarded. The // only goal is to warm the state caches. -func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, cfg vm.Config, interrupt *atomic.Bool) { +func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool) { var ( fails atomic.Int64 header = block.Header() @@ -94,6 +94,9 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c // Execute the message to preload the implicit touched states evm := vm.NewEVM(NewEVMBlockContext(header, p.chain, nil), stateCpy, p.config, cfg) defer evm.Release() + if jumpDestCache != nil { + evm.SetJumpDestCache(jumpDestCache) + } // Convert the transaction into an executable message and pre-cache its sender msg, err := TransactionToMessage(tx, signer, header.BaseFee) diff --git a/core/state_processor.go b/core/state_processor.go index 5690a152e7..5092379056 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -63,7 +63,7 @@ func (p *StateProcessor) chainConfig() *params.ChainConfig { // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) { +func (p *StateProcessor) Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config) (*ProcessResult, error) { var ( config = p.chainConfig() receipts = make(types.Receipts, 0, len(block.Transactions())) @@ -88,6 +88,9 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated blockAccessList = bal.NewConstructionBlockAccessList() ) defer evm.Release() + if jumpDestCache != nil { + evm.SetJumpDestCache(jumpDestCache) + } // Run the pre-execution system calls blockAccessList.Merge(PreExecution(ctx, block.BeaconRoot(), block.ParentHash(), config, evm, block.Number(), block.Time())) diff --git a/core/stateless.go b/core/stateless.go index 86d4dc304b..805ef7ffbe 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -68,7 +68,7 @@ func ExecuteStateless(ctx context.Context, config *params.ChainConfig, vmconfig validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block // Run the stateless blocks processing and self-validate certain fields - res, err := processor.Process(ctx, block, db, vmconfig) + res, err := processor.Process(ctx, block, db, nil, vmconfig) if err != nil { return common.Hash{}, common.Hash{}, err } diff --git a/core/types.go b/core/types.go index edbfc43db3..f6f15101e0 100644 --- a/core/types.go +++ b/core/types.go @@ -42,7 +42,7 @@ type Prefetcher interface { // Prefetch processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb, but any changes are discarded. The // only goal is to pre-cache transaction signatures and state trie nodes. - Prefetch(block *types.Block, statedb *state.StateDB, cfg vm.Config, interrupt *atomic.Bool) + Prefetch(block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config, interrupt *atomic.Bool) } // Processor is an interface for processing blocks using a given initial state. @@ -50,7 +50,7 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(ctx context.Context, block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) + Process(ctx context.Context, block *types.Block, statedb *state.StateDB, jumpDestCache vm.JumpDestCache, cfg vm.Config) (*ProcessResult, error) } // ProcessResult contains the values computed by Process. diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 284ddf4305..3c3539dbdb 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -151,7 +151,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, base *st if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, nil, fmt.Errorf("block #%d not found", next) } - _, err := eth.blockchain.Processor().Process(ctx, current, statedb, vm.Config{}) + _, err := eth.blockchain.Processor().Process(ctx, current, statedb, nil, vm.Config{}) if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } From ace9c512332f8cbf899a166781b3a70dbb6206a2 Mon Sep 17 00:00:00 2001 From: vickkkkkyy Date: Wed, 27 May 2026 15:03:32 +0800 Subject: [PATCH 32/76] cmd/utils: fix archive mode detection for TransactionHistory override (#33880) Archive nodes store the full history of transactions in the index. This PR fixes a bug for users who provided the NoPruning field in a YAML config file. Now geth correctly stores full transaction history if archive is configured via YAML. --------- Co-authored-by: Sina Mahmoodi --- cmd/utils/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index a48d9ccef2..77a3bffc8b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1841,7 +1841,7 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { log.Warn("The flag --txlookuplimit is deprecated and will be removed, please use --history.transactions") cfg.TransactionHistory = ctx.Uint64(TxLookupLimitFlag.Name) } - if ctx.String(GCModeFlag.Name) == "archive" { + if cfg.NoPruning { if cfg.TransactionHistory != 0 { cfg.TransactionHistory = 0 log.Warn("Disabled transaction unindexing for archive node") From ac1fdc5f8f1459abba5a1032b5156d02e444b275 Mon Sep 17 00:00:00 2001 From: locoholy <68823405+locoholy@users.noreply.github.com> Date: Wed, 27 May 2026 13:15:09 +0500 Subject: [PATCH 33/76] internal/ethapi: add eth_capabilities RPC method (#33886) There is currently no way for JSON-RPC clients to discover which historical data a node can serve without probing with trial-and-error calls and interpreting opaque error messages (`pruned history unavailable`). This makes it hard to build robust tooling on top of nodes that prune their history, for example nodes started with `--history.chain postmerge` or with reduced `TransactionHistory`, `LogHistory`, or `StateHistory` windows. This PR implements `eth_capabilities` as defined in ethereum/execution-apis#755. The method takes no parameters and returns the current head plus six per-resource capability records: - `state` - `tx` - `logs` - `receipts` - `blocks` - `stateproofs` Closes #33828 --- eth/api_backend.go | 14 + internal/ethapi/api_test.go | 3 + internal/ethapi/backend.go | 1 + internal/ethapi/capabilities.go | 219 ++++++++++++ internal/ethapi/capabilities_test.go | 420 +++++++++++++++++++++++ internal/ethapi/transaction_args_test.go | 3 +- internal/web3ext/web3ext.go | 5 + 7 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 internal/ethapi/capabilities.go create mode 100644 internal/ethapi/capabilities_test.go diff --git a/eth/api_backend.go b/eth/api_backend.go index 5e3558d8eb..7d3b5d483e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -41,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) @@ -279,6 +280,19 @@ func (b *EthAPIBackend) HistoryPruningCutoff() uint64 { return bn } +func (b *EthAPIBackend) HistoryRetention() ethapi.HistoryRetention { + cfg := b.eth.config + return ethapi.HistoryRetention{ + TxIndexHistory: cfg.TransactionHistory, + LogIndexHistory: cfg.LogHistory, + LogIndexDisabled: cfg.LogNoHistory, + StateHistory: cfg.StateHistory, + TrienodeHistory: cfg.TrienodeHistory, + StateArchive: cfg.NoPruning, + StateScheme: b.eth.blockchain.TrieDB().Scheme(), + } +} + func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { return b.eth.blockchain.GetReceiptsByHash(hash), nil } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 561ce2c2d2..3b72742e95 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -708,6 +708,9 @@ func (b testBackend) HistoryPruningCutoff() uint64 { bn, _ := b.chain.HistoryPruningCutoff() return bn } +func (b testBackend) HistoryRetention() HistoryRetention { + return HistoryRetention{StateScheme: b.chain.TrieDB().Scheme()} +} func TestEstimateGas(t *testing.T) { t.Parallel() diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index f23be85782..25f21c65da 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -91,6 +91,7 @@ type Backend interface { ChainConfig() *params.ChainConfig Engine() consensus.Engine HistoryPruningCutoff() uint64 + HistoryRetention() HistoryRetention // This is copied from filters.Backend // eth/filters needs to be initialized from this backend type, so methods needed by diff --git a/internal/ethapi/capabilities.go b/internal/ethapi/capabilities.go new file mode 100644 index 0000000000..a7ff9eee76 --- /dev/null +++ b/internal/ethapi/capabilities.go @@ -0,0 +1,219 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/rawdb" + corestate "github.com/ethereum/go-ethereum/core/state" +) + +// HistoryRetention reports a node's configured history retention windows. +// It is consumed by the eth_capabilities RPC method to derive the response +// described in https://github.com/ethereum/execution-apis/pull/755. +type HistoryRetention struct { + // TxIndexHistory is the number of recent blocks for which the + // transaction lookup index is maintained. Zero means the index covers + // the entire available chain. + TxIndexHistory uint64 + + // LogIndexHistory is the number of recent blocks for which the log + // search index is maintained. Zero means the index covers the entire + // available chain. + LogIndexHistory uint64 + + // LogIndexDisabled reports whether the log search index has been + // turned off entirely. + LogIndexDisabled bool + + // StateHistory is the number of recent blocks for which historical + // state is retained in path-based archive mode. Zero means the entire + // available state history is kept. + StateHistory uint64 + + // TrienodeHistory is the number of recent blocks for which trie node + // history is retained in path-based archive mode. Zero means the entire + // available trienode history is kept; negative means no trienode history + // is stored. + TrienodeHistory int64 + + // StateArchive reports whether state pruning is disabled + // (--gcmode=archive). + StateArchive bool + + // StateScheme is the state storage scheme in use, either "hash" or + // "path". + StateScheme string +} + +// Capabilities reports which historical data the node can serve. It is +// returned by the eth_capabilities RPC method as defined in +// https://github.com/ethereum/execution-apis/pull/755. +type Capabilities struct { + Head CapabilityHead `json:"head"` + State CapabilityResource `json:"state"` + Tx CapabilityResource `json:"tx"` + Logs CapabilityResource `json:"logs"` + Receipts CapabilityResource `json:"receipts"` + Blocks CapabilityResource `json:"blocks"` + StateProofs CapabilityResource `json:"stateproofs"` +} + +// CapabilityHead is the current canonical head as reported by the node. +type CapabilityHead struct { + Number hexutil.Uint64 `json:"number"` + Hash common.Hash `json:"hash"` +} + +// CapabilityResource describes the availability of a single data resource. +type CapabilityResource struct { + Disabled bool `json:"disabled"` + OldestBlock *hexutil.Uint64 `json:"oldestBlock,omitempty"` + DeleteStrategy *DeleteStrategy `json:"deleteStrategy,omitempty"` +} + +// DeleteStrategy describes how data of a resource is removed over time. +// +// The spec currently defines one strategy: "window", meaning data is retained +// for a sliding window of the most recent RetentionBlocks blocks. Resources +// without sliding deletion omit deleteStrategy. +type DeleteStrategy struct { + Type string `json:"type"` + RetentionBlocks *hexutil.Uint64 `json:"retentionBlocks,omitempty"` +} + +// strategyWindow returns a DeleteStrategy with type "window" and the given +// retention block count. +func strategyWindow(retention uint64) *DeleteStrategy { + blocks := hexutil.Uint64(retention) + return &DeleteStrategy{Type: "window", RetentionBlocks: &blocks} +} + +func capabilityOldestBlock(number uint64) *hexutil.Uint64 { + oldest := hexutil.Uint64(number) + return &oldest +} + +// Capabilities implements the eth_capabilities RPC method as defined in +// https://github.com/ethereum/execution-apis/pull/755. It returns a +// description of the historical data this node can serve, allowing RPC +// routers to determine which queries can be answered without hitting +// "history pruned" errors. +func (api *BlockChainAPI) Capabilities() *Capabilities { + head := api.b.CurrentHeader() + return buildCapabilities( + head.Number.Uint64(), + head.Hash(), + api.b.HistoryPruningCutoff(), + api.b.HistoryRetention(), + ) +} + +// buildCapabilities computes the eth_capabilities response from the head +// block, the absolute history pruning cutoff, and the configured retention +// windows. It is split out from the RPC method so the mapping rules can be +// unit tested without a backend. +func buildCapabilities(headNum uint64, headHash common.Hash, cutoff uint64, ret HistoryRetention) *Capabilities { + // windowOldest returns the oldest block reachable through a sliding + // window of `window` blocks, never going below the supplied floor. A + // window of zero means "no sliding deletion" and reports the floor + // itself. + windowOldest := func(window uint64, floor uint64) uint64 { + if window == 0 || headNum+1 <= window { + return floor + } + oldest := headNum + 1 - window + if oldest < floor { + return floor + } + return oldest + } + + // resource builds a CapabilityResource for a window-style resource. + // Disabled resources intentionally omit oldestBlock and deleteStrategy, + // because those fields would otherwise look like usable history ranges. + resource := func(disabled bool, window uint64, floor uint64) CapabilityResource { + if disabled { + return CapabilityResource{Disabled: true} + } + res := CapabilityResource{ + OldestBlock: capabilityOldestBlock(windowOldest(window, floor)), + } + if window != 0 { + res.DeleteStrategy = strategyWindow(window) + } + return res + } + + // Bodies and receipts share the same retention model in + // geth: they are either kept in full ("all") or pruned to a fixed + // boundary ("postmerge"). In neither case is there a sliding deletion + // window, so deleteStrategy is omitted and oldestBlock equals the history + // pruning cutoff. + blocks := CapabilityResource{ + OldestBlock: capabilityOldestBlock(cutoff), + } + receipts := blocks + + tx := resource(false, ret.TxIndexHistory, cutoff) + logs := resource(ret.LogIndexDisabled, ret.LogIndexHistory, cutoff) + + // State availability is determined primarily by gcmode: + // + // - full mode: only the in-memory state window is reachable, + // regardless of the storage scheme. + // - archive+hash: full state history is reachable. + // - archive+path: honors the configured StateHistory window. + var state CapabilityResource + switch { + case !ret.StateArchive: + state = resource(false, corestate.TriesInMemory, 0) + case ret.StateScheme == rawdb.HashScheme: + state = resource(false, 0, 0) + default: + state = resource(false, ret.StateHistory, 0) + } + + // eth_getProof availability tracks state availability in hash mode and + // in path-based full mode. Path-based archive nodes store trie node + // history separately from state history. + stateproofs := state + if ret.StateArchive && ret.StateScheme == rawdb.PathScheme { + switch { + case ret.TrienodeHistory < 0: + stateproofs = resource(false, corestate.TriesInMemory, 0) + case ret.TrienodeHistory == 0: + stateproofs = resource(false, 0, 0) + default: + stateproofs = resource(false, uint64(ret.TrienodeHistory), 0) + } + } + + return &Capabilities{ + Head: CapabilityHead{ + Number: hexutil.Uint64(headNum), + Hash: headHash, + }, + State: state, + Tx: tx, + Logs: logs, + Receipts: receipts, + Blocks: blocks, + StateProofs: stateproofs, + } +} diff --git a/internal/ethapi/capabilities_test.go b/internal/ethapi/capabilities_test.go new file mode 100644 index 0000000000..04816774e9 --- /dev/null +++ b/internal/ethapi/capabilities_test.go @@ -0,0 +1,420 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package ethapi + +import ( + "encoding/json" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/rawdb" + corestate "github.com/ethereum/go-ethereum/core/state" +) + +func TestBuildCapabilities(t *testing.T) { + const ( + archiveHead uint64 = 3_000_000 + postmerge uint64 = 15_537_393 + ) + headHash := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + + tests := []struct { + name string + headNum uint64 + cutoff uint64 + ret HistoryRetention + expected map[string]CapabilityResource // by JSON field name + }{ + { + name: "archive node, path scheme, all defaults", + headNum: archiveHead, + cutoff: 0, + ret: HistoryRetention{ + StateArchive: true, + StateScheme: rawdb.PathScheme, + }, + expected: map[string]CapabilityResource{ + "blocks": {OldestBlock: hexUintPtr(0)}, + "receipts": {OldestBlock: hexUintPtr(0)}, + "tx": {OldestBlock: hexUintPtr(0)}, + "logs": {OldestBlock: hexUintPtr(0)}, + "state": {OldestBlock: hexUintPtr(0)}, + "stateproofs": {OldestBlock: hexUintPtr(0)}, + }, + }, + { + name: "post-merge pruned chain", + headNum: archiveHead, + cutoff: postmerge, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + }, + expected: map[string]CapabilityResource{ + // blocks/receipts honor the absolute cutoff with no + // sliding window. + "blocks": {OldestBlock: hexUintPtr(postmerge)}, + "receipts": {OldestBlock: hexUintPtr(postmerge)}, + }, + }, + { + name: "default tx and log indices, head above window", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + TxIndexHistory: 2_350_000, + LogIndexHistory: 2_350_000, + }, + expected: map[string]CapabilityResource{ + "tx": { + OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1), + DeleteStrategy: windowStrategy(2_350_000), + }, + "logs": { + OldestBlock: hexUintPtr(5_000_000 - 2_350_000 + 1), + DeleteStrategy: windowStrategy(2_350_000), + }, + }, + }, + { + name: "head below tx window: clamp to cutoff, no underflow", + headNum: 100, + cutoff: 0, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + TxIndexHistory: 2_350_000, + }, + expected: map[string]CapabilityResource{ + "tx": { + OldestBlock: hexUintPtr(0), + DeleteStrategy: windowStrategy(2_350_000), + }, + }, + }, + { + name: "tx window oldest clamped to history pruning cutoff", + headNum: 5_000_000, + cutoff: 4_000_000, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + TxIndexHistory: 2_350_000, // would otherwise reach back to 2.65M + }, + expected: map[string]CapabilityResource{ + "tx": { + OldestBlock: hexUintPtr(4_000_000), + DeleteStrategy: windowStrategy(2_350_000), + }, + }, + }, + { + name: "state windows are not clamped to history pruning cutoff", + headNum: 5_000_000, + cutoff: 4_950_000, + ret: HistoryRetention{ + StateArchive: true, + StateScheme: rawdb.PathScheme, + StateHistory: 90_000, + TrienodeHistory: 100_000, + }, + expected: map[string]CapabilityResource{ + "state": { + OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - 100_000 + 1), + DeleteStrategy: windowStrategy(100_000), + }, + }, + }, + { + name: "log index disabled", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + LogIndexHistory: 2_350_000, + LogIndexDisabled: true, + }, + expected: map[string]CapabilityResource{ + "logs": { + Disabled: true, + }, + }, + }, + { + name: "path archive with separate state and trienode history windows", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateArchive: true, + StateScheme: rawdb.PathScheme, + StateHistory: 90_000, + TrienodeHistory: 50_000, + }, + expected: map[string]CapabilityResource{ + "state": { + OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - 50_000 + 1), + DeleteStrategy: windowStrategy(50_000), + }, + }, + }, + { + name: "path archive with trienode history disabled retains in-memory proofs", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateArchive: true, + StateScheme: rawdb.PathScheme, + StateHistory: 90_000, + TrienodeHistory: -1, + }, + expected: map[string]CapabilityResource{ + "state": { + OldestBlock: hexUintPtr(5_000_000 - 90_000 + 1), + DeleteStrategy: windowStrategy(90_000), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + }, + }, + { + name: "hash scheme archive ignores StateHistory", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateArchive: true, + StateScheme: rawdb.HashScheme, + StateHistory: 90_000, + }, + expected: map[string]CapabilityResource{ + "state": {OldestBlock: hexUintPtr(0)}, + "stateproofs": {OldestBlock: hexUintPtr(0)}, + }, + }, + { + name: "full mode hash scheme retains in-memory state window", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateScheme: rawdb.HashScheme, + StateHistory: 90_000, // ignored under hash scheme + }, + expected: map[string]CapabilityResource{ + "state": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + }, + }, + { + name: "full mode path scheme ignores StateHistory", + headNum: 5_000_000, + cutoff: 0, + ret: HistoryRetention{ + StateScheme: rawdb.PathScheme, + StateHistory: 90_000, + }, + expected: map[string]CapabilityResource{ + "state": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + "stateproofs": { + OldestBlock: hexUintPtr(5_000_000 - corestate.TriesInMemory + 1), + DeleteStrategy: windowStrategy(corestate.TriesInMemory), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caps := buildCapabilities(tt.headNum, headHash, tt.cutoff, tt.ret) + + // Head is always present. + if uint64(caps.Head.Number) != tt.headNum { + t.Errorf("head.number = %d, want %d", uint64(caps.Head.Number), tt.headNum) + } + if caps.Head.Hash != headHash { + t.Errorf("head.hash = %x, want %x", caps.Head.Hash, headHash) + } + + actual := map[string]CapabilityResource{ + "state": caps.State, + "tx": caps.Tx, + "logs": caps.Logs, + "receipts": caps.Receipts, + "blocks": caps.Blocks, + "stateproofs": caps.StateProofs, + } + for name, want := range tt.expected { + got := actual[name] + if got.Disabled != want.Disabled { + t.Errorf("%s.disabled = %v, want %v", name, got.Disabled, want.Disabled) + } + switch { + case want.OldestBlock == nil && got.OldestBlock != nil: + t.Errorf("%s.oldestBlock = %d, want absent", name, uint64(*got.OldestBlock)) + case want.OldestBlock != nil && got.OldestBlock == nil: + t.Errorf("%s.oldestBlock absent, want %d", name, uint64(*want.OldestBlock)) + case want.OldestBlock != nil && got.OldestBlock != nil: + if *got.OldestBlock != *want.OldestBlock { + t.Errorf("%s.oldestBlock = %d, want %d", + name, uint64(*got.OldestBlock), uint64(*want.OldestBlock)) + } + } + switch { + case want.DeleteStrategy == nil && got.DeleteStrategy != nil: + t.Errorf("%s.deleteStrategy = %#v, want absent", name, got.DeleteStrategy) + case want.DeleteStrategy != nil && got.DeleteStrategy == nil: + t.Errorf("%s.deleteStrategy absent, want %#v", name, want.DeleteStrategy) + case want.DeleteStrategy != nil && got.DeleteStrategy != nil: + if got.DeleteStrategy.Type != want.DeleteStrategy.Type { + t.Errorf("%s.deleteStrategy.type = %q, want %q", name, got.DeleteStrategy.Type, want.DeleteStrategy.Type) + } + switch { + case want.DeleteStrategy.RetentionBlocks == nil && got.DeleteStrategy.RetentionBlocks != nil: + t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want absent", + name, *got.DeleteStrategy.RetentionBlocks) + case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks == nil: + t.Errorf("%s.deleteStrategy.retentionBlocks absent, want %d", + name, *want.DeleteStrategy.RetentionBlocks) + case want.DeleteStrategy.RetentionBlocks != nil && got.DeleteStrategy.RetentionBlocks != nil: + if *got.DeleteStrategy.RetentionBlocks != *want.DeleteStrategy.RetentionBlocks { + t.Errorf("%s.deleteStrategy.retentionBlocks = %d, want %d", + name, *got.DeleteStrategy.RetentionBlocks, *want.DeleteStrategy.RetentionBlocks) + } + } + } + } + }) + } +} + +// TestCapabilitiesJSONShape verifies that the marshalled JSON conforms to +// the schema defined in https://github.com/ethereum/execution-apis/pull/755: +// head fields are named number/hash, resources without a sliding window omit +// deleteStrategy, disabled resources omit range fields, and retentionBlocks is +// a hex quantity. +func TestCapabilitiesJSONShape(t *testing.T) { + caps := buildCapabilities( + 5_000_000, + common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), + 0, + HistoryRetention{ + StateScheme: rawdb.PathScheme, + TxIndexHistory: 2_350_000, + LogIndexHistory: 2_350_000, + LogIndexDisabled: true, + StateHistory: 90_000, + }, + ) + + raw, err := json.Marshal(caps) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + // Round-trip through a generic map so we can assert on key presence. + var generic map[string]any + if err := json.Unmarshal(raw, &generic); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + // Top-level keys must match the spec. + required := []string{"head", "state", "tx", "logs", "receipts", "blocks", "stateproofs"} + for _, k := range required { + if _, ok := generic[k]; !ok { + t.Errorf("missing top-level key %q", k) + } + } + + // head.number must be a hex string ("0x..."), hash must be a 0x hash. + head := generic["head"].(map[string]any) + if number, ok := head["number"].(string); !ok || len(number) < 3 || number[:2] != "0x" { + t.Errorf("head.number not hex string: %v", head["number"]) + } + if hash, ok := head["hash"].(string); !ok || len(hash) != 66 { + t.Errorf("head.hash not 32-byte hex string: %v", head["hash"]) + } + if _, present := head["blockNumber"]; present { + t.Errorf("head must not include blockNumber") + } + if _, present := head["blockHash"]; present { + t.Errorf("head must not include blockHash") + } + + // blocks have a fixed oldest block but no deletion strategy. + blocks := generic["blocks"].(map[string]any) + if ob, ok := blocks["oldestBlock"].(string); !ok || len(ob) < 3 || ob[:2] != "0x" { + t.Errorf("blocks.oldestBlock not hex string: %v", blocks["oldestBlock"]) + } + if _, present := blocks["deleteStrategy"]; present { + t.Errorf("blocks must not include deleteStrategy without sliding deletion") + } + + // Disabled resources must not advertise an availability range. + logs := generic["logs"].(map[string]any) + if logs["disabled"] != true { + t.Errorf("logs.disabled = %v, want true", logs["disabled"]) + } + if _, present := logs["oldestBlock"]; present { + t.Errorf("disabled logs must not include oldestBlock") + } + if _, present := logs["deleteStrategy"]; present { + t.Errorf("disabled logs must not include deleteStrategy") + } + + // tx.deleteStrategy is "window" → must contain retentionBlocks as a + // hex quantity, not a decimal JSON number. + tx := generic["tx"].(map[string]any) + tds := tx["deleteStrategy"].(map[string]any) + if tds["type"] != "window" { + t.Errorf("tx.deleteStrategy.type = %v, want window", tds["type"]) + } + if rb, ok := tds["retentionBlocks"].(string); !ok || rb != "0x23dbb0" { + t.Errorf("tx.deleteStrategy.retentionBlocks = %v, want 0x23dbb0", tds["retentionBlocks"]) + } + + // tx.oldestBlock must be a hex string. + if ob, ok := tx["oldestBlock"].(string); !ok || len(ob) < 3 || ob[:2] != "0x" { + t.Errorf("tx.oldestBlock not hex string: %v", tx["oldestBlock"]) + } +} + +// hexUintPtr and windowStrategy keep the test tables compact. +func hexUintPtr(n uint64) *hexutil.Uint64 { + v := hexutil.Uint64(n) + return &v +} + +func windowStrategy(n uint64) *DeleteStrategy { + blocks := hexutil.Uint64(n) + return &DeleteStrategy{Type: "window", RetentionBlocks: &blocks} +} diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index ccb46a810d..f3fc16dcbb 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -411,4 +411,5 @@ func (b *backendMock) Engine() consensus.Engine { return nil } func (b *backendMock) CurrentView() *filtermaps.ChainView { return nil } func (b *backendMock) NewMatcherBackend() filtermaps.MatcherBackend { return nil } -func (b *backendMock) HistoryPruningCutoff() uint64 { return 0 } +func (b *backendMock) HistoryPruningCutoff() uint64 { return 0 } +func (b *backendMock) HistoryRetention() HistoryRetention { return HistoryRetention{} } diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 1d1b5fbcd1..6a5f3c9a8a 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -612,6 +612,11 @@ web3._extend({ name: 'config', call: 'eth_config', params: 0, + }), + new web3._extend.Method({ + name: 'capabilities', + call: 'eth_capabilities', + params: 0, }) ], properties: [ From 90cd7d1937f279514ec0c4250c116688fef011f2 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 27 May 2026 18:53:03 +0800 Subject: [PATCH 34/76] eth: should return basefee for the next block as doc says (#35023) --- eth/api_backend.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eth/api_backend.go b/eth/api_backend.go index 7d3b5d483e..d527d4756e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -446,8 +446,10 @@ func (b *EthAPIBackend) FeeHistory(ctx context.Context, blockCount uint64, lastB } func (b *EthAPIBackend) BaseFee(ctx context.Context) *big.Int { - if b.ChainConfig().IsLondon(b.CurrentHeader().Number) { - return eip1559.CalcBaseFee(b.ChainConfig(), b.CurrentHeader()) + header := b.CurrentHeader() + next := new(big.Int).Add(header.Number, common.Big1) + if b.ChainConfig().IsLondon(next) { + return eip1559.CalcBaseFee(b.ChainConfig(), header) } return nil } From c782197d48f65985a84cb94d6660e23514adcfa5 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Wed, 27 May 2026 12:53:34 +0200 Subject: [PATCH 35/76] graphql: limit request body size (#35034) Fixes #35033 ## Problem The GraphQL HTTP handler decoded request bodies directly before executing the query. Unlike the JSON-RPC HTTP path, `/graphql` did not have an explicit request body limit before JSON decoding. A single `Decode` also stops after the first JSON value, so the handler now requires EOF after the GraphQL request object to ensure oversized trailing request data is not ignored. ## Changes - Limit GraphQL request bodies to 5 MiB, matching the existing JSON-RPC default body limit. - Return `413 Request Entity Too Large` when the limit is exceeded. - Require EOF after the request JSON object. - Add regression coverage for oversized query bodies and oversized trailing request data. - Fix an existing GraphQL test fixture that had an unintended trailing quote after the JSON object. ## Validation - `gofmt -w graphql/service.go graphql/graphql_test.go` - `go run golang.org/x/tools/cmd/goimports@latest -w graphql/service.go graphql/graphql_test.go` - `go test ./graphql -run TestGraphQLHTTPBodyLimit -count=1` - `go test ./graphql -count=1` --- graphql/graphql_test.go | 32 +++++++++++++++++++++++++++++++- graphql/service.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index ca864d5fb2..d0f80ee282 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -23,6 +23,7 @@ import ( "io" "math/big" "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -132,7 +133,7 @@ func TestGraphQLBlockSerialization(t *testing.T) { code: 400, }, { - body: `{"query": "{bleh{number}}","variables": null}"`, + body: `{"query": "{bleh{number}}","variables": null}`, want: `{"errors":[{"message":"Cannot query field \"bleh\" on type \"Query\".","locations":[{"line":1,"column":2}]}]}`, code: 400, }, @@ -175,6 +176,35 @@ func TestGraphQLBlockSerialization(t *testing.T) { } } +func TestGraphQLHTTPBodyLimit(t *testing.T) { + tests := []struct { + name string + body string + }{ + { + name: "should reject oversized request body if query field exceeds limit", + body: `{"query":"` + strings.Repeat("a", maxRequestBodySize) + `"}`, + }, + { + name: "should reject oversized request body if trailing data exceeds limit", + body: `{"query":"{block{number}}"}` + strings.Repeat(" ", maxRequestBodySize), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(test.body)) + w := httptest.NewRecorder() + + handler{}.ServeHTTP(w, req) + + if w.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("got status %d, want %d", w.Code, http.StatusRequestEntityTooLarge) + } + }) + } +} + func TestGraphQLBlockSerializationEIP2718(t *testing.T) { // Account for signing txes var ( diff --git a/graphql/service.go b/graphql/service.go index 9381a51da6..4d530586a3 100644 --- a/graphql/service.go +++ b/graphql/service.go @@ -19,6 +19,8 @@ package graphql import ( "context" "encoding/json" + "errors" + "io" "net/http" "strconv" "sync" @@ -35,18 +37,31 @@ import ( // maxQueryDepth limits the maximum field nesting depth allowed in GraphQL queries. const maxQueryDepth = 20 +// maxRequestBodySize limits the size of incoming GraphQL request bodies. +const maxRequestBodySize = 5 * 1024 * 1024 + type handler struct { Schema *graphql.Schema } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) + var params struct { Query string `json:"query"` OperationName string `json:"operationName"` Variables map[string]interface{} `json:"variables"` } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + dec := json.NewDecoder(r.Body) + if err := dec.Decode(¶ms); err != nil { + writeRequestError(w, err) + return + } + if err := dec.Decode(&struct{}{}); err != io.EOF { + if err == nil { + err = errors.New("unexpected content after JSON value") + } + writeRequestError(w, err) return } @@ -108,6 +123,15 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) } +func writeRequestError(w http.ResponseWriter, err error) { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, err.Error(), http.StatusRequestEntityTooLarge) + return + } + http.Error(w, err.Error(), http.StatusBadRequest) +} + // New constructs a new GraphQL service instance. func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) error { _, err := newHandler(stack, backend, filterSystem, cors, vhosts) From 1a2333650a73b81ffaa7114f2c3c1b21617e2e1d Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Wed, 27 May 2026 04:57:13 -0600 Subject: [PATCH 36/76] eth/catalyst: import new payload if at genesis, regardless of sync status (#32673) fixes #32672 This is kind of a band aid solution since it fixes the issue by bypassing the snap sync expectations of an empty db and attempting to import the new payload if we're at block 1. The next FCU will set the status to synced. Will continue looking to better understand how the above issue arises and find a more thorough solution. --------- Co-authored-by: Marius van der Wijden --- eth/catalyst/api.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 5dc0deb324..71e92e315d 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -911,7 +911,11 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl // into the database directly will conflict with the assumptions of snap sync // that it has an empty db that it can fill itself. if api.eth.Downloader().ConfigSyncMode() == ethconfig.SnapSync { - return api.delayPayloadImport(block), nil + // If the client is started at genesis of a test network with snap sync + // enabled, just try to import the block since there is nothing to sync. + if block.NumberU64() != 1 { + return api.delayPayloadImport(block), nil + } } if !api.eth.BlockChain().HasBlockAndState(block.ParentHash(), block.NumberU64()-1) { api.remoteBlocks.put(block.Hash(), block.Header()) From 45698e9cb9d917a5677bf5b0f0f7876cae97f1a0 Mon Sep 17 00:00:00 2001 From: ozpool <151670776+ozpool@users.noreply.github.com> Date: Wed, 27 May 2026 17:08:18 +0530 Subject: [PATCH 37/76] p2p/nat: bump pion/stun to v3 to pull in fixed pion/dtls (#34980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary Closes #34621. `github.com/pion/dtls/v2` is affected by [CVE-2026-26014](https://nvd.nist.gov/vuln/detail/CVE-2026-26014); the fix lives in `github.com/pion/dtls/v3`. In this tree, dtls/v2 is pulled in indirectly via `github.com/pion/stun/v2 v2.0.0` (declared at `go.mod:53`), which is the only direct consumer — `p2p/nat/stun.go` is the sole call site. `github.com/pion/stun/v3` already uses dtls/v3, so bumping `stun` upgrades the vulnerable dependency without touching `pion/dtls` directly. ### API check The v3 surface used by `p2p/nat/stun.go` is byte-identical in shape to v2: | Symbol | v2 | v3 | |---|---|---| | `Dial` | `func Dial(network, address string) (*Client, error)` | same | | `Build` | `func Build(setters ...Setter) (*Message, error)` | same | | `TransactionID` | `var TransactionID Setter` | same | | `BindingRequest` | `var BindingRequest = NewType(MethodBinding, ClassRequest)` | same | | `Event` | `type Event struct` | same | | `XORMappedAddress` | `type XORMappedAddress struct { … GetFrom(*Message) error }` | same | | `DefaultPort` | `const DefaultPort = 3478` | same | So the code change is just the import rename plus an alias rename to keep the local label honest (`stunV2` → `stunV3`). ### Change `go.mod` / `go.sum`: - Replace direct `github.com/pion/stun/v2 v2.0.0` with `github.com/pion/stun/v3 v3.0.1`. - `go mod tidy` drops every `pion/dtls/v2` and `pion/stun/v2` entry from `go.sum` and pulls `pion/dtls/v3 v3.0.7`, `pion/stun/v3 v3.0.1`, `pion/transport/v3 v3.0.8` as the new indirect set. `p2p/nat/stun.go`: - Update the import path and rename the alias from `stunV2` to `stunV3`. ### Verification - `go build ./p2p/nat/` clean. - `go test ./p2p/nat/ -count=1` passes (26s). - `grep 'pion/dtls/v2\|pion/stun/v2' go.sum` returns zero matches. ### Notes - `pion/dtls` is not imported directly anywhere in the tree, so no other code needs touching. - `pion/transport/v3` was already in the dependency graph (the `stun/v3` upgrade just bumps the patch from v3.0.1 → v3.0.8); the v2 transport drops out cleanly. --- go.mod | 10 +++++----- go.sum | 44 ++++++++++---------------------------------- p2p/nat/stun.go | 12 ++++++------ 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/go.mod b/go.mod index 56869d255d..2a96f2c761 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 - github.com/pion/stun/v2 v2.0.0 + github.com/pion/stun/v3 v3.0.1 github.com/protolambda/bls12-381-util v0.1.0 github.com/protolambda/zrnt v0.34.1 github.com/protolambda/ztyp v0.2.2 @@ -87,6 +87,8 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/wlynxg/anet v0.0.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect @@ -147,10 +149,8 @@ require ( github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/naoina/go-stringutil v0.1.0 // indirect github.com/opentracing/opentracing-go v1.1.0 // indirect - github.com/pion/dtls/v2 v2.2.7 // indirect - github.com/pion/logging v0.2.2 // indirect - github.com/pion/transport/v2 v2.2.1 // indirect - github.com/pion/transport/v3 v3.0.1 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/transport/v3 v3.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.15.0 // indirect diff --git a/go.sum b/go.sum index 6335fb5698..a95ae43224 100644 --- a/go.sum +++ b/go.sum @@ -297,16 +297,14 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= -github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= +github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= +github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= +github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -348,18 +346,12 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= @@ -375,6 +367,8 @@ github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -410,8 +404,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= @@ -419,7 +411,6 @@ golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N0 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -433,10 +424,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -446,7 +433,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -476,9 +462,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= @@ -486,10 +470,6 @@ golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -497,9 +477,6 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -512,7 +489,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/p2p/nat/stun.go b/p2p/nat/stun.go index 36d54398b5..30d2bc80d0 100644 --- a/p2p/nat/stun.go +++ b/p2p/nat/stun.go @@ -26,7 +26,7 @@ import ( "time" "github.com/ethereum/go-ethereum/log" - stunV2 "github.com/pion/stun/v2" + stunV3 "github.com/pion/stun/v3" ) //go:embed stun-list.txt @@ -107,24 +107,24 @@ func (s *stun) randomServers(n int) []string { func (s *stun) externalIP(server string) (net.IP, error) { _, _, err := net.SplitHostPort(server) if err != nil { - server += fmt.Sprintf(":%d", stunV2.DefaultPort) + server += fmt.Sprintf(":%d", stunV3.DefaultPort) } log.Trace("Attempting STUN binding request", "server", server) - conn, err := stunV2.Dial("udp4", server) + conn, err := stunV3.Dial("udp4", server) if err != nil { return nil, err } defer conn.Close() - message, err := stunV2.Build(stunV2.TransactionID, stunV2.BindingRequest) + message, err := stunV3.Build(stunV3.TransactionID, stunV3.BindingRequest) if err != nil { return nil, err } var responseError error - var mappedAddr stunV2.XORMappedAddress - err = conn.Do(message, func(event stunV2.Event) { + var mappedAddr stunV3.XORMappedAddress + err = conn.Do(message, func(event stunV3.Event) { if event.Error != nil { responseError = event.Error return From 14820029c933c26edb62f1d6ea2960b3b9ed1e54 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 27 May 2026 14:58:31 +0200 Subject: [PATCH 38/76] cmd/clef, cmd/geth: remove CLI flags that were deprecated for more than a year (#35021) This is another one of my slop-PRs, aimed at reducing the amount of future slop PRs by doing it all in one go. All of the deprecated cli flags have been in that state for over a year. It's time to remove them, especially since they are ineffective. Note that I kept the code to report and manage deprecated cli flags, as I assume we will be deprecating more flags in the future. --- cmd/clef/main.go | 10 ++- cmd/geth/chaincmd.go | 4 +- cmd/geth/config.go | 3 - cmd/geth/consolecmd_test.go | 5 +- cmd/geth/main.go | 19 ------ cmd/geth/run_test.go | 7 +- cmd/utils/flags.go | 36 +---------- cmd/utils/flags_legacy.go | 124 +----------------------------------- node/config.go | 4 -- signer/core/api.go | 50 +++++++-------- signer/core/api_test.go | 4 +- 11 files changed, 35 insertions(+), 231 deletions(-) diff --git a/cmd/clef/main.go b/cmd/clef/main.go index dde4ae853f..72dfd5016b 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -269,7 +269,6 @@ func init() { configdirFlag, chainIdFlag, utils.LightKDFFlag, - utils.NoUSBFlag, utils.SmartCardDaemonPathFlag, utils.HTTPListenAddrFlag, utils.HTTPVirtualHostsFlag, @@ -408,8 +407,8 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error ksLoc = c.String(keystoreFlag.Name) lightKdf = c.Bool(utils.LightKDFFlag.Name) ) - am := core.StartClefAccountManager(ksLoc, true, lightKdf, "") - api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage) + am := core.StartClefAccountManager(ksLoc, lightKdf, "") + api := core.NewSignerAPI(am, 0, ui, nil, false, pwStorage) internalApi := core.NewUIServerAPI(api) return internalApi, ui, nil } @@ -698,14 +697,13 @@ func signer(c *cli.Context) error { ksLoc = c.String(keystoreFlag.Name) lightKdf = c.Bool(utils.LightKDFFlag.Name) advanced = c.Bool(advancedMode.Name) - nousb = c.Bool(utils.NoUSBFlag.Name) scpath = c.String(utils.SmartCardDaemonPathFlag.Name) ) log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, "light-kdf", lightKdf, "advanced", advanced) - am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath) + am := core.StartClefAccountManager(ksLoc, lightKdf, scpath) defer am.Close() - apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) + apiImpl := core.NewSignerAPI(am, chainId, ui, db, advanced, pwStorage) // Establish the bidirectional communication, by creating a new UI backend and registering // it with the UI. diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 98ed348d8c..d32756591b 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -101,7 +101,6 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.NoCompactionFlag, utils.LogSlowBlockFlag, utils.MetricsEnabledFlag, - utils.MetricsEnabledExpensiveFlag, utils.MetricsHTTPFlag, utils.MetricsPortFlag, utils.MetricsEnableInfluxDBFlag, @@ -116,7 +115,6 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, utils.StateSizeTrackingFlag, - utils.TxLookupLimitFlag, utils.VMTraceFlag, utils.VMTraceJsonConfigFlag, utils.TransactionHistoryFlag, @@ -157,7 +155,7 @@ be gzipped.`, Name: "import-history", Usage: "Import an Era archive", ArgsUsage: "", - Flags: slices.Concat([]cli.Flag{utils.TxLookupLimitFlag, utils.TransactionHistoryFlag, utils.EraFormatFlag}, utils.DatabaseFlags, utils.NetworkFlags), + Flags: slices.Concat([]cli.Flag{utils.TransactionHistoryFlag, utils.EraFormatFlag}, utils.DatabaseFlags, utils.NetworkFlags), Description: ` The import-history command will import blocks and their corresponding receipts from Era archives. diff --git a/cmd/geth/config.go b/cmd/geth/config.go index c02e307bdc..ab9a336349 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -354,9 +354,6 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) { if ctx.IsSet(utils.MetricsEnabledFlag.Name) { cfg.Metrics.Enabled = ctx.Bool(utils.MetricsEnabledFlag.Name) } - if ctx.IsSet(utils.MetricsEnabledExpensiveFlag.Name) { - log.Warn("Expensive metrics are collected by default, please remove this flag", "flag", utils.MetricsEnabledExpensiveFlag.Name) - } if ctx.IsSet(utils.MetricsHTTPFlag.Name) { cfg.Metrics.HTTP = ctx.String(utils.MetricsHTTPFlag.Name) } diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 12ee7e7dd1..2c8522d109 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -51,10 +51,9 @@ func runMinimalGeth(t *testing.T, args ...string) *testgeth { // then terminated by closing the input stream. func TestConsoleWelcome(t *testing.T) { t.Parallel() - coinbase := "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182" // Start a geth console, make sure it's cleaned up and terminate the console - geth := runMinimalGeth(t, "--miner.etherbase", coinbase, "console") + geth := runMinimalGeth(t, "console") // Gather all the infos the welcome message needs to contain geth.SetTemplateFunc("goos", func() string { return runtime.GOOS }) @@ -98,7 +97,7 @@ func TestAttachWelcome(t *testing.T) { p := trulyRandInt(1024, 65533) // Yeah, sometimes this will fail, sorry :P httpPort = strconv.Itoa(p) wsPort = strconv.Itoa(p + 1) - geth := runMinimalGeth(t, "--miner.etherbase", "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182", + geth := runMinimalGeth(t, "--ipcpath", ipc, "--http", "--http.port", httpPort, "--ws", "--ws.port", wsPort) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 850e26d161..e547256e00 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -49,13 +49,11 @@ var ( // flags that configure the node nodeFlags = slices.Concat([]cli.Flag{ utils.IdentityFlag, - utils.UnlockedAccountFlag, utils.PasswordFileFlag, utils.BootnodesFlag, utils.MinFreeDiskSpaceFlag, utils.KeyStoreDirFlag, utils.ExternalSignerFlag, - utils.NoUSBFlag, // deprecated utils.USBFlag, utils.SmartCardDaemonPathFlag, utils.OverrideOsaka, @@ -63,7 +61,6 @@ var ( utils.OverrideBPO2, utils.OverrideUBT, utils.OverrideGenesisFlag, - utils.EnablePersonal, // deprecated utils.TxPoolLocalsFlag, utils.TxPoolNoLocalsFlag, utils.TxPoolJournalFlag, @@ -83,7 +80,6 @@ var ( utils.ExitWhenSyncedFlag, utils.GCModeFlag, utils.SnapshotFlag, - utils.TxLookupLimitFlag, // deprecated utils.TransactionHistoryFlag, utils.ChainHistoryFlag, utils.LogHistoryFlag, @@ -95,12 +91,9 @@ var ( utils.BinTrieGroupDepthFlag, utils.LightKDFFlag, utils.EthRequiredBlocksFlag, - utils.LegacyWhitelistFlag, // deprecated utils.CacheFlag, utils.CacheDatabaseFlag, utils.CacheTrieFlag, - utils.CacheTrieJournalFlag, // deprecated - utils.CacheTrieRejournalFlag, // deprecated utils.CacheGCFlag, utils.CacheSnapshotFlag, utils.CacheNoPrefetchFlag, @@ -112,20 +105,16 @@ var ( utils.DiscoveryPortFlag, utils.MaxPeersFlag, utils.MaxPendingPeersFlag, - utils.MiningEnabledFlag, // deprecated utils.MinerGasLimitFlag, utils.MinerGasPriceFlag, - utils.MinerEtherbaseFlag, // deprecated utils.MinerExtraDataFlag, utils.MinerMaxBlobsFlag, utils.MinerRecommitIntervalFlag, utils.MinerPendingFeeRecipientFlag, - utils.MinerNewPayloadTimeoutFlag, // deprecated utils.NATFlag, utils.NoDiscoverFlag, utils.DiscoveryV4Flag, utils.DiscoveryV5Flag, - utils.LegacyDiscoveryV5Flag, // deprecated utils.NetrestrictFlag, utils.NodeKeyFileFlag, utils.NodeKeyHexFlag, @@ -145,8 +134,6 @@ var ( utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, configFileFlag, - utils.LogDebugFlag, - utils.LogBacktraceAtFlag, utils.BeaconApiFlag, utils.BeaconApiHeaderFlag, utils.BeaconThresholdFlag, @@ -182,7 +169,6 @@ var ( utils.WSPathPrefixFlag, utils.IPCDisabledFlag, utils.IPCPathFlag, - utils.InsecureUnlockAllowedFlag, utils.RPCGlobalGasCapFlag, utils.RPCGlobalEVMTimeoutFlag, utils.RPCGlobalTxFeeCapFlag, @@ -204,7 +190,6 @@ var ( metricsFlags = []cli.Flag{ utils.MetricsEnabledFlag, - utils.MetricsEnabledExpensiveFlag, utils.MetricsHTTPFlag, utils.MetricsPortFlag, utils.MetricsEnableInfluxDBFlag, @@ -340,10 +325,6 @@ func startNode(ctx *cli.Context, stack *node.Node, isConsole bool) { // Start up the node itself utils.StartNode(ctx, stack, isConsole) - if ctx.IsSet(utils.UnlockedAccountFlag.Name) { - log.Warn(`The "unlock" flag has been deprecated and has no effect`) - } - // Register wallet event handlers to open and auto-derive wallets events := make(chan accounts.WalletEvent, 16) stack.AccountManager().Subscribe(events) diff --git a/cmd/geth/run_test.go b/cmd/geth/run_test.go index 1d32880325..f75e32f570 100644 --- a/cmd/geth/run_test.go +++ b/cmd/geth/run_test.go @@ -32,8 +32,7 @@ type testgeth struct { *cmdtest.TestCmd // template variables for expect - Datadir string - Etherbase string + Datadir string } func init() { @@ -75,10 +74,6 @@ func runGeth(t *testing.T, args ...string) *testgeth { if i < len(args)-1 { tt.Datadir = args[i+1] } - case "--miner.etherbase": - if i < len(args)-1 { - tt.Etherbase = args[i+1] - } } } if tt.Datadir == "" { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 77a3bffc8b..df969bc3cc 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1432,9 +1432,6 @@ func MakeDatabaseHandles(max int) int { // setEtherbase retrieves the etherbase from the directly specified command line flags. func setEtherbase(ctx *cli.Context, cfg *ethconfig.Config) { - if ctx.IsSet(MinerEtherbaseFlag.Name) { - log.Warn("Option --miner.etherbase is deprecated as the etherbase is set by the consensus client post-merge") - } if !ctx.IsSet(MinerPendingFeeRecipientFlag.Name) { return } @@ -1507,9 +1504,6 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { if ctx.IsSet(JWTSecretFlag.Name) { cfg.JWTSecret = ctx.String(JWTSecretFlag.Name) } - if ctx.IsSet(EnablePersonal.Name) { - log.Warn(fmt.Sprintf("Option --%s is deprecated. The 'personal' RPC namespace has been removed.", EnablePersonal.Name)) - } if ctx.IsSet(ExternalSignerFlag.Name) { cfg.ExternalSigner = ctx.String(ExternalSignerFlag.Name) @@ -1524,15 +1518,9 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { if ctx.IsSet(LightKDFFlag.Name) { cfg.UseLightweightKDF = ctx.Bool(LightKDFFlag.Name) } - if ctx.IsSet(NoUSBFlag.Name) || cfg.NoUSB { - log.Warn("Option --nousb is deprecated and USB is deactivated by default. Use --usb to enable") - } if ctx.IsSet(USBFlag.Name) { cfg.USB = ctx.Bool(USBFlag.Name) } - if ctx.IsSet(InsecureUnlockAllowedFlag.Name) { - log.Warn(fmt.Sprintf("Option --%s is deprecated and has no effect", InsecureUnlockAllowedFlag.Name)) - } if ctx.IsSet(DBEngineFlag.Name) { dbEngine := ctx.String(DBEngineFlag.Name) if dbEngine != "leveldb" && dbEngine != "pebble" { @@ -1541,13 +1529,6 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { log.Info(fmt.Sprintf("Using %s as db engine", dbEngine)) cfg.DBEngine = dbEngine } - // deprecation notice for log debug flags (TODO: find a more appropriate place to put these?) - if ctx.IsSet(LogBacktraceAtFlag.Name) { - log.Warn("Option --log.backtrace flag is deprecated") - } - if ctx.IsSet(LogDebugFlag.Name) { - log.Warn("Option --log.debug flag is deprecated") - } } func setSmartCard(ctx *cli.Context, cfg *node.Config) { @@ -1685,9 +1666,6 @@ func setBlobPool(ctx *cli.Context, cfg *blobpool.Config) { } func setMiner(ctx *cli.Context, cfg *miner.Config) { - if ctx.Bool(MiningEnabledFlag.Name) { - log.Warn("The flag --mine is deprecated and will be removed") - } if ctx.IsSet(MinerExtraDataFlag.Name) { cfg.ExtraData = []byte(ctx.String(MinerExtraDataFlag.Name)) } @@ -1700,10 +1678,6 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { if ctx.IsSet(MinerRecommitIntervalFlag.Name) { cfg.Recommit = ctx.Duration(MinerRecommitIntervalFlag.Name) } - if ctx.IsSet(MinerNewPayloadTimeoutFlag.Name) { - log.Warn("The flag --miner.newpayload-timeout is deprecated and will be removed, please use --miner.recommit") - cfg.Recommit = ctx.Duration(MinerNewPayloadTimeoutFlag.Name) - } if ctx.IsSet(MinerMaxBlobsFlag.Name) { cfg.MaxBlobsPerBlock = ctx.Int(MinerMaxBlobsFlag.Name) } @@ -1712,12 +1686,7 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) { requiredBlocks := ctx.String(EthRequiredBlocksFlag.Name) if requiredBlocks == "" { - if ctx.IsSet(LegacyWhitelistFlag.Name) { - log.Warn("The flag --whitelist is deprecated and will be removed, please use --eth.requiredblocks") - requiredBlocks = ctx.String(LegacyWhitelistFlag.Name) - } else { - return - } + return } cfg.RequiredBlocks = make(map[uint64]common.Hash) for _, entry := range strings.Split(requiredBlocks, ",") { @@ -1837,9 +1806,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { } if ctx.IsSet(TransactionHistoryFlag.Name) { cfg.TransactionHistory = ctx.Uint64(TransactionHistoryFlag.Name) - } else if ctx.IsSet(TxLookupLimitFlag.Name) { - log.Warn("The flag --txlookuplimit is deprecated and will be removed, please use --history.transactions") - cfg.TransactionHistory = ctx.Uint64(TxLookupLimitFlag.Name) } if cfg.NoPruning { if cfg.TransactionHistory != 0 { diff --git a/cmd/utils/flags_legacy.go b/cmd/utils/flags_legacy.go index 239be03ad6..2a3e417993 100644 --- a/cmd/utils/flags_legacy.go +++ b/cmd/utils/flags_legacy.go @@ -19,8 +19,6 @@ package utils import ( "fmt" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/internal/flags" "github.com/urfave/cli/v2" ) @@ -32,127 +30,7 @@ var ShowDeprecated = &cli.Command{ Description: "Show flags that have been deprecated and will soon be removed", } -var DeprecatedFlags = []cli.Flag{ - NoUSBFlag, - LegacyWhitelistFlag, - CacheTrieJournalFlag, - CacheTrieRejournalFlag, - LegacyDiscoveryV5Flag, - TxLookupLimitFlag, - LogBacktraceAtFlag, - LogDebugFlag, - MinerNewPayloadTimeoutFlag, - MinerEtherbaseFlag, - MiningEnabledFlag, - MetricsEnabledExpensiveFlag, - EnablePersonal, - UnlockedAccountFlag, - InsecureUnlockAllowedFlag, -} - -var ( - // Deprecated May 2020, shown in aliased flags section - NoUSBFlag = &cli.BoolFlag{ - Name: "nousb", - Hidden: true, - Usage: "Disables monitoring for and managing USB hardware wallets (deprecated)", - Category: flags.DeprecatedCategory, - } - // Deprecated March 2022 - LegacyWhitelistFlag = &cli.StringFlag{ - Name: "whitelist", - Hidden: true, - Usage: "Comma separated block number-to-hash mappings to enforce (=) (deprecated in favor of --eth.requiredblocks)", - Category: flags.DeprecatedCategory, - } - // Deprecated July 2023 - CacheTrieJournalFlag = &cli.StringFlag{ - Name: "cache.trie.journal", - Hidden: true, - Usage: "Disk journal directory for trie cache to survive node restarts", - Category: flags.DeprecatedCategory, - } - CacheTrieRejournalFlag = &cli.DurationFlag{ - Name: "cache.trie.rejournal", - Hidden: true, - Usage: "Time interval to regenerate the trie cache journal", - Category: flags.DeprecatedCategory, - } - LegacyDiscoveryV5Flag = &cli.BoolFlag{ - Name: "v5disc", - Hidden: true, - Usage: "Enables the experimental RLPx V5 (Topic Discovery) mechanism (deprecated, use --discv5 instead)", - Category: flags.DeprecatedCategory, - } - // Deprecated August 2023 - TxLookupLimitFlag = &cli.Uint64Flag{ - Name: "txlookuplimit", - Hidden: true, - Usage: "Number of recent blocks to maintain transactions index for (default = about one year, 0 = entire chain) (deprecated, use history.transactions instead)", - Value: ethconfig.Defaults.TransactionHistory, - Category: flags.DeprecatedCategory, - } - // Deprecated November 2023 - LogBacktraceAtFlag = &cli.StringFlag{ - Name: "log.backtrace", - Hidden: true, - Usage: "Request a stack trace at a specific logging statement (deprecated)", - Value: "", - Category: flags.DeprecatedCategory, - } - LogDebugFlag = &cli.BoolFlag{ - Name: "log.debug", - Hidden: true, - Usage: "Prepends log messages with call-site location (deprecated)", - Category: flags.DeprecatedCategory, - } - // Deprecated February 2024 - MinerNewPayloadTimeoutFlag = &cli.DurationFlag{ - Name: "miner.newpayload-timeout", - Hidden: true, - Usage: "Specify the maximum time allowance for creating a new payload (deprecated)", - Value: ethconfig.Defaults.Miner.Recommit, - Category: flags.DeprecatedCategory, - } - MinerEtherbaseFlag = &cli.StringFlag{ - Name: "miner.etherbase", - Hidden: true, - Usage: "0x prefixed public address for block mining rewards (deprecated)", - Category: flags.DeprecatedCategory, - } - MiningEnabledFlag = &cli.BoolFlag{ - Name: "mine", - Hidden: true, - Usage: "Enable mining (deprecated)", - Category: flags.DeprecatedCategory, - } - MetricsEnabledExpensiveFlag = &cli.BoolFlag{ - Name: "metrics.expensive", - Hidden: true, - Usage: "Enable expensive metrics collection and reporting (deprecated)", - Category: flags.DeprecatedCategory, - } - // Deprecated Oct 2024 - EnablePersonal = &cli.BoolFlag{ - Name: "rpc.enabledeprecatedpersonal", - Hidden: true, - Usage: "This used to enable the 'personal' namespace.", - Category: flags.DeprecatedCategory, - } - UnlockedAccountFlag = &cli.StringFlag{ - Name: "unlock", - Hidden: true, - Usage: "Comma separated list of accounts to unlock (deprecated)", - Value: "", - Category: flags.DeprecatedCategory, - } - InsecureUnlockAllowedFlag = &cli.BoolFlag{ - Name: "allow-insecure-unlock", - Hidden: true, - Usage: "Allow insecure account unlocking when account-related RPCs are exposed by http (deprecated)", - Category: flags.DeprecatedCategory, - } -) +var DeprecatedFlags = []cli.Flag{} // showDeprecated displays deprecated flags that will be soon removed from the codebase. func showDeprecated(*cli.Context) error { diff --git a/node/config.go b/node/config.go index 255b0f0aa9..77ca78a471 100644 --- a/node/config.go +++ b/node/config.go @@ -86,10 +86,6 @@ type Config struct { // InsecureUnlockAllowed is a deprecated option to allow users to accounts in unsafe http environment. InsecureUnlockAllowed bool `toml:",omitempty"` - // NoUSB disables hardware wallet monitoring and connectivity. - // Deprecated: USB monitoring is disabled by default and must be enabled explicitly. - NoUSB bool `toml:",omitempty"` - // USB enables hardware wallet monitoring and connectivity. USB bool `toml:",omitempty"` diff --git a/signer/core/api.go b/signer/core/api.go index 3b7b53a312..c5cc226b79 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -130,7 +130,7 @@ type Metadata struct { Origin string `json:"Origin"` } -func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath string) *accounts.Manager { +func StartClefAccountManager(ksLocation string, lightKDF bool, scpath string) *accounts.Manager { var ( backends []accounts.Backend n, p = keystore.StandardScryptN, keystore.StandardScryptP @@ -142,28 +142,26 @@ func StartClefAccountManager(ksLocation string, nousb, lightKDF bool, scpath str if len(ksLocation) > 0 { backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) } - if !nousb { - // Start a USB hub for Ledger hardware wallets - if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { - log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) - } else { - backends = append(backends, ledgerhub) - log.Debug("Ledger support enabled") - } - // Start a USB hub for Trezor hardware wallets (HID version) - if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { - log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) - } else { - backends = append(backends, trezorhub) - log.Debug("Trezor support enabled via HID") - } - // Start a USB hub for Trezor hardware wallets (WebUSB version) - if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { - log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) - } else { - backends = append(backends, trezorhub) - log.Debug("Trezor support enabled via WebUSB") - } + // Start a USB hub for Ledger hardware wallets + if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { + log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) + } else { + backends = append(backends, ledgerhub) + log.Debug("Ledger support enabled") + } + // Start a USB hub for Trezor hardware wallets (HID version) + if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { + log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) + } else { + backends = append(backends, trezorhub) + log.Debug("Trezor support enabled via HID") + } + // Start a USB hub for Trezor hardware wallets (WebUSB version) + if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { + log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) + } else { + backends = append(backends, trezorhub) + log.Debug("Trezor support enabled via WebUSB") } // Start a smart card hub @@ -282,14 +280,12 @@ var ErrRequestDenied = errors.New("request denied") // key that is generated when a new Account is created. // noUSB disables USB support that is required to support hardware devices such as // ledger and trezor. -func NewSignerAPI(am *accounts.Manager, chainID int64, noUSB bool, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { +func NewSignerAPI(am *accounts.Manager, chainID int64, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { if advancedMode { log.Info("Clef is in advanced mode: will warn instead of reject") } signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials} - if !noUSB { - signer.startUSBListener() - } + signer.startUSBListener() return signer } func (api *SignerAPI) openTrezor(url accounts.URL) { diff --git a/signer/core/api_test.go b/signer/core/api_test.go index ed4fdc5096..7c5cefdf8c 100644 --- a/signer/core/api_test.go +++ b/signer/core/api_test.go @@ -120,8 +120,8 @@ func setup(t *testing.T) (*core.SignerAPI, *headlessUi) { t.Fatal(err.Error()) } ui := &headlessUi{make(chan string, 20), make(chan string, 20)} - am := core.StartClefAccountManager(tmpDirName(t), true, true, "") - api := core.NewSignerAPI(am, 1337, true, ui, db, true, &storage.NoStorage{}) + am := core.StartClefAccountManager(tmpDirName(t), true, "") + api := core.NewSignerAPI(am, 1337, ui, db, true, &storage.NoStorage{}) return api, ui } func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) { From 9d21f6ebd5817dc7c0078ea3e765531de1760f05 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 27 May 2026 22:30:03 +0800 Subject: [PATCH 39/76] crypto/ecies: correctly return ErrInvalidMessage (#35037) --- crypto/ecies/ecies.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crypto/ecies/ecies.go b/crypto/ecies/ecies.go index 378d764a19..c99756a65b 100644 --- a/crypto/ecies/ecies.go +++ b/crypto/ecies/ecies.go @@ -254,9 +254,12 @@ func Encrypt(rand io.Reader, pub *PublicKey, m, s1, s2 []byte) (ct []byte, err e Ke, Km := deriveKeys(hash, z, s1, params.KeyLen) em, err := symEncrypt(rand, params, Ke, m) - if err != nil || len(em) <= params.BlockSize { + if err != nil { return nil, err } + if len(em) <= params.BlockSize { + return nil, ErrInvalidMessage + } d := messageTag(params.Hash, Km, em, s2) From f4a90d178a53ab8792dde74eec8db40c6120e111 Mon Sep 17 00:00:00 2001 From: cui Date: Wed, 27 May 2026 22:32:51 +0800 Subject: [PATCH 40/76] rpc: fix method-name matched before maxMethodNameLength (#35038) Co-authored-by: MariusVanDerWijden --- rpc/handler.go | 9 ++++----- rpc/websocket_test.go | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/rpc/handler.go b/rpc/handler.go index 89fc78236c..a9ffdc7071 100644 --- a/rpc/handler.go +++ b/rpc/handler.go @@ -502,6 +502,10 @@ func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMess // handleCall processes method calls. func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage { + // Check method name length + if len(msg.Method) > maxMethodNameLength { + return msg.errorResponse(&invalidRequestError{fmt.Sprintf("method name too long: %d > %d", len(msg.Method), maxMethodNameLength)}) + } if msg.isSubscribe() { return h.handleSubscribe(cp, msg) } @@ -512,11 +516,6 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage } return h.runMethod(cp.ctx, msg, h.unsubscribeCb, args) } - - // Check method name length - if len(msg.Method) > maxMethodNameLength { - return msg.errorResponse(&invalidRequestError{fmt.Sprintf("method name too long: %d > %d", len(msg.Method), maxMethodNameLength)}) - } callb, service, method := h.reg.callback(msg.Method) // If the method is not found, return an error. diff --git a/rpc/websocket_test.go b/rpc/websocket_test.go index 3b7d5a9da0..0661ffdb24 100644 --- a/rpc/websocket_test.go +++ b/rpc/websocket_test.go @@ -432,10 +432,10 @@ func TestWebsocketMethodNameLengthLimit(t *testing.T) { isSubscription: true, }, { - name: "subscription name too long", + name: "method name too long subscribe", method: string(make([]byte, maxMethodNameLength+1)) + "_subscribe", params: []interface{}{"newHeads"}, - expectedError: "subscription name too long", + expectedError: "method name too long", isSubscription: true, }, } From ab20d50dba07ce4e0fde35fca802b5deb246b5ee Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 28 May 2026 00:27:35 +0800 Subject: [PATCH 41/76] log: return TerminalHandler write errors from Handle (#35055) Propagate slog Handle failures when the underlying io.Writer rejects output. --- log/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/log/handler.go b/log/handler.go index 56eff6671f..9c98552e7c 100644 --- a/log/handler.go +++ b/log/handler.go @@ -77,9 +77,9 @@ func (h *TerminalHandler) Handle(_ context.Context, r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() buf := h.format(h.buf, r, h.useColor) - h.wr.Write(buf) + _, err := h.wr.Write(buf) h.buf = buf[:0] - return nil + return err } func (h *TerminalHandler) Enabled(_ context.Context, level slog.Level) bool { From b0df33967c12bd17ca28f44746df3a1463c72c62 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Thu, 28 May 2026 02:30:08 -0500 Subject: [PATCH 42/76] node, cmd/clef, graphql: disable gzip on engine API (#35057) Add a disableGzip parameter to NewHTTPHandlerStack and httpConfig. initAuth sets it true so compression is disabled in the engine api. Public HTTP RPC behavior is unchanged. --- cmd/clef/main.go | 2 +- graphql/service.go | 2 +- node/node.go | 1 + node/rpcstack.go | 8 ++++++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/clef/main.go b/cmd/clef/main.go index 72dfd5016b..2b54bb14cb 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -739,7 +739,7 @@ func signer(c *cli.Context) error { if err != nil { utils.Fatalf("Could not register API: %w", err) } - handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil) + handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil, false) // set port port := c.Int(rpcPortFlag.Name) diff --git a/graphql/service.go b/graphql/service.go index 4d530586a3..b65ac20baf 100644 --- a/graphql/service.go +++ b/graphql/service.go @@ -148,7 +148,7 @@ func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters. return nil, err } h := handler{Schema: s} - handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil) + handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil, false) stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{}) stack.RegisterHandler("GraphQL UI", "/graphql/ui/", GraphiQL{}) diff --git a/node/node.go b/node/node.go index 56ecd7d522..9e87337c2d 100644 --- a/node/node.go +++ b/node/node.go @@ -445,6 +445,7 @@ func (n *Node) startRPC() error { Vhosts: n.config.AuthVirtualHosts, Modules: DefaultAuthModules, prefix: DefaultAuthPrefix, + disableGzip: true, rpcEndpointConfig: sharedConfig, }) if err != nil { diff --git a/node/rpcstack.go b/node/rpcstack.go index 1db2ed3f44..bd19b58b84 100644 --- a/node/rpcstack.go +++ b/node/rpcstack.go @@ -42,6 +42,7 @@ type httpConfig struct { CorsAllowedOrigins []string Vhosts []string prefix string // path prefix on which to mount http handler + disableGzip bool rpcEndpointConfig } @@ -320,7 +321,7 @@ func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error { } h.httpConfig = config h.httpHandler.Store(&rpcHandler{ - Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret), + Handler: NewHTTPHandlerStack(srv, config.CorsAllowedOrigins, config.Vhosts, config.jwtSecret, config.disableGzip), prefix: config.prefix, server: srv, }) @@ -402,13 +403,16 @@ func isWebsocket(r *http.Request) bool { } // NewHTTPHandlerStack returns wrapped http-related handlers -func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte) http.Handler { +func NewHTTPHandlerStack(srv http.Handler, cors []string, vhosts []string, jwtSecret []byte, disableGzip bool) http.Handler { // Wrap the CORS-handler within a host-handler handler := newCorsHandler(srv, cors) handler = newVHostHandler(vhosts, handler) if len(jwtSecret) != 0 { handler = newJWTHandler(jwtSecret, handler) } + if disableGzip { + return handler + } return newGzipHandler(handler) } From 10a198220350b6e767346afc0157a57ef1816dcd Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 28 May 2026 16:05:40 +0800 Subject: [PATCH 43/76] eth/protocols/eth: fix track-before-send (#35056) Track the transaction only after it was sent out --- eth/protocols/eth/peer.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 2d7079fa12..9e1daae608 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -159,11 +159,13 @@ func (p *Peer) MarkTransaction(hash common.Hash) { // The reasons this is public is to allow packages using this protocol to write // tests that directly send messages without having to do the async queueing. func (p *Peer) SendTransactions(txs types.Transactions) error { - // Mark all the transactions as known, but ensure we don't overflow our limits + if err := p2p.Send(p.rw, TransactionsMsg, txs); err != nil { + return err + } for _, tx := range txs { p.knownTxs.Add(tx.Hash()) } - return p2p.Send(p.rw, TransactionsMsg, txs) + return nil } // AsyncSendTransactions queues a list of transactions (by hash) to eventually From 9f434c04db0a5e443139efd381e25095e1a6da48 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Thu, 28 May 2026 10:16:15 +0200 Subject: [PATCH 44/76] go.mod: update pion/dtls (#35062) Updates dtls to newest version --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 2a96f2c761..1a35f89ce5 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 - github.com/pion/stun/v3 v3.0.1 + github.com/pion/stun/v3 v3.1.2 github.com/protolambda/bls12-381-util v0.1.0 github.com/protolambda/zrnt v0.34.1 github.com/protolambda/ztyp v0.2.2 @@ -75,7 +75,7 @@ require ( golang.org/x/sync v0.19.0 golang.org/x/sys v0.40.0 golang.org/x/text v0.33.0 - golang.org/x/time v0.9.0 + golang.org/x/time v0.10.0 golang.org/x/tools v0.40.0 google.golang.org/protobuf v1.36.11 gopkg.in/natefinch/lumberjack.v2 v2.2.1 @@ -87,7 +87,8 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect - github.com/pion/dtls/v3 v3.0.7 // indirect + github.com/pion/dtls/v3 v3.1.2 // indirect + github.com/pion/transport/v4 v4.0.1 // indirect github.com/wlynxg/anet v0.0.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect @@ -150,7 +151,6 @@ require ( github.com/naoina/go-stringutil v0.1.0 // indirect github.com/opentracing/opentracing-go v1.1.0 // indirect github.com/pion/logging v0.2.4 // indirect - github.com/pion/transport/v3 v3.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.15.0 // indirect diff --git a/go.sum b/go.sum index a95ae43224..b355b32050 100644 --- a/go.sum +++ b/go.sum @@ -297,14 +297,14 @@ github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQm github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= -github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= -github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= -github.com/pion/transport/v3 v3.0.8 h1:oI3myyYnTKUSTthu/NZZ8eu2I5sHbxbUNNFW62olaYc= -github.com/pion/transport/v3 v3.0.8/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= +github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -481,8 +481,8 @@ golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= From 4017efe3450586e932cf12f85ad1fefc7091d266 Mon Sep 17 00:00:00 2001 From: rayoo Date: Thu, 28 May 2026 16:38:34 +0800 Subject: [PATCH 45/76] rpc: reject empty batch in BatchCallContext (#34985) The server already rejects empty batches with -32600. On the client side, calling BatchCallContext with a zero-length slice on inproc/WS/IPC transports registers no request IDs but the server still replies with an error message whose id is null. The dispatch loop has no requestOp to match it to, so op.resp is never written and op.wait blocks until ctx deadline. Short-circuit on len(b) == 0 with the same invalidRequestError the server uses, so all transports return immediately with -32600. --- rpc/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpc/client.go b/rpc/client.go index 9175626241..51a595e726 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -397,6 +397,9 @@ func (c *Client) BatchCall(b []BatchElem) error { // // Note that batch calls may not be executed atomically on the server side. func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error { + if len(b) == 0 { + return &invalidRequestError{"empty batch"} + } var ( msgs = make([]*jsonrpcMessage, len(b)) byID = make(map[string]int, len(b)) From 95320ffe69fd889cde412f5a073f3ac2f26b8ef2 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Thu, 28 May 2026 10:52:27 +0200 Subject: [PATCH 46/76] miner: set slot number for pending block post-Amsterdam (#34792) --- miner/miner.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/miner/miner.go b/miner/miner.go index 0ff0237a08..921200bcaa 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -151,12 +151,24 @@ func (miner *Miner) getPending() *newPayloadResult { return cached } var ( - timestamp = uint64(time.Now().Unix()) - withdrawal types.Withdrawals + timestamp = uint64(time.Now().Unix()) + childNumber = new(big.Int).Add(header.Number, big.NewInt(1)) + withdrawal types.Withdrawals + slotNum *uint64 ) - if miner.chainConfig.IsShanghai(new(big.Int).Add(header.Number, big.NewInt(1)), timestamp) { + if miner.chainConfig.IsShanghai(childNumber, timestamp) { withdrawal = []*types.Withdrawal{} } + // Post-Amsterdam, prepareWork requires a slot number (EIP-7843). The pending + // block is synthetic and has no canonical slot, so derive one from the parent + // when available and fall back to zero otherwise. + if miner.chainConfig.IsAmsterdam(childNumber, timestamp) { + var n uint64 + if header.SlotNumber != nil { + n = *header.SlotNumber + 1 + } + slotNum = &n + } ret := miner.generateWork(context.Background(), &generateParams{ timestamp: timestamp, @@ -166,6 +178,7 @@ func (miner *Miner) getPending() *newPayloadResult { random: common.Hash{}, withdrawals: withdrawal, beaconRoot: nil, + slotNum: slotNum, noTxs: false, }, false) // we will never make a witness for a pending block if ret.err != nil { From 61342e9c01b90d8ccb3c51af78f2c8e9c0b7dd38 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Thu, 28 May 2026 17:06:47 +0200 Subject: [PATCH 47/76] trie/bintrie: record inserted leaves for t8n (#34843) Because the UBT doesn't differentiate slots from accounts, the content of the tree can not be exported as a `GenesisAlloc`, which means that `evm t8n` can not intergrate it. We have tried integrating the new format into execution-specs, but this is very hard to maintain because the team doesn't see it as a priority and their own repository is seeing a lot of churn. This PR adds the ability to capture the structure of what is being inserted in the tree, so that the information isn't lost and it can be dumped in the t8n context. --------- Co-authored-by: felipe --- cmd/evm/internal/t8ntool/execution.go | 22 +- cmd/evm/internal/t8ntool/transition.go | 48 ++++- core/state/database_ubt.go | 29 ++- tests/init.go | 28 +++ trie/bintrie/recorder.go | 126 +++++++++++ trie/bintrie/recorder_test.go | 277 +++++++++++++++++++++++++ trie/bintrie/trie.go | 36 +++- 7 files changed, 556 insertions(+), 10 deletions(-) create mode 100644 trie/bintrie/recorder.go create mode 100644 trie/bintrie/recorder_test.go diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 39dfbf772b..85a581eba6 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -418,9 +418,24 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, return statedb, execRs, body, nil } +// newPrestateTrieDBConfig returns the triedb config used to construct the +// prestate. UBT mode requires the path-based backend; the legacy hash-based +// backend cannot decode UBT-encoded nodes. +func newPrestateTrieDBConfig(isBintrie bool) *triedb.Config { + if isBintrie { + cfg := *triedb.UBTDefaults + cfg.Preimages = true + return &cfg + } + return &triedb.Config{Preimages: true} +} + func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool) *state.StateDB { - tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie}) + tdb := triedb.NewDatabase(db, newPrestateTrieDBConfig(isBintrie)) sdb := state.NewDatabase(tdb, nil) + if isBintrie { + sdb.(*state.UBTDatabase).EnableAllocRecording() + } root := types.EmptyRootHash if isBintrie { @@ -458,8 +473,11 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, isBintrie bool // MakePreStateStreaming is like MakePreState, but decodes the alloc from disk // one account at a time so the full map is never held in memory. func MakePreStateStreaming(db ethdb.Database, allocPath string, isBintrie bool) (*state.StateDB, error) { - tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsUBT: isBintrie}) + tdb := triedb.NewDatabase(db, newPrestateTrieDBConfig(isBintrie)) sdb := state.NewDatabase(tdb, nil) + if isBintrie { + sdb.(*state.UBTDatabase).EnableAllocRecording() + } root := types.EmptyRootHash if isBintrie { diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go index 89b703d3b8..6c6667e409 100644 --- a/cmd/evm/internal/t8ntool/transition.go +++ b/cmd/evm/internal/t8ntool/transition.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" @@ -243,14 +244,55 @@ func Transition(ctx *cli.Context) error { collector = make(Alloc) s.DumpToCollector(collector, nil) default: - btleaves = make(map[common.Hash]hexutil.Bytes) - if err := s.DumpBinTrieLeaves(btleaves); err != nil { - return err + udb, ok := s.Database().(*state.UBTDatabase) + if !ok { + return NewError(ErrorEVM, errors.New("expected UBTDatabase in binary trie mode")) + } + rec := udb.AllocRecorder() + if rec == nil { + return NewError(ErrorEVM, errors.New("UBT alloc recorder was not enabled")) + } + collector = Alloc(rec.Alloc()) + if err := mergeUnmigratedBaseAlloc(udb, s.IntermediateRoot(false), collector); err != nil { + return NewError(ErrorEVM, fmt.Errorf("failed to merge base MPT alloc: %v", err)) } } return dispatchOutput(ctx, baseDir, result, collector, allocOutput, body, btleaves) } +func mergeUnmigratedBaseAlloc(udb *state.UBTDatabase, currentRoot common.Hash, dst Alloc) error { + ts := overlay.LoadTransitionState(udb.TrieDB().Disk(), currentRoot, true) + if !ts.InTransition() { + return nil + } + if ts.BaseRoot == (common.Hash{}) || ts.BaseRoot == types.EmptyRootHash { + return nil + } + mptDB := state.NewMPTDatabase(udb.TrieDB(), nil) + sdb, err := state.New(ts.BaseRoot, mptDB) + if err != nil { + return fmt.Errorf("open base MPT at %x: %w", ts.BaseRoot, err) + } + if _, err := sdb.DumpToCollector(mergeAlloc(dst), nil); err != nil { + return fmt.Errorf("walk base MPT at %x: %w", ts.BaseRoot, err) + } + return nil +} + +type mergeAlloc Alloc + +func (m mergeAlloc) OnRoot(common.Hash) {} + +func (m mergeAlloc) OnAccount(addr *common.Address, da state.DumpAccount) { + if addr == nil { + return + } + if _, exists := m[*addr]; exists { + return + } + m[*addr] = dumpAccountToTypesAccount(da) +} + // writeStreamedAlloc writes the post-state alloc to path one account at a // time, producing the same JSON shape as saveFile on an Alloc map. func writeStreamedAlloc(path string, s *state.StateDB) error { diff --git a/core/state/database_ubt.go b/core/state/database_ubt.go index 16579f6d6a..d9b2f07a77 100644 --- a/core/state/database_ubt.go +++ b/core/state/database_ubt.go @@ -27,10 +27,26 @@ import ( // It provides the same functionality as MPTDatabase but uses unified binary // trie for state hashing instead of Merkle Patricia Tries. type UBTDatabase struct { - triedb *triedb.Database - codedb *CodeDB + triedb *triedb.Database + codedb *CodeDB + recorder *bintrie.Recorder } +// EnableAllocRecording installs an alloc recorder shared across every binary +// trie opened from this database. The recorder captures account, storage, and +// code writes keyed by their original (unhashed) addresses, which is required +// for tooling like evm t8n to render the post-state as a types.GenesisAlloc. +func (db *UBTDatabase) EnableAllocRecording() *bintrie.Recorder { + if db.recorder == nil { + db.recorder = bintrie.NewRecorder() + } + return db.recorder +} + +// AllocRecorder returns the attached recorder, or nil if recording was never +// enabled on this database. +func (db *UBTDatabase) AllocRecorder() *bintrie.Recorder { return db.recorder } + // Type returns Binary, indicating this database is backed by a Universal Binary Trie. func (db *UBTDatabase) Type() DatabaseType { return TypeUBT } @@ -96,7 +112,14 @@ func (db *UBTDatabase) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Rea // OpenTrie opens the main account trie at a specific root hash. func (db *UBTDatabase) OpenTrie(root common.Hash) (Trie, error) { - return bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth()) + tr, err := bintrie.NewBinaryTrie(root, db.triedb, db.triedb.BinTrieGroupDepth()) + if err != nil { + return nil, err + } + if db.recorder != nil { + tr.SetRecorder(db.recorder) + } + return tr, nil } // OpenStorageTrie opens the storage trie of an account. In binary trie mode, diff --git a/tests/init.go b/tests/init.go index 3db988a993..2550eb1231 100644 --- a/tests/init.go +++ b/tests/init.go @@ -776,6 +776,34 @@ var Forks = map[string]*params.ChainConfig{ ShanghaiTime: u64(0), UBTTime: u64(0), }, + "Binary": { + ChainID: big.NewInt(1), + HomesteadBlock: big.NewInt(0), + EIP150Block: big.NewInt(0), + EIP155Block: big.NewInt(0), + EIP158Block: big.NewInt(0), + ByzantiumBlock: big.NewInt(0), + ConstantinopleBlock: big.NewInt(0), + PetersburgBlock: big.NewInt(0), + IstanbulBlock: big.NewInt(0), + MuirGlacierBlock: big.NewInt(0), + BerlinBlock: big.NewInt(0), + LondonBlock: big.NewInt(0), + ArrowGlacierBlock: big.NewInt(0), + MergeNetsplitBlock: big.NewInt(0), + TerminalTotalDifficulty: big.NewInt(0), + ShanghaiTime: u64(0), + CancunTime: u64(0), + PragueTime: u64(0), + OsakaTime: u64(0), + UBTTime: u64(0), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, + BlobScheduleConfig: ¶ms.BlobScheduleConfig{ + Cancun: params.DefaultCancunBlobConfig, + Prague: params.DefaultPragueBlobConfig, + Osaka: params.DefaultOsakaBlobConfig, + }, + }, } var bpo1BlobConfig = ¶ms.BlobConfig{ diff --git a/trie/bintrie/recorder.go b/trie/bintrie/recorder.go new file mode 100644 index 0000000000..e6c757b106 --- /dev/null +++ b/trie/bintrie/recorder.go @@ -0,0 +1,126 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package bintrie + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Recorder maintains the inverse of the binary-trie key transform: it captures +// every mutation applied to a BinaryTrie keyed by the original address (and, +// for storage, the original slot key) so the post-state can be rendered as a +// types.GenesisAlloc. +type Recorder struct { + accounts map[common.Address]*types.Account +} + +// NewRecorder returns an empty Recorder. +func NewRecorder() *Recorder { + return &Recorder{accounts: make(map[common.Address]*types.Account)} +} + +// entry returns the existing account entry, or creates a fresh one. +func (r *Recorder) entry(addr common.Address) *types.Account { + if acc, ok := r.accounts[addr]; ok { + return acc + } + acc := &types.Account{} + r.accounts[addr] = acc + return acc +} + +// RecordAccount upserts the nonce and balance for addr. Existing storage and +// code on the entry are preserved. +func (r *Recorder) RecordAccount(addr common.Address, acc *types.StateAccount) { + e := r.entry(addr) + e.Nonce = acc.Nonce + if acc.Balance != nil { + e.Balance = acc.Balance.ToBig() + } else { + e.Balance = nil + } +} + +// RecordStorage records a storage write. A zero value removes the slot. +func (r *Recorder) RecordStorage(addr common.Address, key, value []byte) { + k := bytesToHash(key) + v := bytesToHash(value) + e := r.entry(addr) + if (v == common.Hash{}) { + if e.Storage != nil { + delete(e.Storage, k) + if len(e.Storage) == 0 { + e.Storage = nil + } + } + return + } + if e.Storage == nil { + e.Storage = make(map[common.Hash]common.Hash) + } + e.Storage[k] = v +} + +// RecordCode records the contract code for addr. Empty code clears the field. +func (r *Recorder) RecordCode(addr common.Address, code []byte) { + e := r.entry(addr) + if len(code) == 0 { + e.Code = nil + return + } + e.Code = common.CopyBytes(code) +} + +// RecordDeleteAccount drops addr entirely from the recorded set. +func (r *Recorder) RecordDeleteAccount(addr common.Address) { + delete(r.accounts, addr) +} + +// RecordDeleteStorage clears a single storage slot for addr. +func (r *Recorder) RecordDeleteStorage(addr common.Address, key []byte) { + r.RecordStorage(addr, key, nil) +} + +// Alloc returns the recorded post-state as a types.GenesisAlloc. The returned +// map shares storage with the recorder; callers must not mutate it concurrently +// with further Record calls. +func (r *Recorder) Alloc() types.GenesisAlloc { + out := make(types.GenesisAlloc, len(r.accounts)) + for addr, a := range r.accounts { + out[addr] = *a + } + return out +} + +// Has reports whether addr has been recorded. +func (r *Recorder) Has(addr common.Address) bool { + _, ok := r.accounts[addr] + return ok +} + +// bytesToHash left-pads short slices into a common.Hash, matching the +// normalization performed by BinaryTrie.UpdateStorage on values. +func bytesToHash(b []byte) common.Hash { + var h common.Hash + if len(b) >= common.HashLength { + copy(h[:], b[:common.HashLength]) + } else { + copy(h[common.HashLength-len(b):], b) + } + return h +} diff --git a/trie/bintrie/recorder_test.go b/trie/bintrie/recorder_test.go new file mode 100644 index 0000000000..57a7232dfd --- /dev/null +++ b/trie/bintrie/recorder_test.go @@ -0,0 +1,277 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package bintrie + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" +) + +func newRecorderTestTrie() *BinaryTrie { + return &BinaryTrie{ + store: newNodeStore(), + tracer: trie.NewPrevalueTracer(), + } +} + +// TestRecorderCapturesAccountWrite verifies the recorder mirrors a single +// UpdateAccount call into the resulting GenesisAlloc. +func TestRecorderCapturesAccountWrite(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x1111111111111111111111111111111111111111") + acc := &types.StateAccount{ + Nonce: 7, + Balance: uint256.NewInt(42), + CodeHash: common.HexToHash("aa").Bytes(), + } + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + + alloc := rec.Alloc() + got, ok := alloc[addr] + if !ok { + t.Fatalf("address %x missing from alloc", addr) + } + if got.Nonce != 7 { + t.Errorf("nonce: got %d want 7", got.Nonce) + } + if got.Balance == nil || got.Balance.Uint64() != 42 { + t.Errorf("balance: got %v want 42", got.Balance) + } +} + +// TestRecorderStorageRoundTrip verifies that storage writes are recorded with +// the original (unhashed) slot keys. +func TestRecorderStorageRoundTrip(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x2222222222222222222222222222222222222222") + acc := &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)} + if err := tr.UpdateAccount(addr, acc, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + + slot := common.HexToHash("00000000000000000000000000000000000000000000000000000000000000ff") + value := common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef") + if err := tr.UpdateStorage(addr, slot[:], value[:]); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + + alloc := rec.Alloc() + got := alloc[addr] + if got.Storage == nil { + t.Fatalf("storage map nil") + } + if got.Storage[slot] != value { + t.Errorf("storage[%x] = %x, want %x", slot, got.Storage[slot], value) + } +} + +// TestRecorderDeleteStorage verifies that writing a zero value (or calling +// DeleteStorage) removes the slot from the recorded set, matching MPT-dump +// semantics. +func TestRecorderDeleteStorage(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x3333333333333333333333333333333333333333") + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + + slotKept := common.HexToHash("00000000000000000000000000000000000000000000000000000000000000ff") + slotGone := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001") + if err := tr.UpdateStorage(addr, slotKept[:], common.HexToHash("01").Bytes()); err != nil { + t.Fatalf("UpdateStorage(kept): %v", err) + } + if err := tr.UpdateStorage(addr, slotGone[:], common.HexToHash("02").Bytes()); err != nil { + t.Fatalf("UpdateStorage(gone): %v", err) + } + if err := tr.DeleteStorage(addr, slotGone[:]); err != nil { + t.Fatalf("DeleteStorage: %v", err) + } + + alloc := rec.Alloc() + if _, exists := alloc[addr].Storage[slotGone]; exists { + t.Errorf("deleted slot still present") + } + if _, exists := alloc[addr].Storage[slotKept]; !exists { + t.Errorf("retained slot missing") + } +} + +// TestRecorderDeleteAccount verifies an account removed via DeleteAccount +// disappears from the alloc entirely, including its storage. +func TestRecorderDeleteAccount(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addrKept := common.HexToAddress("0x4444444444444444444444444444444444444444") + addrGone := common.HexToAddress("0x5555555555555555555555555555555555555555") + for _, a := range []common.Address{addrKept, addrGone} { + if err := tr.UpdateAccount(a, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil { + t.Fatalf("UpdateAccount(%x): %v", a, err) + } + } + slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001") + if err := tr.UpdateStorage(addrGone, slot[:], common.HexToHash("0a").Bytes()); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + if err := tr.DeleteAccount(addrGone); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + + alloc := rec.Alloc() + if _, exists := alloc[addrGone]; exists { + t.Errorf("deleted account still present in alloc") + } + if _, exists := alloc[addrKept]; !exists { + t.Errorf("untouched account missing from alloc") + } +} + +// TestRecorderDeleteThenRecreate verifies that recreating an account after a +// delete starts from a fresh entry — old storage and code do not bleed into +// the new account. +func TestRecorderDeleteThenRecreate(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x6666666666666666666666666666666666666666") + slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001") + + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(100)}, 0); err != nil { + t.Fatalf("UpdateAccount #1: %v", err) + } + if err := tr.UpdateStorage(addr, slot[:], common.HexToHash("0a").Bytes()); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + if err := tr.UpdateContractCode(addr, common.Hash{}, []byte{0x60, 0x00}); err != nil { + t.Fatalf("UpdateContractCode: %v", err) + } + if err := tr.DeleteAccount(addr); err != nil { + t.Fatalf("DeleteAccount: %v", err) + } + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 7, Balance: uint256.NewInt(9999)}, 0); err != nil { + t.Fatalf("UpdateAccount #2: %v", err) + } + + alloc := rec.Alloc() + got := alloc[addr] + if got.Nonce != 7 { + t.Errorf("nonce after recreate: got %d want 7", got.Nonce) + } + if got.Balance == nil || got.Balance.Uint64() != 9999 { + t.Errorf("balance after recreate: got %v want 9999", got.Balance) + } + if len(got.Storage) != 0 { + t.Errorf("recreated account has stale storage: %v", got.Storage) + } + if len(got.Code) != 0 { + t.Errorf("recreated account has stale code: %x", got.Code) + } +} + +// TestRecorderCodeOverwrite verifies that a second UpdateContractCode call +// replaces the previously-recorded code. +func TestRecorderCodeOverwrite(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x7777777777777777777777777777777777777777") + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + first := []byte{0x60, 0x01} + second := []byte{0x60, 0x02, 0x60, 0x03} + if err := tr.UpdateContractCode(addr, common.Hash{}, first); err != nil { + t.Fatalf("UpdateContractCode #1: %v", err) + } + if err := tr.UpdateContractCode(addr, common.Hash{}, second); err != nil { + t.Fatalf("UpdateContractCode #2: %v", err) + } + + alloc := rec.Alloc() + if !bytes.Equal(alloc[addr].Code, second) { + t.Errorf("code: got %x want %x", alloc[addr].Code, second) + } +} + +// TestRecorderPartialUpdatePreservesStorage verifies that a nonce/balance +// update on an account does not wipe its previously-recorded storage or code. +func TestRecorderPartialUpdatePreservesStorage(t *testing.T) { + tr := newRecorderTestTrie() + rec := NewRecorder() + tr.SetRecorder(rec) + + addr := common.HexToAddress("0x8888888888888888888888888888888888888888") + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + slot := common.HexToHash("0100000000000000000000000000000000000000000000000000000000000001") + if err := tr.UpdateStorage(addr, slot[:], common.HexToHash("0a").Bytes()); err != nil { + t.Fatalf("UpdateStorage: %v", err) + } + code := []byte{0x60, 0x05} + if err := tr.UpdateContractCode(addr, common.Hash{}, code); err != nil { + t.Fatalf("UpdateContractCode: %v", err) + } + // Bump nonce only; storage and code should survive. + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 2, Balance: uint256.NewInt(1)}, len(code)); err != nil { + t.Fatalf("UpdateAccount #2: %v", err) + } + + alloc := rec.Alloc() + got := alloc[addr] + if got.Nonce != 2 { + t.Errorf("nonce: got %d want 2", got.Nonce) + } + if got.Storage[slot] == (common.Hash{}) { + t.Errorf("storage was cleared by partial update") + } + if !bytes.Equal(got.Code, code) { + t.Errorf("code was cleared by partial update") + } +} + +// TestRecorderDisabledByDefault confirms that without SetRecorder the trie +// performs no recording (sanity check that hooks are gated). +func TestRecorderDisabledByDefault(t *testing.T) { + tr := newRecorderTestTrie() + if tr.Recorder() != nil { + t.Fatal("Recorder() should be nil before SetRecorder") + } + addr := common.HexToAddress("0x9999999999999999999999999999999999999999") + if err := tr.UpdateAccount(addr, &types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1)}, 0); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } +} diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go index e3436e3df1..0d0c0e0e70 100644 --- a/trie/bintrie/trie.go +++ b/trie/bintrie/trie.go @@ -111,12 +111,22 @@ type BinaryTrie struct { reader *trie.Reader tracer *trie.PrevalueTracer groupDepth int // Number of levels per serialized group (1-8, default 8) + recorder *Recorder } func (t *BinaryTrie) GroupDepth() int { return t.groupDepth } +// SetRecorder attaches an alloc recorder to the trie. Subsequent mutating +// operations will report the original (unhashed) account, storage, and code +// writes to the recorder so the post-state can be exported as a GenesisAlloc. +// Pass nil to detach. +func (t *BinaryTrie) SetRecorder(r *Recorder) { t.recorder = r } + +// Recorder returns the currently attached alloc recorder, or nil. +func (t *BinaryTrie) Recorder() *Recorder { return t.recorder } + // ToDot converts the binary trie to a DOT language representation. Useful for debugging. func (t *BinaryTrie) ToDot() string { t.store.computeHash(t.store.root) @@ -255,7 +265,13 @@ func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, values[BasicDataLeafKey] = basicData[:] values[CodeHashLeafKey] = acc.CodeHash[:] - return t.store.InsertValuesAtStem(stem, values, t.nodeResolver) + if err := t.store.InsertValuesAtStem(stem, values, t.nodeResolver); err != nil { + return err + } + if t.recorder != nil { + t.recorder.RecordAccount(addr, acc) + } + return nil } // UpdateStem updates the values for the given stem key. @@ -279,6 +295,9 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er if err != nil { return fmt.Errorf("UpdateStorage (%x) error: %v", address, err) } + if t.recorder != nil { + t.recorder.RecordStorage(address, key, value) + } return nil } @@ -293,7 +312,13 @@ func (t *BinaryTrie) DeleteAccount(addr common.Address) error { values[BasicDataLeafKey] = zero[:] values[CodeHashLeafKey] = zero[:] - return t.store.InsertValuesAtStem(stem, values, t.nodeResolver) + if err := t.store.InsertValuesAtStem(stem, values, t.nodeResolver); err != nil { + return err + } + if t.recorder != nil { + t.recorder.RecordDeleteAccount(addr) + } + return nil } // DeleteStorage removes any existing value for key from the trie. If a node was not @@ -305,6 +330,9 @@ func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error { if err != nil { return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err) } + if t.recorder != nil { + t.recorder.RecordDeleteStorage(addr, key) + } return nil } @@ -352,6 +380,7 @@ func (t *BinaryTrie) Copy() *BinaryTrie { reader: t.reader, tracer: t.tracer.Copy(), groupDepth: t.groupDepth, + recorder: t.recorder, } } @@ -390,6 +419,9 @@ func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Has } } } + if t.recorder != nil { + t.recorder.RecordCode(addr, code) + } return nil } From 0ef867b292ea508c2b1d6580c60a3d1f789422f3 Mon Sep 17 00:00:00 2001 From: Nikhil <150948502+Pablosinyores@users.noreply.github.com> Date: Fri, 29 May 2026 06:08:04 +0530 Subject: [PATCH 48/76] triedb/pathdb: fix swapped want/got args in journal-root mismatch error (#35067) --- triedb/pathdb/journal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go index 657fbbff27..ad0111e6c7 100644 --- a/triedb/pathdb/journal.go +++ b/triedb/pathdb/journal.go @@ -92,7 +92,7 @@ func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { // The journal is not matched with persistent state, discard them. // It can happen that geth crashes without persisting the journal. if !bytes.Equal(root.Bytes(), diskRoot.Bytes()) { - return nil, fmt.Errorf("%w want %x got %x", errUnmatchedJournal, root, diskRoot) + return nil, fmt.Errorf("%w want %x got %x", errUnmatchedJournal, diskRoot, root) } // Load the disk layer from the journal base, err := db.loadDiskLayer(r) From 7a73ffe8a31e3519761182a8593782ef02686346 Mon Sep 17 00:00:00 2001 From: Richard Creighton Date: Fri, 29 May 2026 07:11:42 +0100 Subject: [PATCH 49/76] accounts/usbwallet: check ledger versions for typed txs (#35044) Checks the Ledger Ethereum app version before sending typed transactions that require newer app support. EIP-2930/EIP-1559 transactions now require Ledger app v1.9.0 or newer, and EIP-7702 transactions require v1.17.0 or newer. Older apps now return the same kind of local update error already used for earlier Ledger feature gates instead of sending an unsupported transaction to the device. --------- Co-authored-by: Guillaume Ballet <3272758+gballet@users.noreply.github.com> --- accounts/usbwallet/ledger.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go index 79ff5929ba..09593567c1 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger.go @@ -110,6 +110,16 @@ func (w *ledgerDriver) offline() bool { return w.version == [3]byte{0, 0, 0} } +func ledgerVersionLessThan(version [3]byte, major, minor, patch byte) bool { + if version[0] != major { + return version[0] < major + } + if version[1] != minor { + return version[1] < minor + } + return version[2] < patch +} + // Open implements usbwallet.driver, attempting to initialize the connection to the // Ledger hardware wallet. The Ledger does not require a user passphrase, so that // parameter is silently discarded. @@ -166,7 +176,19 @@ func (w *ledgerDriver) SignTx(path accounts.DerivationPath, tx *types.Transactio return common.Address{}, nil, accounts.ErrWalletClosed } // Ensure the wallet is capable of signing the given transaction - if chainID != nil && (w.version[0] < 1 || (w.version[0] == 1 && w.version[1] == 0 && w.version[2] < 3)) { + switch tx.Type() { + case types.AccessListTxType, types.DynamicFeeTxType: + if ledgerVersionLessThan(w.version, 1, 9, 0) { + //lint:ignore ST1005 brand name displayed on the console + return common.Address{}, nil, fmt.Errorf("Ledger version >= 1.9.0 required for EIP-2930/EIP-1559 signing (found version v%d.%d.%d)", w.version[0], w.version[1], w.version[2]) + } + case types.SetCodeTxType: + if ledgerVersionLessThan(w.version, 1, 17, 0) { + //lint:ignore ST1005 brand name displayed on the console + return common.Address{}, nil, fmt.Errorf("Ledger version >= 1.17.0 required for EIP-7702 signing (found version v%d.%d.%d)", w.version[0], w.version[1], w.version[2]) + } + } + if chainID != nil && ledgerVersionLessThan(w.version, 1, 0, 3) { //lint:ignore ST1005 brand name displayed on the console return common.Address{}, nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", w.version[0], w.version[1], w.version[2]) } @@ -184,7 +206,7 @@ func (w *ledgerDriver) SignTypedMessage(path accounts.DerivationPath, domainHash return nil, accounts.ErrWalletClosed } // Ensure the wallet is capable of signing the given transaction - if w.version[0] < 1 || (w.version[0] == 1 && w.version[1] < 5) { + if ledgerVersionLessThan(w.version, 1, 5, 0) { //lint:ignore ST1005 brand name displayed on the console return nil, fmt.Errorf("Ledger version >= 1.5.0 required for EIP-712 signing (found version v%d.%d.%d)", w.version[0], w.version[1], w.version[2]) } From b90802251146679e9938018b3c1b0d6f35a34dd0 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 29 May 2026 15:20:01 +0200 Subject: [PATCH 50/76] rpc: always set content-length on HTTP responses (#35072) We recently changed the JSON encoding logic to use an internal `[]byte` buffer. This means we can now always set `Content-Length` on the response. --- rpc/http.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rpc/http.go b/rpc/http.go index 49618244df..93f5e26c30 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -288,7 +288,11 @@ func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) Serve // For error responses, it sets Content-Length and flushes to ensure the response // is fully written before any HTTP server write timeout occurs. func httpWriteResult(w http.ResponseWriter, data []byte, isError bool) error { + w.Header().Set("content-length", strconv.Itoa(len(data))) + if !isError { + // Normal path, just send the response and let the HTTP server decide + // when to flush. _, err := w.Write(data) return err } @@ -296,18 +300,15 @@ func httpWriteResult(w http.ResponseWriter, data []byte, isError bool) error { // It's an error response and requires special treatment. // // In case of a timeout error, the response must be written before the HTTP - // server's write timeout occurs. So we need to flush the response. The - // Content-Length header also needs to be set to ensure the client knows - // when it has the full response. - w.Header().Set("content-length", strconv.Itoa(len(data))) - + // server's write timeout occurs. So we need to flush the response. + // // If this request is wrapped in a handler that might remove Content-Length (such // as the automatic gzip we do in package node), we need to ensure the HTTP server // doesn't perform chunked encoding. In case WriteTimeout is reached, the chunked // encoding might not be finished correctly, and some clients do not like it when - // the final chunk is missing. + // the final chunk is missing. To do this, we set TE = identity, which is a signal + // recognized by outer handlers to avoid compression. w.Header().Set("transfer-encoding", "identity") - _, err := w.Write(data) if f, ok := w.(http.Flusher); ok { f.Flush() From 33711da4765b7ff99e35a3ad2685603691151312 Mon Sep 17 00:00:00 2001 From: cui Date: Sat, 30 May 2026 04:27:11 +0800 Subject: [PATCH 51/76] accounts/abi: fix wrong want count for events (#35077) --- accounts/abi/argument.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accounts/abi/argument.go b/accounts/abi/argument.go index e48f763890..c1bf7aec86 100644 --- a/accounts/abi/argument.go +++ b/accounts/abi/argument.go @@ -165,7 +165,7 @@ func (arguments Arguments) copyTuple(v any, marshalledValues []any) error { } case reflect.Slice, reflect.Array: if value.Len() < len(marshalledValues) { - return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(arguments), value.Len()) + return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(marshalledValues), value.Len()) } for i := range nonIndexedArgs { if err := set(value.Index(i), reflect.ValueOf(marshalledValues[i])); err != nil { From 046a10e8a79656273cfc535589085763f8b75419 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Fri, 29 May 2026 15:51:26 -0500 Subject: [PATCH 52/76] go.mod: bump go.opentelemetry.io from 1.40.0 to 1.41.0 (#35073) #35016 + cmd/keeper go mod tidy --- cmd/keeper/go.mod | 10 +++---- cmd/keeper/go.sum | 28 +++++++++--------- go.mod | 34 +++++++++++----------- go.sum | 72 +++++++++++++++++++++++------------------------ 4 files changed, 72 insertions(+), 72 deletions(-) diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index 2d99cb2232..b2caee8b63 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -35,12 +35,12 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect - golang.org/x/crypto v0.47.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index 09c8e55822..4d58974ab3 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -119,16 +119,16 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -137,10 +137,10 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.mod b/go.mod index 1a35f89ce5..cc3f7e7eb1 100644 --- a/go.mod +++ b/go.mod @@ -62,21 +62,21 @@ require ( github.com/supranational/blst v0.3.16 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/urfave/cli/v2 v2.27.5 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 + go.opentelemetry.io/otel/sdk v1.41.0 + go.opentelemetry.io/otel/trace v1.41.0 go.uber.org/automaxprocs v1.5.2 go.uber.org/goleak v1.3.0 - golang.org/x/crypto v0.47.0 + golang.org/x/crypto v0.48.0 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df golang.org/x/sync v0.19.0 - golang.org/x/sys v0.40.0 - golang.org/x/text v0.33.0 + golang.org/x/sys v0.41.0 + golang.org/x/text v0.34.0 golang.org/x/time v0.10.0 - golang.org/x/tools v0.40.0 + golang.org/x/tools v0.41.0 google.golang.org/protobuf v1.36.11 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -86,16 +86,16 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/pion/dtls/v3 v3.1.2 // indirect github.com/pion/transport/v4 v4.0.1 // indirect github.com/wlynxg/anet v0.0.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.1 // indirect ) require ( @@ -163,8 +163,8 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.50.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index b355b32050..1bc679a9f6 100644 --- a/go.sum +++ b/go.sum @@ -200,8 +200,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= @@ -376,22 +376,22 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME= @@ -404,15 +404,15 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -424,8 +424,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -465,8 +465,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -477,8 +477,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= @@ -489,8 +489,8 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -498,12 +498,12 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From b71f750916084571af3bf693d3284d86c0656a3f Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 1 Jun 2026 02:13:59 +0200 Subject: [PATCH 53/76] core, core/txpool, eth: move subscriptions to constructor (#35048) Closes https://github.com/ethereum/go-ethereum/issues/20554 It makes it easier to reason about the lifecycle. --- core/txindexer.go | 11 ++++++++--- core/txpool/txpool.go | 25 +++++++++++++------------ eth/backend.go | 24 ++++++++++++++++++------ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/core/txindexer.go b/core/txindexer.go index b2a94a6ead..ceff84d736 100644 --- a/core/txindexer.go +++ b/core/txindexer.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" ) @@ -64,6 +65,9 @@ type txIndexer struct { db ethdb.Database term chan chan struct{} closed chan struct{} + + headCh chan ChainHeadEvent + headSub event.Subscription } // newTxIndexer initializes the transaction indexer. @@ -75,7 +79,9 @@ func newTxIndexer(limit uint64, chain *BlockChain) *txIndexer { db: chain.db, term: make(chan chan struct{}), closed: make(chan struct{}), + headCh: make(chan ChainHeadEvent), } + indexer.headSub = chain.SubscribeChainHeadEvent(indexer.headCh) indexer.head.Store(indexer.resolveHead()) indexer.tail.Store(rawdb.ReadTxIndexTail(chain.db)) @@ -228,15 +234,14 @@ func (indexer *txIndexer) resolveHead() uint64 { // on the received chain event. func (indexer *txIndexer) loop(chain *BlockChain) { defer close(indexer.closed) + defer indexer.headSub.Unsubscribe() // Listening to chain events and manipulate the transaction indexes. var ( stop chan struct{} // Non-nil if background routine is active done chan struct{} // Non-nil if background routine is active - headCh = make(chan ChainHeadEvent) - sub = chain.SubscribeChainHeadEvent(headCh) + headCh = indexer.headCh ) - defer sub.Unsubscribe() // Validate the transaction indexes and repair if necessary head := indexer.head.Load() diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 9c78748422..cb425e5809 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -76,6 +76,9 @@ type TxPool struct { quit chan chan error // Quit channel to tear down the head updater term chan struct{} // Termination channel to detect a closed pool + newHeadCh chan core.ChainHeadEvent + newHeadSub event.Subscription + sync chan chan error // Testing / simulator channel to block until internal reset is done } @@ -98,13 +101,15 @@ func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) { return nil, err } pool := &TxPool{ - subpools: subpools, - chain: chain, - state: statedb, - quit: make(chan chan error), - term: make(chan struct{}), - sync: make(chan chan error), + subpools: subpools, + chain: chain, + state: statedb, + quit: make(chan chan error), + term: make(chan struct{}), + sync: make(chan chan error), + newHeadCh: make(chan core.ChainHeadEvent), } + pool.newHeadSub = chain.SubscribeChainHeadEvent(pool.newHeadCh) reserver := NewReservationTracker() for i, subpool := range subpools { if err := subpool.Init(gasTip, head, reserver.NewHandle(i)); err != nil { @@ -150,12 +155,8 @@ func (p *TxPool) loop(head *types.Header) { // Close the termination marker when the pool stops defer close(p.term) - // Subscribe to chain head events to trigger subpool resets - var ( - newHeadCh = make(chan core.ChainHeadEvent) - newHeadSub = p.chain.SubscribeChainHeadEvent(newHeadCh) - ) - defer newHeadSub.Unsubscribe() + newHeadCh := p.newHeadCh + defer p.newHeadSub.Unsubscribe() // Track the previous and current head to feed to an idle reset var ( diff --git a/eth/backend.go b/eth/backend.go index af8b04bda6..2f10351b9c 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -49,6 +49,7 @@ import ( "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/shutdowncheck" "github.com/ethereum/go-ethereum/internal/version" @@ -110,6 +111,13 @@ type Ethereum struct { filterMaps *filtermaps.FilterMaps closeFilterMaps chan chan struct{} + // Chain event subscriptions driving updateFilterMapsHeads. The + // subscriptions are registered and consumed in Start. + fmHeadEventCh chan core.ChainEvent + fmHeadSub event.Subscription + fmBlockProcCh chan bool + fmBlockProcSub event.Subscription + APIBackend *EthAPIBackend miner *miner.Miner @@ -199,6 +207,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { p2pServer: stack.Server(), discmix: enode.NewFairMix(discmixTimeout), shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb), + fmHeadEventCh: make(chan core.ChainEvent, 10), + fmBlockProcCh: make(chan bool, 10), } bcVersion := rawdb.ReadDatabaseVersion(chainDb) var dbVer = "" @@ -459,6 +469,10 @@ func (s *Ethereum) Start() error { // Start the connection manager s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() }) + // Subscribe to chain events for the filterMaps head updater. + s.fmHeadSub = s.blockchain.SubscribeChainEvent(s.fmHeadEventCh) + s.fmBlockProcSub = s.blockchain.SubscribeBlockProcessingEvent(s.fmBlockProcCh) + // start log indexer s.filterMaps.Start() go s.updateFilterMapsHeads() @@ -473,13 +487,11 @@ func (s *Ethereum) newChainView(head *types.Header) *filtermaps.ChainView { } func (s *Ethereum) updateFilterMapsHeads() { - headEventCh := make(chan core.ChainEvent, 10) - blockProcCh := make(chan bool, 10) - sub := s.blockchain.SubscribeChainEvent(headEventCh) - sub2 := s.blockchain.SubscribeBlockProcessingEvent(blockProcCh) + headEventCh := s.fmHeadEventCh + blockProcCh := s.fmBlockProcCh defer func() { - sub.Unsubscribe() - sub2.Unsubscribe() + s.fmHeadSub.Unsubscribe() + s.fmBlockProcSub.Unsubscribe() for { select { case <-headEventCh: From ff45d1dd7b918d07481c8da7dddc00377224b969 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 1 Jun 2026 10:56:05 +0800 Subject: [PATCH 54/76] internal: SetCodeTx tx.To must not be nil (#35094) --- internal/ethapi/transaction_args.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go index 1032d067f1..1a7cf1c118 100644 --- a/internal/ethapi/transaction_args.go +++ b/internal/ethapi/transaction_args.go @@ -138,6 +138,9 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config if len(args.data()) == 0 { return errors.New(`contract creation without any data provided`) } + if len(args.AuthorizationList) > 0 { + return errors.New(`authorizationList provided for contract creation, but "to" field is missing`) + } } if args.Gas == nil { From 5016e544066681ac22f099e31fbe0a45d9ce48e3 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 1 Jun 2026 10:56:23 +0800 Subject: [PATCH 55/76] eth/protocols/eth: only track after send is okay (#35086) --- eth/protocols/eth/peer.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 9e1daae608..0dfd2e41c6 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -209,14 +209,15 @@ func (p *Peer) AsyncSendPooledTransactionHashes(hashes []common.Hash) { // ReplyPooledTransactionsRLP is the response to RequestTxs. func (p *Peer) ReplyPooledTransactionsRLP(id uint64, hashes []common.Hash, txs []rlp.RawValue) error { - // Mark all the transactions as known, but ensure we don't overflow our limits - p.knownTxs.Add(hashes...) - // Not packed into PooledTransactionsResponse to avoid RLP decoding - return p2p.Send(p.rw, PooledTransactionsMsg, &PooledTransactionsRLPPacket{ + if err := p2p.Send(p.rw, PooledTransactionsMsg, &PooledTransactionsRLPPacket{ RequestId: id, PooledTransactionsRLPResponse: txs, - }) + }); err != nil { + return err + } + p.knownTxs.Add(hashes...) + return nil } // ReplyBlockHeadersRLP is the response to GetBlockHeaders. From 831ef5a453d0fd2eaea953500a76e81758a47c2f Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 1 Jun 2026 10:56:38 +0800 Subject: [PATCH 56/76] node: only delete db ref on close successfully (#35083) --- node/node.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/node/node.go b/node/node.go index 9e87337c2d..7c0d69775c 100644 --- a/node/node.go +++ b/node/node.go @@ -772,10 +772,13 @@ type closeTrackingDB struct { } func (db *closeTrackingDB) Close() error { - db.n.lock.Lock() - delete(db.n.databases, db) - db.n.lock.Unlock() - return db.Database.Close() + err := db.Database.Close() + if err == nil { + db.n.lock.Lock() + delete(db.n.databases, db) + db.n.lock.Unlock() + } + return err } // wrapDatabase ensures the database will be auto-closed when Node is closed. From 00f7c72ca72782596eb99552edba380336d1ddf9 Mon Sep 17 00:00:00 2001 From: cui Date: Mon, 1 Jun 2026 10:57:13 +0800 Subject: [PATCH 57/76] core/txpool/blobpool: blob pool with status queue (#35075) --- core/txpool/blobpool/blobpool.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 15f4430cc6..4ab97d35bf 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -2268,7 +2268,11 @@ func (p *BlobPool) Stats() (int, int) { for _, txs := range p.index { pending += len(txs) } - return pending, 0 // No non-executable txs in the blob pool + var queue int + for _, txs := range p.gapped { + queue += len(txs) + } + return pending, queue } // Content retrieves the data content of the transaction pool, returning all the From fdf99d9883c71036518fc4a243f3620539f8150b Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Mon, 1 Jun 2026 11:01:42 +0800 Subject: [PATCH 58/76] core/rawdb, ethdb, cmd, triedb: manage finalized block-accessList in freezer (#34977) This PR implements flat-file storage for finalized block access lists, specifically: * The freezer is extended with the notion of tail groups, allowing different groups within a single freezer instance to maintain independent tails, while all tables within the same group remain tail-aligned. * The freezer can now dynamically attach new tables to an existing freezer instance, with both the table head and tail initialized to the freezer's common head. * A new freezer table, **bals**, has been added to the chain freezer with its own dedicated tail group, preserving the flexibility to deploy a tail-pruning policy different from the main chain data group. Additionally, the BALs in the key-value store will be migrated to the freezer instance once they are finalized or there are at least 90K block confirmations on top acting as a "soft finalization". This freezing policy is same with all chain segment data. --- cmd/geth/chaincmd.go | 4 +- core/blockchain.go | 4 +- core/blockchain_test.go | 2 +- core/rawdb/accessors_chain.go | 29 +++- core/rawdb/accessors_chain_test.go | 41 ++++++ core/rawdb/ancient_scheme.go | 56 ++++++-- core/rawdb/ancient_utils.go | 65 +++++---- core/rawdb/ancienttest/testsuite.go | 19 ++- core/rawdb/chain_freezer.go | 41 ++++-- core/rawdb/database.go | 8 +- core/rawdb/freezer.go | 204 +++++++++++++++++++--------- core/rawdb/freezer_memory.go | 67 ++++++--- core/rawdb/freezer_memory_test.go | 8 +- core/rawdb/freezer_resettable.go | 14 +- core/rawdb/freezer_test.go | 135 +++++++++++++++++- core/rawdb/table.go | 8 +- ethdb/database.go | 25 ++-- ethdb/remotedb/remotedb.go | 4 +- triedb/pathdb/disklayer.go | 2 +- triedb/pathdb/history.go | 8 +- triedb/pathdb/history_indexer.go | 2 +- triedb/pathdb/history_inspect.go | 5 +- triedb/pathdb/history_reader.go | 2 +- triedb/pathdb/history_state_test.go | 2 +- 24 files changed, 557 insertions(+), 198 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index d32756591b..a27975c4c1 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -742,7 +742,7 @@ func pruneHistory(ctx *cli.Context) error { ) // Check the current freezer tail to see if pruning is needed/possible. - freezerTail, _ := chaindb.Tail() + freezerTail, _ := chaindb.Tail(rawdb.ChainFreezerBlockDataGroup) if freezerTail > 0 { if freezerTail == targetBlock { log.Info("Database already pruned to target block", "tail", freezerTail) @@ -774,7 +774,7 @@ func pruneHistory(ctx *cli.Context) error { log.Info("Starting history pruning", "head", currentHeader.Number, "target", targetBlock, "targetHash", targetBlockHash.Hex()) start := time.Now() rawdb.PruneTransactionIndex(chaindb, targetBlock) - if _, err := chaindb.TruncateTail(targetBlock); err != nil { + if _, err := chaindb.TruncateTail(rawdb.ChainFreezerBlockDataGroup, targetBlock); err != nil { return fmt.Errorf("failed to truncate ancient data: %v", err) } log.Info("History pruning completed", "tail", targetBlock, "elapsed", common.PrettyDuration(time.Since(start))) diff --git a/core/blockchain.go b/core/blockchain.go index 8d4943532c..166b58b05b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -722,7 +722,7 @@ func (bc *BlockChain) loadLastState() error { // initializeHistoryPruning sets bc.historyPrunePoint. func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { - freezerTail, _ := bc.db.Tail() + freezerTail, _ := bc.db.Tail(rawdb.ChainFreezerBlockDataGroup) policy := bc.cfg.HistoryPolicy switch policy.Mode { @@ -2967,7 +2967,7 @@ func (bc *BlockChain) InsertHeadersBeforeCutoff(headers []*types.Header) (int, e } // Truncate the useless chain segment (zero bodies and receipts) in the // ancient store. - if _, err := bc.db.TruncateTail(last.Number.Uint64() + 1); err != nil { + if _, err := bc.db.TruncateTail(rawdb.ChainFreezerBlockDataGroup, last.Number.Uint64()+1); err != nil { return 0, err } // Last step update all in-memory markers diff --git a/core/blockchain_test.go b/core/blockchain_test.go index a8ddf5caa8..26a6df5755 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4386,7 +4386,7 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, if header.Hash() != hash { t.Errorf("block #%d: header mismatch: want: %v, got: %v", num, hash, header.Hash()) } - tail, err := db.Tail() + tail, err := db.Tail(rawdb.ChainFreezerBlockDataGroup) if err != nil { t.Fatalf("Failed to get chain tail, %v", err) } diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 987b8df392..d8825b6b8f 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -614,9 +614,18 @@ func HasAccessList(db ethdb.Reader, hash common.Hash, number uint64) bool { return has } -// ReadAccessListRLP retrieves the RLP-encoded block access list for a block from KV. +// ReadAccessListRLP retrieves the RLP-encoded block access list for a block. func ReadAccessListRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(accessListKey(number, hash)) + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + data, _ = reader.Ancient(ChainFreezerBALTable, number) + if len(data) > 0 { + return nil + } + // Block is not in ancients, read from key-value store by hash and number. + data, _ = db.Get(accessListKey(number, hash)) + return nil + }) return data } @@ -759,6 +768,13 @@ func writeAncientBlock(op ethdb.AncientWriteOp, block *types.Block, header *type if err := op.Append(ChainFreezerReceiptTable, num, receipts); err != nil { return fmt.Errorf("can't append block %d receipts: %v", num, err) } + // The assumption is held that BAL of ancient block is no longer available + // (it may still reachable, but it's not worthwhile to even retrieve it + // from the network). A nil entry is stored in the BAL table as the absence + // placeholder. + if err := op.AppendRaw(ChainFreezerBALTable, num, nil); err != nil { + return fmt.Errorf("can't append block %d bals: %v", num, err) + } return nil } @@ -781,6 +797,13 @@ func WriteAncientHeaderChain(db ethdb.AncientWriter, headers []*types.Header) (i if err := op.AppendRaw(ChainFreezerReceiptTable, num, nil); err != nil { return fmt.Errorf("can't append block %d receipts: %v", num, err) } + // The assumption is held that BAL of ancient block is no longer available + // (it may still reachable, but it's not worthwhile to even retrieve it + // from the network). A nil entry is stored in the BAL table as the absence + // placeholder. + if err := op.AppendRaw(ChainFreezerBALTable, num, nil); err != nil { + return fmt.Errorf("can't append block %d bals: %v", num, err) + } } return nil }) @@ -791,6 +814,7 @@ func DeleteBlock(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) DeleteHeader(db, hash, number) DeleteBody(db, hash, number) + DeleteAccessList(db, hash, number) } // DeleteBlockWithoutNumber removes all block data associated with a hash, except @@ -799,6 +823,7 @@ func DeleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number DeleteReceipts(db, hash, number) deleteHeaderWithoutNumber(db, hash, number) DeleteBody(db, hash, number) + DeleteAccessList(db, hash, number) } const badBlockToKeep = 10 diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index c35f56ee07..76805cb3ec 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -926,6 +926,47 @@ func makeTestBAL(t *testing.T) (rlp.RawValue, *bal.BlockAccessList) { return encoded, &decoded } +// TestWriteAncientBlocksNilBAL ensures that freezing a block with no block +// access list produces an empty entry in the BAL ancient table and that +// ReadAccessList returns nil afterwards (i.e. the empty entry is not surfaced +// as a malformed BAL). +func TestWriteAncientBlocksNilBAL(t *testing.T) { + db, err := Open(NewMemoryDatabase(), OpenOptions{Ancient: t.TempDir()}) + if err != nil { + t.Fatalf("failed to create database with ancient backend: %v", err) + } + defer db.Close() + + block := types.NewBlockWithHeader(&types.Header{ + Number: big.NewInt(0), + Extra: []byte("nil-bal block"), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyTxsHash, + ReceiptHash: types.EmptyReceiptsHash, + }) + if block.AccessList() != nil { + t.Fatalf("test precondition: block must have nil access list") + } + if _, err := WriteAncientBlocks(db, []*types.Block{block}, types.EncodeBlockReceiptLists([]types.Receipts{nil})); err != nil { + t.Fatalf("WriteAncientBlocks failed: %v", err) + } + hash, number := block.Hash(), block.NumberU64() + + // The BAL ancient entry should exist as an empty blob. + if blob := ReadAccessListRLP(db, hash, number); len(blob) != 0 { + t.Fatalf("ReadAccessListRLP: got %x, want empty", blob) + } + // ReadAccessList must surface nil rather than attempting to RLP-decode + // the empty payload. + if b := ReadAccessList(db, hash, number); b != nil { + t.Fatalf("ReadAccessList: got %v, want nil", b) + } + // HasAccessList only consults the KV store and there's nothing there. + if HasAccessList(db, hash, number) { + t.Fatal("HasAccessList returned true for absent BAL") + } +} + // TestBALStorage tests write/read/delete of BALs in the KV store. func TestBALStorage(t *testing.T) { db := NewMemoryDatabase() diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index afec7848c8..9ee8f9d3d2 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -35,6 +35,23 @@ const ( // ChainFreezerReceiptTable indicates the name of the freezer receipts table. ChainFreezerReceiptTable = "receipts" + + // ChainFreezerBALTable indicates the name of the freezer block access list + // table introduced by EIP-7928. + ChainFreezerBALTable = "bals" +) + +// Identifiers of tail groups used by the chain freezer. +const ( + // ChainFreezerBlockDataGroup is the tail group shared by the body and + // receipt tables. The two tables are pruned together and therefore have + // the same tail position. + ChainFreezerBlockDataGroup = "blockdata" + + // ChainFreezerBALGroup is the tail group for the block access list table. + // BAL is only populated after EIP-7928 activates, so it generally has a + // higher tail than the block-data group and is pruned independently. + ChainFreezerBALGroup = "bal" ) // chainFreezerTableConfigs configures the settings for tables in the chain freezer. @@ -42,16 +59,23 @@ const ( // tail truncation is disabled for the header and hash tables, as these are intended // to be retained long-term. var chainFreezerTableConfigs = map[string]freezerTableConfig{ - ChainFreezerHeaderTable: {noSnappy: false, prunable: false}, - ChainFreezerHashTable: {noSnappy: true, prunable: false}, - ChainFreezerBodiesTable: {noSnappy: false, prunable: true}, - ChainFreezerReceiptTable: {noSnappy: false, prunable: true}, + ChainFreezerHeaderTable: {noSnappy: false}, + ChainFreezerHashTable: {noSnappy: true}, + ChainFreezerBodiesTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, + ChainFreezerReceiptTable: {noSnappy: false, tailGroup: ChainFreezerBlockDataGroup}, + ChainFreezerBALTable: {noSnappy: false, tailGroup: ChainFreezerBALGroup}, } // freezerTableConfig contains the settings for a freezer table. type freezerTableConfig struct { - noSnappy bool // disables item compression - prunable bool // true for tables that can be pruned by TruncateTail + // noSnappy disables item compression when true. + noSnappy bool + + // tailGroup names a logical group of tables that share the same tail + // position. Tables in the same group are pruned together and must agree + // on their tail. An empty value means the table is not prunable; its + // tail is always 0. + tailGroup string } const ( @@ -66,13 +90,17 @@ const ( stateHistoryStorageData = "storage.data" ) +// DefaultHistoryGroup is the tail group shared by all state/trienode history +// tables with tail pruning enabled. +const DefaultHistoryGroup = "history" + // stateFreezerTableConfigs configures the settings for tables in the state freezer. var stateFreezerTableConfigs = map[string]freezerTableConfig{ - stateHistoryMeta: {noSnappy: true, prunable: true}, - stateHistoryAccountIndex: {noSnappy: false, prunable: true}, - stateHistoryStorageIndex: {noSnappy: false, prunable: true}, - stateHistoryAccountData: {noSnappy: false, prunable: true}, - stateHistoryStorageData: {noSnappy: false, prunable: true}, + stateHistoryMeta: {noSnappy: true, tailGroup: DefaultHistoryGroup}, + stateHistoryAccountIndex: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryStorageIndex: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryAccountData: {noSnappy: false, tailGroup: DefaultHistoryGroup}, + stateHistoryStorageData: {noSnappy: false, tailGroup: DefaultHistoryGroup}, } const ( @@ -83,13 +111,13 @@ const ( // trienodeFreezerTableConfigs configures the settings for tables in the trienode freezer. var trienodeFreezerTableConfigs = map[string]freezerTableConfig{ - trienodeHistoryHeaderTable: {noSnappy: false, prunable: true}, + trienodeHistoryHeaderTable: {noSnappy: false, tailGroup: DefaultHistoryGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryKeySectionTable: {noSnappy: true, prunable: true}, + trienodeHistoryKeySectionTable: {noSnappy: true, tailGroup: DefaultHistoryGroup}, // Disable snappy compression to allow efficient partial read. - trienodeHistoryValueSectionTable: {noSnappy: true, prunable: true}, + trienodeHistoryValueSectionTable: {noSnappy: true, tailGroup: DefaultHistoryGroup}, } // The list of identifiers of ancient stores. diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 8c6b18df08..32d5eeb90b 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -24,24 +24,23 @@ import ( "github.com/ethereum/go-ethereum/ethdb" ) -type tableSize struct { - name string - size common.StorageSize +type tableInfo struct { + name string + size common.StorageSize + count uint64 } // freezerInfo contains the basic information of the freezer. type freezerInfo struct { - name string // The identifier of freezer - head uint64 // The number of last stored item in the freezer - tail uint64 // The number of first stored item in the freezer - count uint64 // The number of stored items in the freezer - sizes []tableSize // The storage size per table + name string // The identifier of freezer + head uint64 // The number of last stored item in the freezer + tables []tableInfo // Per-table storage size and item count } // size returns the storage size of the entire freezer. func (info *freezerInfo) size() common.StorageSize { var total common.StorageSize - for _, table := range info.sizes { + for _, table := range info.tables { total += table.size } return total @@ -49,35 +48,41 @@ func (info *freezerInfo) size() common.StorageSize { func inspect(name string, order map[string]freezerTableConfig, reader ethdb.AncientReader) (freezerInfo, error) { info := freezerInfo{name: name} - for t := range order { - size, err := reader.AncientSize(t) - if err != nil { - return freezerInfo{}, err - } - info.sizes = append(info.sizes, tableSize{name: t, size: common.StorageSize(size)}) - } - // Retrieve the number of last stored item + + // Retrieve the number of last stored item. ancients, err := reader.Ancients() if err != nil { return freezerInfo{}, err } if ancients > 0 { info.head = ancients - 1 - } else { - info.head = 0 } - - // Retrieve the number of first stored item - tail, err := reader.Tail() - if err != nil { - return freezerInfo{}, err + // Resolve per-group tails so each table can report its own item count. + groupTails := make(map[string]uint64) + for _, cfg := range order { + if cfg.tailGroup == "" { + continue + } + if _, ok := groupTails[cfg.tailGroup]; ok { + continue + } + t, err := reader.Tail(cfg.tailGroup) + if err != nil { + return freezerInfo{}, err + } + groupTails[cfg.tailGroup] = t } - info.tail = tail - - if ancients == 0 { - info.count = 0 - } else { - info.count = info.head - info.tail + 1 + for t, cfg := range order { + size, err := reader.AncientSize(t) + if err != nil { + return freezerInfo{}, err + } + var count uint64 + if ancients > 0 { + tail := groupTails[cfg.tailGroup] // 0 for non-prunable tables + count = ancients - tail + } + info.tables = append(info.tables, tableInfo{name: t, size: common.StorageSize(size), count: count}) } return info, nil } diff --git a/core/rawdb/ancienttest/testsuite.go b/core/rawdb/ancienttest/testsuite.go index eb66645a3a..a84053e604 100644 --- a/core/rawdb/ancienttest/testsuite.go +++ b/core/rawdb/ancienttest/testsuite.go @@ -25,6 +25,11 @@ import ( "github.com/ethereum/go-ethereum/internal/testrand" ) +// TailGroup is the tail group used by tables created in this test suite. The +// store factory passed to TestAncientSuite must wire its tables to this group +// so that the suite can query the freezer's tail consistently. +const TailGroup = "test" + // TestAncientSuite runs a suite of tests against an ancient database // implementation. func TestAncientSuite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { @@ -58,11 +63,11 @@ func basicRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Test basic tail and head retrievals - tail, err := db.Tail() + tail, err := db.Tail(TailGroup) if err != nil || tail != 10 { t.Fatal("Failed to retrieve tail") } @@ -123,7 +128,7 @@ func batchRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Test the items in range should be reachable @@ -262,12 +267,12 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { } // Write should work after truncating from tail but over the head - db.TruncateTail(200) + db.TruncateTail(TailGroup, 200) head, err := db.Ancients() if err != nil { t.Fatalf("Failed to retrieve head ancients %v", err) } - tail, err := db.Tail() + tail, err := db.Tail(TailGroup) if err != nil { t.Fatalf("Failed to retrieve tail ancients %v", err) } @@ -293,7 +298,7 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) { if err != nil { t.Fatalf("Failed to retrieve head ancients %v", err) } - tail, err = db.Tail() + tail, err = db.Tail(TailGroup) if err != nil { t.Fatalf("Failed to retrieve tail ancients %v", err) } @@ -351,7 +356,7 @@ func TestResettableAncientSuite(t *testing.T, newFn func(kinds []string) ethdb.R }); err != nil { t.Fatalf("Failed to write ancient data %v", err) } - db.TruncateTail(10) + db.TruncateTail(TailGroup, 10) db.TruncateHead(90) // Ancient write should work after resetting diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index d33f7ce33d..4fe85d843b 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -45,9 +45,7 @@ const ( // key-value database to flat files for saving space on live database. type chainFreezer struct { ancients ethdb.AncientStore // Ancient store for storing cold chain segment - - // Optional Era database used as a backup for the pruned chain. - eradb *eradb.Store + eradb *eradb.Store // Optional Era database used as a backup for the pruned chain quit chan struct{} wg sync.WaitGroup @@ -327,6 +325,16 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash if len(receipts) == 0 { return fmt.Errorf("block receipts missing, can't freeze block %d", number) } + // An empty block access list is allowed and may occur in multiple + // scenarios, such as: + // - pre-Amsterdam blocks + // - post-Amsterdam blocks with the BAL absent (e.g. pruned by network) + // - post-Amsterdam blocks with an explicitly empty BAL + // + // In these cases, a nil entry will be stored in the BAL table as the + // absence placeholder. + bals := ReadAccessListRLP(nfdb, hash, number) + // Write to the batch. if err := op.AppendRaw(ChainFreezerHashTable, number, hash[:]); err != nil { return fmt.Errorf("can't write hash to Freezer: %v", err) @@ -340,6 +348,9 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash if err := op.AppendRaw(ChainFreezerReceiptTable, number, receipts); err != nil { return fmt.Errorf("can't write receipts to Freezer: %v", err) } + if err := op.AppendRaw(ChainFreezerBALTable, number, bals); err != nil { + return fmt.Errorf("can't write bals to Freezer: %v", err) + } hashes = append(hashes, hash) } return nil @@ -354,7 +365,11 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { if kind == ChainFreezerHeaderTable || kind == ChainFreezerHashTable { return f.ancients.Ancient(kind, number) } - tail, err := f.ancients.Tail() + group, err := tableTailGroup(kind) + if err != nil { + return nil, err + } + tail, err := f.ancients.Tail(group) if err != nil { return nil, err } @@ -371,10 +386,20 @@ func (f *chainFreezer) Ancient(kind string, number uint64) ([]byte, error) { return f.eradb.GetRawBody(number) case ChainFreezerReceiptTable: return f.eradb.GetRawReceipts(number) + case ChainFreezerBALTable: + return nil, errOutOfBounds } return nil, errUnknownTable } +// tableTailGroup returns the tail group identifier for a chain freezer table. +func tableTailGroup(kind string) (string, error) { + if cfg, ok := chainFreezerTableConfigs[kind]; ok { + return cfg.tailGroup, nil + } + return "", errUnknownTable +} + // ReadAncients executes an operation while preventing mutations to the freezer, // i.e. if fn performs multiple reads, they will be consistent with each other. func (f *chainFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { @@ -391,8 +416,8 @@ func (f *chainFreezer) Ancients() (uint64, error) { return f.ancients.Ancients() } -func (f *chainFreezer) Tail() (uint64, error) { - return f.ancients.Tail() +func (f *chainFreezer) Tail(group string) (uint64, error) { + return f.ancients.Tail(group) } func (f *chainFreezer) AncientSize(kind string) (uint64, error) { @@ -415,8 +440,8 @@ func (f *chainFreezer) TruncateHead(items uint64) (uint64, error) { return f.ancients.TruncateHead(items) } -func (f *chainFreezer) TruncateTail(items uint64) (uint64, error) { - return f.ancients.TruncateTail(items) +func (f *chainFreezer) TruncateTail(group string, items uint64) (uint64, error) { + return f.ancients.TruncateTail(group, items) } func (f *chainFreezer) SyncAncient() error { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 39e1a64e5a..57abcdb25d 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -113,7 +113,7 @@ func (db *nofreezedb) Ancients() (uint64, error) { } // Tail returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) Tail() (uint64, error) { +func (db *nofreezedb) Tail(group string) (uint64, error) { return 0, errNotSupported } @@ -133,7 +133,7 @@ func (db *nofreezedb) TruncateHead(items uint64) (uint64, error) { } // TruncateTail returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) TruncateTail(items uint64) (uint64, error) { +func (db *nofreezedb) TruncateTail(group string, items uint64) (uint64, error) { return 0, errNotSupported } @@ -658,12 +658,12 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { return err } for _, ancient := range ancients { - for _, table := range ancient.sizes { + for _, table := range ancient.tables { stats = append(stats, []string{ fmt.Sprintf("Ancient store (%s)", strings.Title(ancient.name)), strings.Title(table.name), table.size.String(), - fmt.Sprintf("%d", ancient.count), + fmt.Sprintf("%d", table.count), }) } total.Add(uint64(ancient.size())) diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 0e2f86d6ed..e5c7548aef 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -59,8 +59,8 @@ const freezerTableSize = 2 * 1000 * 1000 * 1000 // - The in-order data ensures that disk reads are always optimized. type Freezer struct { datadir string - head atomic.Uint64 // Number of items stored (including items removed from tail) - tail atomic.Uint64 // Number of the first stored item in the freezer + head atomic.Uint64 // Number of items stored (including items removed from tail) + tails map[string]*atomic.Uint64 // Per-group tail cache, keyed by tail group name // This lock synchronizes writers and the truncate operation, as well as // the "atomic" (batched) read operations. @@ -77,8 +77,8 @@ type Freezer struct { // data according to the given parameters. // // The 'tables' argument defines the freezer tables and their configuration. -// Each value is a freezerTableConfig specifying whether snappy compression is -// disabled (noSnappy) and whether the table is prunable (prunable). +// Each value is a freezerTableConfig describing whether Snappy compression +// is disabled (noSnappy) and which tail group the table belongs to. func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) { // Create the initial freezer object var ( @@ -118,6 +118,7 @@ func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize ui datadir: datadir, readonly: readonly, tables: make(map[string]*freezerTable), + tails: make(map[string]*atomic.Uint64), instanceLock: lock, } @@ -216,9 +217,19 @@ func (f *Freezer) Ancients() (uint64, error) { return f.head.Load(), nil } -// Tail returns the number of first stored item in the freezer. -func (f *Freezer) Tail() (uint64, error) { - return f.tail.Load(), nil +// Tail returns the lowest accessible item index for the given tail group. +// All tables sharing this group agree on the tail; an empty group name +// refers to non-prunable tables and always returns 0. Unknown groups return +// an error. +func (f *Freezer) Tail(group string) (uint64, error) { + if group == "" { + return 0, nil + } + tail, ok := f.tails[group] + if !ok { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + return tail.Load(), nil } // AncientSize returns the ancient size of the specified category. @@ -299,33 +310,43 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { return oitems, nil } -// TruncateTail discards all data below the specified threshold. Note that only -// 'prunable' tables will be truncated. -func (f *Freezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards all data below the specified threshold across every +// table that belongs to the named tail group. Tables that are already past +// the threshold are left untouched. The previous tail of the group is +// returned. An empty group name or an unknown group name returns an error. +func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) { if f.readonly { return 0, errReadOnly } + if group == "" { + return 0, errors.New("empty tail group") + } + cached, ok := f.tails[group] + if !ok { + return 0, fmt.Errorf("unknown tail group: %q", group) + } f.writeLock.Lock() defer f.writeLock.Unlock() - old := f.tail.Load() - if old >= tail { - return old, nil + prev := cached.Load() + if prev >= tail { + return prev, nil } for _, table := range f.tables { - if table.config.prunable { - if err := table.truncateTail(tail); err != nil { - return 0, err - } + if table.config.tailGroup != group { + continue + } + if err := table.truncateTail(tail); err != nil { + return 0, err } } - f.tail.Store(tail) + cached.Store(tail) - // Update the head if the requested tail exceeds the current head + // Update the head if the requested tail exceeds the current head. if f.head.Load() < tail { f.head.Store(tail) } - return old, nil + return prev, nil } // SyncAncient flushes all data tables to disk. @@ -342,84 +363,133 @@ func (f *Freezer) SyncAncient() error { return nil } -// validate checks that every table has the same boundary. -// Used instead of `repair` in readonly mode. +// validate checks that every table has the same head and that tables sharing +// a tail group also share a tail. Used instead of `repair` in readonly mode. func (f *Freezer) validate() error { if len(f.tables) == 0 { return nil } var ( - head uint64 - prunedTail *uint64 + head uint64 + headSet bool + tails = make(map[string]uint64) ) - // get any head value - for _, table := range f.tables { - head = table.items.Load() - break - } for kind, table := range f.tables { - // all tables have to have the same head - if head != table.items.Load() { - return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, table.items.Load(), head) + // A freshly added table is empty and has not yet been aligned to the + // common head, skip the error here. + // + // Tradeoff: + // It loosens corruption detection slightly: a table that lost its data + // and now reports items == 0 would be treated as "freshly added" rather + // than flagged. It's the tradeoff we accept. + items := table.items.Load() + if items == 0 { + continue } - if !table.config.prunable { - // non-prunable tables have to start at 0 + // Validate the table head + if !headSet { + head = items + headSet = true + } else if items != head { + return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, items, head) + } + // Validate the table tail + if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { return fmt.Errorf("non-prunable freezer table '%s' has a non-zero tail: %d", kind, table.itemHidden.Load()) } + continue + } + hidden := table.itemHidden.Load() + if t, ok := tails[table.config.tailGroup]; ok { + if t != hidden { + return fmt.Errorf("freezer table %s has differing tail in group %q: %d != %d", kind, table.config.tailGroup, hidden, t) + } } else { - // prunable tables have to have the same length - if prunedTail == nil { - tmp := table.itemHidden.Load() - prunedTail = &tmp - } - if *prunedTail != table.itemHidden.Load() { - return fmt.Errorf("freezer table %s has differing tail: %d != %d", kind, table.itemHidden.Load(), *prunedTail) - } + tails[table.config.tailGroup] = hidden } } - - if prunedTail == nil { - tmp := uint64(0) - prunedTail = &tmp - } - f.head.Store(head) - f.tail.Store(*prunedTail) + + for group, tail := range tails { + counter := new(atomic.Uint64) + counter.Store(tail) + f.tails[group] = counter + } return nil } -// repair truncates all data tables to the same length. +// repair brings every table into a consistent state. The common head is taken +// as the minimum item count among non-empty tables; freshly added empty tables +// are fast-forwarded to that head via tail truncation. Within each tail group +// the maximum tail wins, and prunable tables are truncated to it. func (f *Freezer) repair() error { + // Determine the common head from non-empty tables. Empty tables are + // excluded so that a freshly added table cannot drag the existing head + // down to zero on first cold-start. var ( - head = uint64(math.MaxUint64) - prunedTail = uint64(0) + hasNonEmpty bool + head uint64 = math.MaxUint64 ) - // get the minimal head and the maximum tail for _, table := range f.tables { - head = min(head, table.items.Load()) - prunedTail = max(prunedTail, table.itemHidden.Load()) + if table.items.Load() == 0 { + continue + } + if items := table.items.Load(); items < head { + head = items + } + hasNonEmpty = true } - // apply the pruning - for kind, table := range f.tables { - // all tables need to have the same head + if !hasNonEmpty { + head = 0 + } + // Align newly added empty tables to the common head. truncateTail + // internally calls resetTo when the requested tail exceeds the current + // head, which is exactly what we need here. + if head > 0 { + for _, table := range f.tables { + if table.items.Load() == 0 { + if err := table.truncateTail(head); err != nil { + return err + } + } + } + } + // Truncate every table to the common head. + for _, table := range f.tables { if err := table.truncateHead(head); err != nil { return err } - if !table.config.prunable { - // non-prunable tables have to start at 0 + } + // Per-group tail alignment: take the maximum tail in each group and apply + // it to all members. Non-prunable tables must remain at tail 0. + tails := make(map[string]uint64) + for kind, table := range f.tables { + if table.config.tailGroup == "" { if table.itemHidden.Load() != 0 { panic(fmt.Sprintf("non-prunable freezer table %s has non-zero tail: %v", kind, table.itemHidden.Load())) } - } else { - // prunable tables have to have the same length - if err := table.truncateTail(prunedTail); err != nil { - return err - } + continue + } + hidden := table.itemHidden.Load() + if t, ok := tails[table.config.tailGroup]; !ok || hidden > t { + tails[table.config.tailGroup] = hidden + } + } + for _, table := range f.tables { + if table.config.tailGroup == "" { + continue + } + if err := table.truncateTail(tails[table.config.tailGroup]); err != nil { + return err } } - f.head.Store(head) - f.tail.Store(prunedTail) + + for group, tail := range tails { + counter := new(atomic.Uint64) + counter.Store(tail) + f.tails[group] = counter + } return nil } diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index ec6d4b22e2..b2d8912ee2 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -228,7 +228,7 @@ func (b *memoryBatch) commit(freezer *MemoryFreezer) (items uint64, writeSize in // interface and can be used along with ephemeral key-value store. type MemoryFreezer struct { items uint64 // Number of items stored - tail uint64 // Number of the first stored item in the freezer + tails map[string]uint64 // Per-group tail cache; access serialized by lock readonly bool // Flag if the freezer is only for reading lock sync.RWMutex // Lock to protect fields tables map[string]*memoryTable // Tables for storing everything @@ -237,14 +237,21 @@ type MemoryFreezer struct { // NewMemoryFreezer initializes an in-memory freezer instance. func NewMemoryFreezer(readonly bool, tableName map[string]freezerTableConfig) *MemoryFreezer { - tables := make(map[string]*memoryTable) + var ( + tables = make(map[string]*memoryTable) + tails = make(map[string]uint64) + ) for name, cfg := range tableName { tables[name] = newMemoryTable(name, cfg) + if cfg.tailGroup != "" { + tails[cfg.tailGroup] = 0 + } } return &MemoryFreezer{ writeBatch: newMemoryBatch(), readonly: readonly, tables: tables, + tails: tails, } } @@ -289,13 +296,21 @@ func (f *MemoryFreezer) Ancients() (uint64, error) { return f.items, nil } -// Tail returns the number of first stored item in the freezer. -// This number can also be interpreted as the total deleted item numbers. -func (f *MemoryFreezer) Tail() (uint64, error) { +// Tail returns the lowest accessible item index for the given tail group. +// All tables sharing the group agree on the tail; an empty group name +// refers to non-prunable tables and always returns 0. +func (f *MemoryFreezer) Tail(group string) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.tail, nil + if group == "" { + return 0, nil + } + tail, ok := f.tails[group] + if !ok { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + return tail, nil } // AncientSize returns the ancient size of the specified category. @@ -375,32 +390,39 @@ func (f *MemoryFreezer) TruncateHead(items uint64) (uint64, error) { return old, nil } -// TruncateTail discards all data below the provided threshold number. -// Note this will only truncate 'prunable' tables. Block headers and canonical -// hashes cannot be truncated at this time. -func (f *MemoryFreezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards all data below the provided threshold across every +// table that belongs to the named tail group. Tables already past the +// threshold are left untouched. The previous tail of the group is returned. +func (f *MemoryFreezer) TruncateTail(group string, tail uint64) (uint64, error) { f.lock.Lock() defer f.lock.Unlock() if f.readonly { return 0, errReadOnly } - old := f.tail - if old >= tail { - return old, nil + if group == "" { + return 0, errors.New("empty tail group") + } + prev, ok := f.tails[group] + if !ok { + return 0, fmt.Errorf("unknown tail group: %q", group) + } + if prev >= tail { + return prev, nil } for _, table := range f.tables { - if table.config.prunable { - if err := table.truncateTail(tail); err != nil { - return 0, err - } + if table.config.tailGroup != group { + continue + } + if err := table.truncateTail(tail); err != nil { + return 0, err } } - f.tail = tail + f.tails[group] = tail if f.items < tail { f.items = tail } - return old, nil + return prev, nil } // SyncAncient flushes all data tables to disk. @@ -426,11 +448,16 @@ func (f *MemoryFreezer) Reset() error { defer f.lock.Unlock() tables := make(map[string]*memoryTable) + tails := make(map[string]uint64) for name, table := range f.tables { tables[name] = newMemoryTable(name, table.config) + if table.config.tailGroup != "" { + tails[table.config.tailGroup] = 0 + } } f.tables = tables - f.items, f.tail = 0, 0 + f.tails = tails + f.items = 0 return nil } diff --git a/core/rawdb/freezer_memory_test.go b/core/rawdb/freezer_memory_test.go index 4bd31d8027..6baa1765b1 100644 --- a/core/rawdb/freezer_memory_test.go +++ b/core/rawdb/freezer_memory_test.go @@ -28,8 +28,8 @@ func TestMemoryFreezer(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } return NewMemoryFreezer(false, tables) @@ -38,8 +38,8 @@ func TestMemoryFreezer(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } return NewMemoryFreezer(false, tables) diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 5494a648c8..48ab502eb7 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -143,12 +143,12 @@ func (f *resettableFreezer) Ancients() (uint64, error) { return f.freezer.Ancients() } -// Tail returns the number of first stored item in the freezer. -func (f *resettableFreezer) Tail() (uint64, error) { +// Tail returns the lowest accessible item index for the given tail group. +func (f *resettableFreezer) Tail(group string) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.freezer.Tail() + return f.freezer.Tail(group) } // AncientSize returns the ancient size of the specified category. @@ -185,13 +185,13 @@ func (f *resettableFreezer) TruncateHead(items uint64) (uint64, error) { return f.freezer.TruncateHead(items) } -// TruncateTail discards any recent data below the provided threshold number. -// It returns the previous value -func (f *resettableFreezer) TruncateTail(tail uint64) (uint64, error) { +// TruncateTail discards data below the provided threshold for the named tail +// group. It returns the previous tail of the group. +func (f *resettableFreezer) TruncateTail(group string, tail uint64) (uint64, error) { f.lock.RLock() defer f.lock.RUnlock() - return f.freezer.TruncateTail(tail) + return f.freezer.TruncateTail(group, tail) } // SyncAncient flushes all data tables to disk. diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index fab3319a2a..0fc4f90011 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -371,6 +371,133 @@ func checkAncientCount(t *testing.T, f *Freezer, kind string, n uint64) { } } +// TestChainFreezerBALAlignment exercises the new-table alignment path: a chain +// freezer is first opened with the legacy table set (no BAL), populated with a +// few blocks and closed. It is then re-opened with the full chain freezer +// table set (which includes the BAL column). The expectation is that the BAL +// table is fast-forwarded to the existing head without disturbing the body / +// receipt tables, and that subsequent writes append cleanly across all tables. +func TestChainFreezerBALAlignment(t *testing.T) { + dir := t.TempDir() + + // Build a "legacy" subset of the chain freezer table set, omitting BAL. + legacyTables := make(map[string]freezerTableConfig) + for name, cfg := range chainFreezerTableConfigs { + if name == ChainFreezerBALTable { + continue + } + legacyTables[name] = cfg + } + + // First open: legacy config. Fill in `items` blocks of dummy data. + const items = uint64(10) + payload := bytes.Repeat([]byte{0xab}, 64) + + f, err := NewFreezer(dir, "", false, 2049, legacyTables) + if err != nil { + t.Fatalf("can't open legacy freezer: %v", err) + } + if _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := uint64(0); i < items; i++ { + if err := op.AppendRaw(ChainFreezerHashTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerHeaderTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBodiesTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerReceiptTable, i, payload); err != nil { + return err + } + } + return nil + }); err != nil { + t.Fatalf("legacy write failed: %v", err) + } + if got, _ := f.Ancients(); got != items { + t.Fatalf("legacy head: got %d, want %d", got, items) + } + require.NoError(t, f.Close()) + + // Re-open with the full chain freezer table set, which now includes BAL. + // repair() should detect the empty BAL table and fast-forward it to the + // existing head rather than truncating everyone down to zero. + f, err = NewFreezer(dir, "", false, 2049, chainFreezerTableConfigs) + if err != nil { + t.Fatalf("can't re-open freezer with BAL added: %v", err) + } + defer f.Close() + + // The head must be preserved. + if got, _ := f.Ancients(); got != items { + t.Fatalf("head after re-open: got %d, want %d", got, items) + } + // Existing data must still be readable in full. + for i := uint64(0); i < items; i++ { + for _, kind := range []string{ + ChainFreezerHashTable, ChainFreezerHeaderTable, + ChainFreezerBodiesTable, ChainFreezerReceiptTable, + } { + got, err := f.Ancient(kind, i) + if err != nil { + t.Fatalf("read %s[%d]: %v", kind, i, err) + } + if !bytes.Equal(got, payload) { + t.Fatalf("read %s[%d]: payload mismatch", kind, i) + } + } + } + // The block-data tail must be unchanged (no spurious tail bump). + if tail, err := f.Tail(ChainFreezerBlockDataGroup); err != nil || tail != 0 { + t.Fatalf("blockdata tail: got %d (err %v), want 0", tail, err) + } + // The BAL tail should equal the head — the table is empty but aligned. + if tail, err := f.Tail(ChainFreezerBALGroup); err != nil || tail != items { + t.Fatalf("BAL tail: got %d (err %v), want %d", tail, err, items) + } + // Reads to BAL for any pre-alignment block must report out-of-bounds. + for i := uint64(0); i < items; i++ { + if _, err := f.Ancient(ChainFreezerBALTable, i); err == nil { + t.Fatalf("reading BAL[%d] succeeded; want error (out of bounds)", i) + } + } + // A subsequent batch must append uniformly to every table, BAL included. + balPayload := []byte("real-bal") + if _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + i := items + if err := op.AppendRaw(ChainFreezerHashTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerHeaderTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBodiesTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerReceiptTable, i, payload); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBALTable, i, balPayload); err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("post-alignment write failed: %v", err) + } + if got, _ := f.Ancients(); got != items+1 { + t.Fatalf("head after post-alignment write: got %d, want %d", got, items+1) + } + got, err := f.Ancient(ChainFreezerBALTable, items) + if err != nil { + t.Fatalf("BAL[%d]: %v", items, err) + } + if !bytes.Equal(got, balPayload) { + t.Fatalf("BAL[%d]: got %x, want %x", items, got, balPayload) + } +} + func TestFreezerCloseSync(t *testing.T) { t.Parallel() f, _ := newFreezerForTesting(t, map[string]freezerTableConfig{"a": {noSnappy: true}, "b": {noSnappy: true}}) @@ -398,8 +525,8 @@ func TestFreezerSuite(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } f, _ := newFreezerForTesting(t, tables) @@ -409,8 +536,8 @@ func TestFreezerSuite(t *testing.T) { tables := make(map[string]freezerTableConfig) for _, kind := range kinds { tables[kind] = freezerTableConfig{ - noSnappy: true, - prunable: true, + noSnappy: true, + tailGroup: ancienttest.TailGroup, } } f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, tables) diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 407a619c9f..7640f1cf43 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -76,8 +76,8 @@ func (t *table) Ancients() (uint64, error) { // Tail is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) Tail() (uint64, error) { - return t.db.Tail() +func (t *table) Tail(group string) (uint64, error) { + return t.db.Tail(group) } // AncientSize is a noop passthrough that just forwards the request to the underlying @@ -103,8 +103,8 @@ func (t *table) TruncateHead(items uint64) (uint64, error) { // TruncateTail is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) TruncateTail(items uint64) (uint64, error) { - return t.db.TruncateTail(items) +func (t *table) TruncateTail(group string, items uint64) (uint64, error) { + return t.db.TruncateTail(group, items) } // SyncAncient is a noop passthrough that just forwards the request to the underlying diff --git a/ethdb/database.go b/ethdb/database.go index 534fcad4fc..a5953bd42a 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -128,9 +128,12 @@ type AncientReaderOp interface { // Ancients returns the ancient item numbers in the ancient store. Ancients() (uint64, error) - // Tail returns the number of first stored item in the ancient store. - // This number can also be interpreted as the total deleted items. - Tail() (uint64, error) + // Tail returns the lowest accessible item index for the given tail group. + // This number can also be interpreted as the total deleted items in the + // group. Tables sharing a group are pruned together and therefore agree + // on the value. An empty group name refers to non-prunable tables and + // always returns 0. + Tail(group string) (uint64, error) // AncientSize returns the ancient size of the specified category. AncientSize(kind string) (uint64, error) @@ -159,14 +162,16 @@ type AncientWriter interface { // After the truncation, the latest item can be accessed it item_n-1(start from 0). TruncateHead(n uint64) (uint64, error) - // TruncateTail discards the first n ancient data from the ancient store. The already - // deleted items are ignored. After the truncation, the earliest item can be accessed - // is item_n(start from 0). The deleted items may not be removed from the ancient store - // immediately, but only when the accumulated deleted data reach the threshold then - // will be removed all together. + // TruncateTail discards the first n items from every table belonging to + // the named tail group. Already-deleted items are ignored. After the + // truncation, the earliest accessible item in the group is item_n + // (starting from 0). Deleted items may not be removed from disk + // immediately, but only once the accumulated deleted data reaches the + // threshold, at which point they are removed all together. // - // Note that data marked as non-prunable will still be retained and remain accessible. - TruncateTail(n uint64) (uint64, error) + // The previous tail of the group is returned. Tables outside the group + // (including non-prunable ones) are untouched. + TruncateTail(group string, n uint64) (uint64, error) } // AncientWriteOp is given to the function argument of ModifyAncients. diff --git a/ethdb/remotedb/remotedb.go b/ethdb/remotedb/remotedb.go index 0d0d854fe4..87e45cf4a0 100644 --- a/ethdb/remotedb/remotedb.go +++ b/ethdb/remotedb/remotedb.go @@ -67,7 +67,7 @@ func (db *Database) Ancients() (uint64, error) { return resp, err } -func (db *Database) Tail() (uint64, error) { +func (db *Database) Tail(group string) (uint64, error) { panic("not supported") } @@ -99,7 +99,7 @@ func (db *Database) TruncateHead(n uint64) (uint64, error) { panic("not supported") } -func (db *Database) TruncateTail(n uint64) (uint64, error) { +func (db *Database) TruncateTail(group string, n uint64) (uint64, error) { panic("not supported") } diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 50c7279d0e..8c0a751932 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -378,7 +378,7 @@ func (dl *diskLayer) writeHistory(typ historyType, diff *diffLayer) (bool, error if limit == 0 { return false, nil } - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return false, err } // firstID = tail+1 diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index 7f5b0e35ba..55ec29e4f0 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -273,7 +273,7 @@ func truncateFromHead(store ethdb.AncientStore, typ historyType, nhead uint64) ( if err != nil { return 0, err } - otail, err := store.Tail() + otail, err := store.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } @@ -303,7 +303,7 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if err != nil { return 0, err } - otail, err := store.Tail() + otail, err := store.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } @@ -315,7 +315,7 @@ func truncateFromTail(store ethdb.AncientStore, typ historyType, ntail uint64) ( if otail == ntail { return 0, nil } - otail, err = store.TruncateTail(ntail) + otail, err = store.TruncateTail(rawdb.DefaultHistoryGroup, ntail) if err != nil { return 0, err } @@ -430,7 +430,7 @@ func repairHistory(db ethdb.Database, isUBT bool, readOnly bool, stateID uint64, truncTo = min(truncTo, thead) } else { if thead == 0 { - _, err = trienodes.TruncateTail(stateID) + _, err = trienodes.TruncateTail(rawdb.DefaultHistoryGroup, stateID) if err != nil { return nil, nil, err } diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index 9b215b917f..3789fae19b 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -542,7 +542,7 @@ func (i *indexIniter) run(recover bool) { // next returns the ID of the next state history to be indexed. func (i *indexIniter) next() (uint64, error) { - tail, err := i.freezer.Tail() + tail, err := i.freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } diff --git a/triedb/pathdb/history_inspect.go b/triedb/pathdb/history_inspect.go index 74b8bb8df2..8b5624f441 100644 --- a/triedb/pathdb/history_inspect.go +++ b/triedb/pathdb/history_inspect.go @@ -21,6 +21,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" @@ -37,7 +38,7 @@ type HistoryStats struct { // sanitizeRange limits the given range to fit within the local history store. func sanitizeRange(start, end uint64, freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, 0, err } @@ -132,7 +133,7 @@ func storageHistory(freezer ethdb.AncientReader, address common.Address, slot co // historyRange returns the block number range of local state histories. func historyRange(freezer ethdb.AncientReader) (uint64, uint64, error) { // Load the id of the first history object in local store. - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, 0, err } diff --git a/triedb/pathdb/history_reader.go b/triedb/pathdb/history_reader.go index 4ae1fb36cb..1652e412eb 100644 --- a/triedb/pathdb/history_reader.go +++ b/triedb/pathdb/history_reader.go @@ -470,7 +470,7 @@ func checkStateAvail(state stateIdent, exptyp historyType, freezer ethdb.Ancient return 0, fmt.Errorf("unsupported history type: %d, want: %v", toHistoryType(state.typ), exptyp) } // firstID = tail+1 - tail, err := freezer.Tail() + tail, err := freezer.Tail(rawdb.DefaultHistoryGroup) if err != nil { return 0, err } diff --git a/triedb/pathdb/history_state_test.go b/triedb/pathdb/history_state_test.go index 4046fb9640..5c3026a571 100644 --- a/triedb/pathdb/history_state_test.go +++ b/triedb/pathdb/history_state_test.go @@ -237,7 +237,7 @@ func TestTruncateOutOfRange(t *testing.T) { // Ensure of-out-range truncations are rejected correctly. head, _ := freezer.Ancients() - tail, _ := freezer.Tail() + tail, _ := freezer.Tail(rawdb.DefaultHistoryGroup) cases := []struct { mode int From 02dd66dfc0b1551bb55b95ae0b914a58441ce2ac Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 2 Jun 2026 09:46:11 +0800 Subject: [PATCH 59/76] core/txpool/locals: fix data race (#35096) Supersedes #35060 ``` go test -race ./core/txpool/locals/ ok github.com/ethereum/go-ethereum/core/txpool/locals 1.782s ``` --- core/txpool/locals/tx_tracker.go | 33 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/core/txpool/locals/tx_tracker.go b/core/txpool/locals/tx_tracker.go index 66f3248105..59626a5e66 100644 --- a/core/txpool/locals/tx_tracker.go +++ b/core/txpool/locals/tx_tracker.go @@ -173,6 +173,17 @@ func (tracker *TxTracker) recheck(journalCheck bool) []*types.Transaction { // Start is called after all services have been constructed and the networking // layer was also initialized to spawn any goroutines required by the service. func (tracker *TxTracker) Start() error { + if tracker.journal != nil { + tracker.journal.load(func(transactions []*types.Transaction) []error { + tracker.TrackAll(transactions) + return nil + }) + // Setup the writer for the upcoming transactions + if err := tracker.journal.setupWriter(); err != nil { + log.Error("Failed to setup the journal writer", "err", err) + return err + } + } tracker.wg.Add(1) go tracker.loop() return nil @@ -184,25 +195,19 @@ func (tracker *TxTracker) Start() error { func (tracker *TxTracker) Stop() error { close(tracker.shutdownCh) tracker.wg.Wait() - return nil + + tracker.mu.Lock() + var err error + if tracker.journal != nil { + err = tracker.journal.close() + } + tracker.mu.Unlock() + return err } func (tracker *TxTracker) loop() { defer tracker.wg.Done() - if tracker.journal != nil { - tracker.journal.load(func(transactions []*types.Transaction) []error { - tracker.TrackAll(transactions) - return nil - }) - - // Setup the writer for the upcoming transactions - if err := tracker.journal.setupWriter(); err != nil { - log.Error("Failed to setup the journal writer", "err", err) - return - } - defer tracker.journal.close() - } var ( lastJournal = time.Now() timer = time.NewTimer(10 * time.Second) // Do initial check after 10 seconds, do rechecks more seldom. From 77a28164685bf694d2efabc38dbd1409b2e923ec Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Tue, 2 Jun 2026 14:48:26 +0800 Subject: [PATCH 60/76] eth/protocols/snap: introduce snapv2 skeleton (#35098) This PR is a prerequisite for landing snap v2, the BAL-healing snap sync algorithm. It duplicates much of the snap v1 skeleton, which is expected to be deprecated once v2 is enabled. The code duplication is acceptable as a short-term tradeoff, simplifying development and reducing integration complexity. --- eth/protocols/snap/syncv2.go | 1959 +++++++++++++++++++++++++++++ eth/protocols/snap/syncv2_test.go | 1164 +++++++++++++++++ 2 files changed, 3123 insertions(+) create mode 100644 eth/protocols/snap/syncv2.go create mode 100644 eth/protocols/snap/syncv2_test.go diff --git a/eth/protocols/snap/syncv2.go b/eth/protocols/snap/syncv2.go new file mode 100644 index 0000000000..0bbcd9c35f --- /dev/null +++ b/eth/protocols/snap/syncv2.go @@ -0,0 +1,1959 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snap + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + "math/rand" + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p/msgrate" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/trie/trienode" +) + +// accountRequestV2 tracks a pending account range request to ensure responses are +// to actual requests and to validate any security constraints. +// +// Concurrency note: account requests and responses are handled concurrently from +// the main runloop to allow Merkle proof verifications on the peer's thread and +// to drop on invalid response. The request struct must contain all the data to +// construct the response without accessing runloop internals (i.e. task). That +// is only included to allow the runloop to match a response to the task being +// synced without having yet another set of maps. +type accountRequestV2 struct { + peer string // Peer to which this request is assigned + id uint64 // Request ID of this request + time time.Time // Timestamp when the request was sent + + deliver chan *accountResponseV2 // Channel to deliver successful response on + revert chan *accountRequestV2 // Channel to deliver request failure on + cancel chan struct{} // Channel to track sync cancellation + timeout *time.Timer // Timer to track delivery timeout + stale chan struct{} // Channel to signal the request was dropped + + origin common.Hash // First account requested to allow continuation checks + limit common.Hash // Last account requested to allow non-overlapping chunking + + task *accountTaskV2 // Task which this request is filling (only access fields through the runloop!!) +} + +// accountResponseV2 is an already Merkle-verified remote response to an account +// range request. +type accountResponseV2 struct { + task *accountTaskV2 // Task which this request is filling + + hashes []common.Hash // Account hashes in the returned range + accounts []*types.StateAccount // Expanded accounts in the returned range + + cont bool // Whether the account range has a continuation +} + +// bytecodeRequestV2 tracks a pending bytecode request to ensure responses are to +// actual requests and to validate any security constraints. +// +// Concurrency note: bytecode requests and responses are handled concurrently from +// the main runloop to allow Keccak256 hash verifications on the peer's thread and +// to drop on invalid response. The request struct must contain all the data to +// construct the response without accessing runloop internals (i.e. task). That +// is only included to allow the runloop to match a response to the task being +// synced without having yet another set of maps. +type bytecodeRequestV2 struct { + peer string // Peer to which this request is assigned + id uint64 // Request ID of this request + time time.Time // Timestamp when the request was sent + + deliver chan *bytecodeResponseV2 // Channel to deliver successful response on + revert chan *bytecodeRequestV2 // Channel to deliver request failure on + cancel chan struct{} // Channel to track sync cancellation + timeout *time.Timer // Timer to track delivery timeout + stale chan struct{} // Channel to signal the request was dropped + + hashes []common.Hash // Bytecode hashes to validate responses + task *accountTaskV2 // Task which this request is filling (only access fields through the runloop!!) +} + +// bytecodeResponseV2 is an already verified remote response to a bytecode request. +type bytecodeResponseV2 struct { + task *accountTaskV2 // Task which this request is filling + + hashes []common.Hash // Hashes of the bytecode to avoid double hashing + codes [][]byte // Actual bytecodes to store into the database (nil = missing) +} + +// storageRequestV2 tracks a pending storage ranges request to ensure responses are +// to actual requests and to validate any security constraints. +// +// Concurrency note: storage requests and responses are handled concurrently from +// the main runloop to allow Merkle proof verifications on the peer's thread and +// to drop on invalid response. The request struct must contain all the data to +// construct the response without accessing runloop internals (i.e. tasks). That +// is only included to allow the runloop to match a response to the task being +// synced without having yet another set of maps. +type storageRequestV2 struct { + peer string // Peer to which this request is assigned + id uint64 // Request ID of this request + time time.Time // Timestamp when the request was sent + + deliver chan *storageResponseV2 // Channel to deliver successful response on + revert chan *storageRequestV2 // Channel to deliver request failure on + cancel chan struct{} // Channel to track sync cancellation + timeout *time.Timer // Timer to track delivery timeout + stale chan struct{} // Channel to signal the request was dropped + + accounts []common.Hash // Account hashes to validate responses + roots []common.Hash // Storage roots to validate responses + + origin common.Hash // First storage slot requested to allow continuation checks + limit common.Hash // Last storage slot requested to allow non-overlapping chunking + + mainTask *accountTaskV2 // Task which this response belongs to (only access fields through the runloop!!) + subTask *storageTaskV2 // Task which this response is filling (only access fields through the runloop!!) +} + +// storageResponseV2 is an already Merkle-verified remote response to a storage +// range request. +type storageResponseV2 struct { + mainTask *accountTaskV2 // Task which this response belongs to + subTask *storageTaskV2 // Task which this response is filling + + accounts []common.Hash // Account hashes requested, may be only partially filled + roots []common.Hash // Storage roots requested, may be only partially filled + + hashes [][]common.Hash // Storage slot hashes in the returned range + slots [][][]byte // Storage slot values in the returned range + + cont bool // Whether the last storage range has a continuation +} + +// accountTaskV2 represents the sync task for a chunk of the account snapshot. +type accountTaskV2 struct { + // These fields get serialized to key-value store on shutdown + Next common.Hash // Next account to sync in this interval + Last common.Hash // Last account to sync in this interval + SubTasks map[common.Hash][]*storageTaskV2 // Storage intervals needing fetching for large contracts + + // Pending accounts whose storage has already been fully committed in + // this cycle, but which cannot advance Next yet because account commits + // must be sequential. Persisting them across cycle switches avoids + // refetching their storage. + StorageCompleted []common.Hash + + // These fields are internals used during runtime + req *accountRequestV2 // Pending request to fill this task + res *accountResponseV2 // Validate response filling this task + pend int // Number of pending subtasks for this round + + needCode []bool // Flags whether the filling accounts need code retrieval + needState []bool // Flags whether the filling accounts need storage retrieval + + codeTasks map[common.Hash]struct{} // Code hashes that need retrieval + stateTasks map[common.Hash]common.Hash // Account hashes->roots that need full state retrieval + stateCompleted map[common.Hash]struct{} // Account hashes whose storage have been completed + + done bool // Flag whether the task can be removed +} + +// activeSubTasks returns the set of storage tasks covered by the current account +// range. Normally this would be the entire subTask set, but on a sync interrupt +// and later resume it can happen that a shorter account range is retrieved. This +// method ensures that we only start up the subtasks covered by the latest account +// response. +// +// Nil is returned if the account range is empty. +func (task *accountTaskV2) activeSubTasks() map[common.Hash][]*storageTaskV2 { + if len(task.res.hashes) == 0 { + return nil + } + var ( + tasks = make(map[common.Hash][]*storageTaskV2) + last = task.res.hashes[len(task.res.hashes)-1] + ) + for hash, subTasks := range task.SubTasks { + if hash.Cmp(last) <= 0 { + tasks[hash] = subTasks + } + } + return tasks +} + +// storageTaskV2 represents the sync task for a chunk of the storage snapshot. +type storageTaskV2 struct { + Next common.Hash // Next account to sync in this interval + Last common.Hash // Last account to sync in this interval + + // These fields are internals used during runtime + root common.Hash // Storage root hash for this instance + req *storageRequestV2 // Pending request to fill this task + done bool // Flag whether the task can be removed +} + +// SyncProgressV2 is a database entry to allow suspending and resuming a snapshot state +// sync. Opposed to full and fast sync, there is no way to restart a suspended +// snap sync without prior knowledge of the suspension point. +type SyncProgressV2 struct { + Tasks []*accountTaskV2 // The suspended account tasks (contract tasks within) + + // Status report during syncing phase + AccountSynced uint64 // Number of accounts downloaded + AccountBytes common.StorageSize // Number of account trie bytes persisted to disk + BytecodeSynced uint64 // Number of bytecodes downloaded + BytecodeBytes common.StorageSize // Number of bytecode bytes downloaded + StorageSynced uint64 // Number of storage slots downloaded + StorageBytes common.StorageSize // Number of storage trie bytes persisted to disk +} + +// SyncPeerV2 abstracts out the methods required for a peer to be synced against +// with the goal of allowing the construction of mock peers without the full +// blown networking. +type SyncPeerV2 interface { + // ID retrieves the peer's unique identifier. + ID() string + + // RequestAccountRange fetches a batch of accounts rooted in a specific account + // trie, starting with the origin. + RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes int) error + + // RequestStorageRanges fetches a batch of storage slots belonging to one or + // more accounts. If slots from only one account is requested, an origin marker + // may also be used to retrieve from there. + RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes int) error + + // RequestByteCodes fetches a batch of bytecodes by hash. + RequestByteCodes(id uint64, hashes []common.Hash, bytes int) error + + // Log retrieves the peer's own contextual logger. + Log() log.Logger +} + +// SyncerV2 is an Ethereum account and storage state syncer based on the snap +// protocol. It's purpose is to download all the accounts and storage slots +// from remote peers, fixing the state inconsistencies between multiple sync +// targets with BALs(block level accessList) and ultimately reassemble the state +// trie (both account trie and storage tries) locally. +// +// Every network request has a variety of failure events: +// - The peer disconnects after task assignment, failing to send the request +// - The peer disconnects after sending the request, before delivering on it +// - The peer remains connected, but does not deliver a response in time +// - The peer delivers a stale response after a previous timeout +// - The peer delivers a refusal to serve the requested state +type SyncerV2 struct { + db ethdb.KeyValueStore // Database to store the trie nodes into (and dedup) + scheme string // Node scheme used in node database + + root common.Hash // Current state trie root being synced + tasks []*accountTaskV2 // Current account task set being synced + update chan struct{} // Notification channel for possible sync progression + + peers map[string]SyncPeerV2 // Currently active peers to download from + peerJoin *event.Feed // Event feed to react to peers joining + peerDrop *event.Feed // Event feed to react to peers dropping + rates *msgrate.Trackers // Message throughput rates for peers + + // Request tracking during syncing phase + statelessPeers map[string]struct{} // Peers that failed to deliver state data + accountIdlers map[string]struct{} // Peers that aren't serving account requests + bytecodeIdlers map[string]struct{} // Peers that aren't serving bytecode requests + storageIdlers map[string]struct{} // Peers that aren't serving storage requests + + accountReqs map[uint64]*accountRequestV2 // Account requests currently running + bytecodeReqs map[uint64]*bytecodeRequestV2 // Bytecode requests currently running + storageReqs map[uint64]*storageRequestV2 // Storage requests currently running + + accountSynced uint64 // Number of accounts downloaded + accountBytes common.StorageSize // Number of account trie bytes persisted to disk + bytecodeSynced uint64 // Number of bytecodes downloaded + bytecodeBytes common.StorageSize // Number of bytecode bytes downloaded + storageSynced uint64 // Number of storage slots downloaded + storageBytes common.StorageSize // Number of storage trie bytes persisted to disk + + extProgress *SyncProgressV2 // progress that can be exposed to external caller. + + startTime time.Time // Time instance when snapshot sync started + logTime time.Time // Time instance when status was last reported + + pend sync.WaitGroup // Tracks network request goroutines for graceful shutdown + lock sync.RWMutex // Protects fields that can change outside of sync (peers, reqs, root) +} + +// NewSyncerV2 creates a new snapshot syncer to download the Ethereum state over the +// snap protocol. +func NewSyncerV2(db ethdb.KeyValueStore, scheme string) *SyncerV2 { + return &SyncerV2{ + db: db, + scheme: scheme, + + peers: make(map[string]SyncPeerV2), + peerJoin: new(event.Feed), + peerDrop: new(event.Feed), + rates: msgrate.NewTrackers(log.New("proto", "snap")), + update: make(chan struct{}, 1), + + accountIdlers: make(map[string]struct{}), + storageIdlers: make(map[string]struct{}), + bytecodeIdlers: make(map[string]struct{}), + + accountReqs: make(map[uint64]*accountRequestV2), + storageReqs: make(map[uint64]*storageRequestV2), + bytecodeReqs: make(map[uint64]*bytecodeRequestV2), + + extProgress: new(SyncProgressV2), + } +} + +// Register injects a new data source into the syncer's peerset. +func (s *SyncerV2) Register(peer SyncPeerV2) error { + // Make sure the peer is not registered yet + id := peer.ID() + + s.lock.Lock() + if _, ok := s.peers[id]; ok { + log.Error("Snap peer already registered", "id", id) + + s.lock.Unlock() + return errors.New("already registered") + } + s.peers[id] = peer + s.rates.Track(id, msgrate.NewTracker(s.rates.MeanCapacities(), s.rates.MedianRoundTrip())) + + // Mark the peer as idle, even if no sync is running + s.accountIdlers[id] = struct{}{} + s.storageIdlers[id] = struct{}{} + s.bytecodeIdlers[id] = struct{}{} + s.lock.Unlock() + + // Notify any active syncs that a new peer can be assigned data + s.peerJoin.Send(id) + return nil +} + +// Unregister injects a new data source into the syncer's peerset. +func (s *SyncerV2) Unregister(id string) error { + // Remove all traces of the peer from the registry + s.lock.Lock() + if _, ok := s.peers[id]; !ok { + log.Error("Snap peer not registered", "id", id) + + s.lock.Unlock() + return errors.New("not registered") + } + delete(s.peers, id) + s.rates.Untrack(id) + + // Remove status markers, even if no sync is running + delete(s.statelessPeers, id) + + delete(s.accountIdlers, id) + delete(s.storageIdlers, id) + delete(s.bytecodeIdlers, id) + s.lock.Unlock() + + // Notify any active syncs that pending requests need to be reverted + s.peerDrop.Send(id) + return nil +} + +// Sync starts (or resumes a previous) sync cycle to iterate over a state trie +// with the given root and reconstruct the nodes based on the snapshot leaves. +// Previously downloaded segments will not be redownloaded of fixed, rather any +// errors will be healed after the leaves are fully accumulated. +func (s *SyncerV2) Sync(root common.Hash, cancel chan struct{}) error { + // Move the trie root from any previous value, revert stateless markers for + // any peers and initialize the syncer if it was not yet run + s.lock.Lock() + s.root = root + s.statelessPeers = make(map[string]struct{}) + s.lock.Unlock() + + if s.startTime.IsZero() { + s.startTime = time.Now() + } + // Retrieve the previous sync status from LevelDB and abort if already synced + s.loadSyncStatus() + if len(s.tasks) == 0 { + log.Debug("Snapshot sync already completed") + return nil + } + defer func() { // Persist any progress, independent of failure + for _, task := range s.tasks { + s.forwardAccountTask(task) + } + s.cleanAccountTasks() + s.saveSyncStatus() + }() + + log.Debug("Starting snapshot sync cycle", "root", root) + defer s.report(true) + + // Whether sync completed or not, disregard any future packets + defer func() { + log.Debug("Terminating snapshot sync cycle", "root", root) + s.lock.Lock() + s.accountReqs = make(map[uint64]*accountRequestV2) + s.storageReqs = make(map[uint64]*storageRequestV2) + s.bytecodeReqs = make(map[uint64]*bytecodeRequestV2) + s.lock.Unlock() + }() + // Keep scheduling sync tasks + peerJoin := make(chan string, 16) + peerJoinSub := s.peerJoin.Subscribe(peerJoin) + defer peerJoinSub.Unsubscribe() + + peerDrop := make(chan string, 16) + peerDropSub := s.peerDrop.Subscribe(peerDrop) + defer peerDropSub.Unsubscribe() + + // Create a set of unique channels for this sync cycle. We need these to be + // ephemeral so a data race doesn't accidentally deliver something stale on + // a persistent channel across syncs (yup, this happened) + var ( + accountReqFails = make(chan *accountRequestV2) + storageReqFails = make(chan *storageRequestV2) + bytecodeReqFails = make(chan *bytecodeRequestV2) + accountResps = make(chan *accountResponseV2) + storageResps = make(chan *storageResponseV2) + bytecodeResps = make(chan *bytecodeResponseV2) + ) + for { + // Remove all completed tasks and terminate sync if everything's done + s.cleanStorageTasks() + s.cleanAccountTasks() + if len(s.tasks) == 0 { + return nil + } + // Assign all the data retrieval tasks to any free peers + s.assignAccountTasks(accountResps, accountReqFails, cancel) + s.assignBytecodeTasks(bytecodeResps, bytecodeReqFails, cancel) + s.assignStorageTasks(storageResps, storageReqFails, cancel) + + // Update sync progress + s.lock.Lock() + s.extProgress = &SyncProgressV2{ + AccountSynced: s.accountSynced, + AccountBytes: s.accountBytes, + BytecodeSynced: s.bytecodeSynced, + BytecodeBytes: s.bytecodeBytes, + StorageSynced: s.storageSynced, + StorageBytes: s.storageBytes, + } + s.lock.Unlock() + // Wait for something to happen + select { + case <-s.update: + // Something happened (new peer, delivery, timeout), recheck tasks + case <-peerJoin: + // A new peer joined, try to schedule it new tasks + case id := <-peerDrop: + s.revertRequests(id) + case <-cancel: + return ErrCancelled + + case req := <-accountReqFails: + s.revertAccountRequest(req) + case req := <-bytecodeReqFails: + s.revertBytecodeRequest(req) + case req := <-storageReqFails: + s.revertStorageRequest(req) + + case res := <-accountResps: + s.processAccountResponse(res) + case res := <-bytecodeResps: + s.processBytecodeResponse(res) + case res := <-storageResps: + s.processStorageResponse(res) + } + // Report stats if something meaningful happened + s.report(false) + } +} + +// loadSyncStatus retrieves a previously aborted sync status from the database, +// or generates a fresh one if none is available. +func (s *SyncerV2) loadSyncStatus() { + var progress SyncProgressV2 + + if status := rawdb.ReadSnapshotSyncStatus(s.db); status != nil { + if err := json.Unmarshal(status, &progress); err != nil { + log.Error("Failed to decode snap sync status", "err", err) + } else { + for _, task := range progress.Tasks { + log.Debug("Scheduled account sync task", "from", task.Next, "last", task.Last) + } + s.tasks = progress.Tasks + + for _, task := range s.tasks { + // Restore the completed storages + task.stateCompleted = make(map[common.Hash]struct{}) + for _, hash := range task.StorageCompleted { + task.stateCompleted[hash] = struct{}{} + } + task.StorageCompleted = nil + } + s.lock.Lock() + defer s.lock.Unlock() + + s.accountSynced = progress.AccountSynced + s.accountBytes = progress.AccountBytes + s.bytecodeSynced = progress.BytecodeSynced + s.bytecodeBytes = progress.BytecodeBytes + s.storageSynced = progress.StorageSynced + s.storageBytes = progress.StorageBytes + return + } + } + // Either we've failed to decode the previous state, or there was none. + // Start a fresh sync by chunking up the account range and scheduling + // them for retrieval. + s.tasks = nil + s.accountSynced, s.accountBytes = 0, 0 + s.bytecodeSynced, s.bytecodeBytes = 0, 0 + s.storageSynced, s.storageBytes = 0, 0 + + var next common.Hash + step := new(big.Int).Sub( + new(big.Int).Div( + new(big.Int).Exp(common.Big2, common.Big256, nil), + big.NewInt(int64(accountConcurrency)), + ), common.Big1, + ) + for i := 0; i < accountConcurrency; i++ { + last := common.BigToHash(new(big.Int).Add(next.Big(), step)) + if i == accountConcurrency-1 { + // Make sure we don't overflow if the step is not a proper divisor + last = common.MaxHash + } + s.tasks = append(s.tasks, &accountTaskV2{ + Next: next, + Last: last, + SubTasks: make(map[common.Hash][]*storageTaskV2), + stateCompleted: make(map[common.Hash]struct{}), + }) + log.Debug("Created account sync task", "from", next, "last", last) + next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1)) + } +} + +// saveSyncStatus marshals the remaining sync tasks into leveldb. +func (s *SyncerV2) saveSyncStatus() { + // Serialize any partial progress to disk before spinning down + for _, task := range s.tasks { + // Save the account hashes of completed storage. + task.StorageCompleted = make([]common.Hash, 0, len(task.stateCompleted)) + for hash := range task.stateCompleted { + task.StorageCompleted = append(task.StorageCompleted, hash) + } + if len(task.StorageCompleted) > 0 { + log.Debug("Leftover completed storages", "number", len(task.StorageCompleted), "next", task.Next, "last", task.Last) + } + } + // Store the actual progress markers + progress := &SyncProgressV2{ + Tasks: s.tasks, + AccountSynced: s.accountSynced, + AccountBytes: s.accountBytes, + BytecodeSynced: s.bytecodeSynced, + BytecodeBytes: s.bytecodeBytes, + StorageSynced: s.storageSynced, + StorageBytes: s.storageBytes, + } + status, err := json.Marshal(progress) + if err != nil { + panic(err) // This can only fail during implementation + } + rawdb.WriteSnapshotSyncStatus(s.db, status) +} + +// Progress returns the snap sync status statistics. +func (s *SyncerV2) Progress() *SyncProgressV2 { + s.lock.Lock() + defer s.lock.Unlock() + + return s.extProgress +} + +// cleanAccountTasks removes account range retrieval tasks that have already been +// completed. +func (s *SyncerV2) cleanAccountTasks() { + // If the sync was already done before, don't even bother + if len(s.tasks) == 0 { + return + } + // Sync wasn't finished previously, check for any task that can be finalized + for i := 0; i < len(s.tasks); i++ { + if s.tasks[i].done { + s.tasks = append(s.tasks[:i], s.tasks[i+1:]...) + i-- + } + } + // If everything was just finalized just, generate the account trie and start heal + if len(s.tasks) == 0 { + // Push the final sync report + s.reportSyncProgressV2(true) + } +} + +// cleanStorageTasks iterates over all the account tasks and storage sub-tasks +// within, cleaning any that have been completed. +func (s *SyncerV2) cleanStorageTasks() { + for _, task := range s.tasks { + for account, subtasks := range task.SubTasks { + // Remove storage range retrieval tasks that completed + for j := 0; j < len(subtasks); j++ { + if subtasks[j].done { + subtasks = append(subtasks[:j], subtasks[j+1:]...) + j-- + } + } + if len(subtasks) > 0 { + task.SubTasks[account] = subtasks + continue + } + // If all storage chunks are done, mark the account as done too + for j, hash := range task.res.hashes { + if hash == account { + task.needState[j] = false + } + } + delete(task.SubTasks, account) + task.pend-- + + // Mark the state as complete to prevent resyncing + task.stateCompleted[account] = struct{}{} + + // If this was the last pending task, forward the account task + if task.pend == 0 { + s.forwardAccountTask(task) + } + } + } +} + +// assignAccountTasks attempts to match idle peers to pending account range +// retrievals. +func (s *SyncerV2) assignAccountTasks(success chan *accountResponseV2, fail chan *accountRequestV2, cancel chan struct{}) { + s.lock.Lock() + defer s.lock.Unlock() + + // Sort the peers by download capacity to use faster ones if many available + idlers := &capacitySort{ + ids: make([]string, 0, len(s.accountIdlers)), + caps: make([]int, 0, len(s.accountIdlers)), + } + targetTTL := s.rates.TargetTimeout() + for id := range s.accountIdlers { + if _, ok := s.statelessPeers[id]; ok { + continue + } + idlers.ids = append(idlers.ids, id) + idlers.caps = append(idlers.caps, s.rates.Capacity(id, AccountRangeMsg, targetTTL)) + } + if len(idlers.ids) == 0 { + return + } + sort.Sort(sort.Reverse(idlers)) + + // Iterate over all the tasks and try to find a pending one + for _, task := range s.tasks { + // Skip any tasks already filling + if task.req != nil || task.res != nil { + continue + } + // Task pending retrieval, try to find an idle peer. If no such peer + // exists, we probably assigned tasks for all (or they are stateless). + // Abort the entire assignment mechanism. + if len(idlers.ids) == 0 { + return + } + var ( + idle = idlers.ids[0] + peer = s.peers[idle] + cap = idlers.caps[0] + ) + idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] + + // Matched a pending task to an idle peer, allocate a unique request id + var reqid uint64 + for { + reqid = uint64(rand.Int63()) + if reqid == 0 { + continue + } + if _, ok := s.accountReqs[reqid]; ok { + continue + } + break + } + // Generate the network query and send it to the peer + req := &accountRequestV2{ + peer: idle, + id: reqid, + time: time.Now(), + deliver: success, + revert: fail, + cancel: cancel, + stale: make(chan struct{}), + origin: task.Next, + limit: task.Last, + task: task, + } + req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { + peer.Log().Debug("Account range request timed out", "reqid", reqid) + s.rates.Update(idle, AccountRangeMsg, 0, 0) + s.scheduleRevertAccountRequest(req) + }) + s.accountReqs[reqid] = req + delete(s.accountIdlers, idle) + + s.pend.Add(1) + go func(root common.Hash) { + defer s.pend.Done() + + // Attempt to send the remote request and revert if it fails + if cap > maxRequestSize { + cap = maxRequestSize + } + if cap < minRequestSize { // Don't bother with peers below a bare minimum performance + cap = minRequestSize + } + if err := peer.RequestAccountRange(reqid, root, req.origin, req.limit, cap); err != nil { + peer.Log().Debug("Failed to request account range", "err", err) + s.scheduleRevertAccountRequest(req) + } + }(s.root) + + // Inject the request into the task to block further assignments + task.req = req + } +} + +// assignBytecodeTasks attempts to match idle peers to pending code retrievals. +func (s *SyncerV2) assignBytecodeTasks(success chan *bytecodeResponseV2, fail chan *bytecodeRequestV2, cancel chan struct{}) { + s.lock.Lock() + defer s.lock.Unlock() + + // Sort the peers by download capacity to use faster ones if many available + idlers := &capacitySort{ + ids: make([]string, 0, len(s.bytecodeIdlers)), + caps: make([]int, 0, len(s.bytecodeIdlers)), + } + targetTTL := s.rates.TargetTimeout() + for id := range s.bytecodeIdlers { + if _, ok := s.statelessPeers[id]; ok { + continue + } + idlers.ids = append(idlers.ids, id) + idlers.caps = append(idlers.caps, s.rates.Capacity(id, ByteCodesMsg, targetTTL)) + } + if len(idlers.ids) == 0 { + return + } + sort.Sort(sort.Reverse(idlers)) + + // Iterate over all the tasks and try to find a pending one + for _, task := range s.tasks { + // Skip any tasks not in the bytecode retrieval phase + if task.res == nil { + continue + } + // Skip tasks that are already retrieving (or done with) all codes + if len(task.codeTasks) == 0 { + continue + } + // Task pending retrieval, try to find an idle peer. If no such peer + // exists, we probably assigned tasks for all (or they are stateless). + // Abort the entire assignment mechanism. + if len(idlers.ids) == 0 { + return + } + var ( + idle = idlers.ids[0] + peer = s.peers[idle] + cap = idlers.caps[0] + ) + idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] + + // Matched a pending task to an idle peer, allocate a unique request id + var reqid uint64 + for { + reqid = uint64(rand.Int63()) + if reqid == 0 { + continue + } + if _, ok := s.bytecodeReqs[reqid]; ok { + continue + } + break + } + // Generate the network query and send it to the peer + if cap > maxCodeRequestCount { + cap = maxCodeRequestCount + } + hashes := make([]common.Hash, 0, cap) + for hash := range task.codeTasks { + delete(task.codeTasks, hash) + hashes = append(hashes, hash) + if len(hashes) >= cap { + break + } + } + req := &bytecodeRequestV2{ + peer: idle, + id: reqid, + time: time.Now(), + deliver: success, + revert: fail, + cancel: cancel, + stale: make(chan struct{}), + hashes: hashes, + task: task, + } + req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { + peer.Log().Debug("Bytecode request timed out", "reqid", reqid) + s.rates.Update(idle, ByteCodesMsg, 0, 0) + s.scheduleRevertBytecodeRequest(req) + }) + s.bytecodeReqs[reqid] = req + delete(s.bytecodeIdlers, idle) + + s.pend.Add(1) + go func() { + defer s.pend.Done() + + // Attempt to send the remote request and revert if it fails + if err := peer.RequestByteCodes(reqid, hashes, maxRequestSize); err != nil { + log.Debug("Failed to request bytecodes", "err", err) + s.scheduleRevertBytecodeRequest(req) + } + }() + } +} + +// assignStorageTasks attempts to match idle peers to pending storage range +// retrievals. +func (s *SyncerV2) assignStorageTasks(success chan *storageResponseV2, fail chan *storageRequestV2, cancel chan struct{}) { + s.lock.Lock() + defer s.lock.Unlock() + + // Sort the peers by download capacity to use faster ones if many available + idlers := &capacitySort{ + ids: make([]string, 0, len(s.storageIdlers)), + caps: make([]int, 0, len(s.storageIdlers)), + } + targetTTL := s.rates.TargetTimeout() + for id := range s.storageIdlers { + if _, ok := s.statelessPeers[id]; ok { + continue + } + idlers.ids = append(idlers.ids, id) + idlers.caps = append(idlers.caps, s.rates.Capacity(id, StorageRangesMsg, targetTTL)) + } + if len(idlers.ids) == 0 { + return + } + sort.Sort(sort.Reverse(idlers)) + + // Iterate over all the tasks and try to find a pending one + for _, task := range s.tasks { + // Skip any tasks not in the storage retrieval phase + if task.res == nil { + continue + } + // Skip tasks that are already retrieving (or done with) all small states + storageTaskV2s := task.activeSubTasks() + if len(storageTaskV2s) == 0 && len(task.stateTasks) == 0 { + continue + } + // Task pending retrieval, try to find an idle peer. If no such peer + // exists, we probably assigned tasks for all (or they are stateless). + // Abort the entire assignment mechanism. + if len(idlers.ids) == 0 { + return + } + var ( + idle = idlers.ids[0] + peer = s.peers[idle] + cap = idlers.caps[0] + ) + idlers.ids, idlers.caps = idlers.ids[1:], idlers.caps[1:] + + // Matched a pending task to an idle peer, allocate a unique request id + var reqid uint64 + for { + reqid = uint64(rand.Int63()) + if reqid == 0 { + continue + } + if _, ok := s.storageReqs[reqid]; ok { + continue + } + break + } + // Generate the network query and send it to the peer. If there are + // large contract tasks pending, complete those before diving into + // even more new contracts. + if cap > maxRequestSize { + cap = maxRequestSize + } + if cap < minRequestSize { // Don't bother with peers below a bare minimum performance + cap = minRequestSize + } + storageSets := cap / 1024 + + var ( + accounts = make([]common.Hash, 0, storageSets) + roots = make([]common.Hash, 0, storageSets) + subtask *storageTaskV2 + ) + for account, subtasks := range storageTaskV2s { + for _, st := range subtasks { + // Skip any subtasks already filling + if st.req != nil { + continue + } + // Found an incomplete storage chunk, schedule it + accounts = append(accounts, account) + roots = append(roots, st.root) + subtask = st + break // Large contract chunks are downloaded individually + } + if subtask != nil { + break // Large contract chunks are downloaded individually + } + } + if subtask == nil { + // No large contract required retrieval, but small ones available + for account, root := range task.stateTasks { + delete(task.stateTasks, account) + + accounts = append(accounts, account) + roots = append(roots, root) + + if len(accounts) >= storageSets { + break + } + } + } + // If nothing was found, it means this task is actually already fully + // retrieving, but large contracts are hard to detect. Skip to the next. + if len(accounts) == 0 { + continue + } + req := &storageRequestV2{ + peer: idle, + id: reqid, + time: time.Now(), + deliver: success, + revert: fail, + cancel: cancel, + stale: make(chan struct{}), + accounts: accounts, + roots: roots, + mainTask: task, + subTask: subtask, + } + if subtask != nil { + req.origin = subtask.Next + req.limit = subtask.Last + } + req.timeout = time.AfterFunc(s.rates.TargetTimeout(), func() { + peer.Log().Debug("Storage request timed out", "reqid", reqid) + s.rates.Update(idle, StorageRangesMsg, 0, 0) + s.scheduleRevertStorageRequest(req) + }) + s.storageReqs[reqid] = req + delete(s.storageIdlers, idle) + + s.pend.Add(1) + go func(root common.Hash) { + defer s.pend.Done() + + // Attempt to send the remote request and revert if it fails + var origin, limit []byte + if subtask != nil { + origin, limit = req.origin[:], req.limit[:] + } + if err := peer.RequestStorageRanges(reqid, root, accounts, origin, limit, cap); err != nil { + log.Debug("Failed to request storage", "err", err) + s.scheduleRevertStorageRequest(req) + } + }(s.root) + + // Inject the request into the subtask to block further assignments + if subtask != nil { + subtask.req = req + } + } +} + +// revertRequests locates all the currently pending requests from a particular +// peer and reverts them, rescheduling for others to fulfill. +func (s *SyncerV2) revertRequests(peer string) { + // Gather the requests first, revertals need the lock too + s.lock.Lock() + var accountReqs []*accountRequestV2 + for _, req := range s.accountReqs { + if req.peer == peer { + accountReqs = append(accountReqs, req) + } + } + var bytecodeReqs []*bytecodeRequestV2 + for _, req := range s.bytecodeReqs { + if req.peer == peer { + bytecodeReqs = append(bytecodeReqs, req) + } + } + var storageReqs []*storageRequestV2 + for _, req := range s.storageReqs { + if req.peer == peer { + storageReqs = append(storageReqs, req) + } + } + s.lock.Unlock() + + // Revert all the requests matching the peer + for _, req := range accountReqs { + s.revertAccountRequest(req) + } + for _, req := range bytecodeReqs { + s.revertBytecodeRequest(req) + } + for _, req := range storageReqs { + s.revertStorageRequest(req) + } +} + +// scheduleRevertAccountRequest asks the event loop to clean up an account range +// request and return all failed retrieval tasks to the scheduler for reassignment. +func (s *SyncerV2) scheduleRevertAccountRequest(req *accountRequestV2) { + select { + case req.revert <- req: + // Sync event loop notified + case <-req.cancel: + // Sync cycle got cancelled + case <-req.stale: + // Request already reverted + } +} + +// revertAccountRequest cleans up an account range request and returns all failed +// retrieval tasks to the scheduler for reassignment. +// +// Note, this needs to run on the event runloop thread to reschedule to idle peers. +// On peer threads, use scheduleRevertAccountRequest. +func (s *SyncerV2) revertAccountRequest(req *accountRequestV2) { + log.Debug("Reverting account request", "peer", req.peer, "reqid", req.id) + select { + case <-req.stale: + log.Trace("Account request already reverted", "peer", req.peer, "reqid", req.id) + return + default: + } + close(req.stale) + + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). + s.lock.Lock() + delete(s.accountReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.accountIdlers[req.peer] = struct{}{} + } + s.lock.Unlock() + + // If there's a timeout timer still running, abort it and mark the account + // task as not-pending, ready for rescheduling + req.timeout.Stop() + if req.task.req == req { + req.task.req = nil + } +} + +// scheduleRevertBytecodeRequest asks the event loop to clean up a bytecode request +// and return all failed retrieval tasks to the scheduler for reassignment. +func (s *SyncerV2) scheduleRevertBytecodeRequest(req *bytecodeRequestV2) { + select { + case req.revert <- req: + // Sync event loop notified + case <-req.cancel: + // Sync cycle got cancelled + case <-req.stale: + // Request already reverted + } +} + +// revertBytecodeRequest cleans up a bytecode request and returns all failed +// retrieval tasks to the scheduler for reassignment. +// +// Note, this needs to run on the event runloop thread to reschedule to idle peers. +// On peer threads, use scheduleRevertBytecodeRequest. +func (s *SyncerV2) revertBytecodeRequest(req *bytecodeRequestV2) { + log.Debug("Reverting bytecode request", "peer", req.peer) + select { + case <-req.stale: + log.Trace("Bytecode request already reverted", "peer", req.peer, "reqid", req.id) + return + default: + } + close(req.stale) + + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). + s.lock.Lock() + delete(s.bytecodeReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.bytecodeIdlers[req.peer] = struct{}{} + } + s.lock.Unlock() + + // If there's a timeout timer still running, abort it and mark the code + // retrievals as not-pending, ready for rescheduling + req.timeout.Stop() + for _, hash := range req.hashes { + req.task.codeTasks[hash] = struct{}{} + } +} + +// scheduleRevertStorageRequest asks the event loop to clean up a storage range +// request and return all failed retrieval tasks to the scheduler for reassignment. +func (s *SyncerV2) scheduleRevertStorageRequest(req *storageRequestV2) { + select { + case req.revert <- req: + // Sync event loop notified + case <-req.cancel: + // Sync cycle got cancelled + case <-req.stale: + // Request already reverted + } +} + +// revertStorageRequest cleans up a storage range request and returns all failed +// retrieval tasks to the scheduler for reassignment. +// +// Note, this needs to run on the event runloop thread to reschedule to idle peers. +// On peer threads, use scheduleRevertStorageRequest. +func (s *SyncerV2) revertStorageRequest(req *storageRequestV2) { + log.Debug("Reverting storage request", "peer", req.peer) + select { + case <-req.stale: + log.Trace("Storage request already reverted", "peer", req.peer, "reqid", req.id) + return + default: + } + close(req.stale) + + // Remove the request from the tracked set and restore the peer to the + // idle pool so it can be reassigned work (skip if peer already left). + s.lock.Lock() + delete(s.storageReqs, req.id) + if _, ok := s.peers[req.peer]; ok { + s.storageIdlers[req.peer] = struct{}{} + } + s.lock.Unlock() + + // If there's a timeout timer still running, abort it and mark the storage + // task as not-pending, ready for rescheduling + req.timeout.Stop() + if req.subTask != nil { + req.subTask.req = nil + } else { + for i, account := range req.accounts { + req.mainTask.stateTasks[account] = req.roots[i] + } + } +} + +// processAccountResponse integrates an already validated account range response +// into the account tasks. +func (s *SyncerV2) processAccountResponse(res *accountResponseV2) { + // Switch the task from pending to filling + res.task.req = nil + res.task.res = res + + // Ensure that the response doesn't overflow into the subsequent task + lastBig := res.task.Last.Big() + for i, hash := range res.hashes { + // Mark the range complete if the last is already included. + // Keep iteration to delete the extra states if exists. + cmp := hash.Big().Cmp(lastBig) + if cmp == 0 { + res.cont = false + continue + } + if cmp > 0 { + // Chunk overflown, cut off excess + res.hashes = res.hashes[:i] + res.accounts = res.accounts[:i] + res.cont = false // Mark range completed + break + } + } + // Iterate over all the accounts and assemble which ones need further sub- + // filling before the entire account range can be persisted. + res.task.needCode = make([]bool, len(res.accounts)) + res.task.needState = make([]bool, len(res.accounts)) + res.task.codeTasks = make(map[common.Hash]struct{}) + res.task.stateTasks = make(map[common.Hash]common.Hash) + + resumed := make(map[common.Hash]struct{}) + + res.task.pend = 0 + for i, account := range res.accounts { + // Check if the account is a contract with an unknown code + if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) { + if !rawdb.HasCodeWithPrefix(s.db, common.BytesToHash(account.CodeHash)) { + res.task.codeTasks[common.BytesToHash(account.CodeHash)] = struct{}{} + res.task.needCode[i] = true + res.task.pend++ + } + } + // Check if the account is a contract with an unknown storage trie + if account.Root != types.EmptyRootHash { + // If the storage was already retrieved in the last cycle, there's no need + // to resync it again, regardless of whether the storage root is consistent + // or not. + if _, exist := res.task.stateCompleted[res.hashes[i]]; exist { + // The leftover storage tasks are not expected, unless system is + // very wrong. + if _, ok := res.task.SubTasks[res.hashes[i]]; ok { + panic(fmt.Errorf("unexpected leftover storage tasks, owner: %x", res.hashes[i])) + } + } else { + // If there was a previous large state retrieval in progress, + // don't restart it from scratch. This happens if a sync cycle + // is interrupted and resumed later. However, *do* update the + // previous root hash. + if subtasks, ok := res.task.SubTasks[res.hashes[i]]; ok { + log.Debug("Resuming large storage retrieval", "account", res.hashes[i], "root", account.Root) + for _, subtask := range subtasks { + subtask.root = account.Root + } + resumed[res.hashes[i]] = struct{}{} + largeStorageResumedGauge.Inc(1) + } else { + // It's possible that in the hash scheme, the storage, along + // with the trie nodes of the given root, is already present + // in the database. Schedule the storage task anyway to simplify + // the logic here. + res.task.stateTasks[res.hashes[i]] = account.Root + } + res.task.needState[i] = true + res.task.pend++ + } + } + } + // Delete any subtasks that have been aborted but not resumed. It's essential + // as the corresponding contract might be self-destructed in this cycle(it's + // no longer possible in ethereum as self-destruction is disabled in Cancun + // Fork, but the condition is still necessary for other networks). + // + // Keep the leftover storage tasks if they are not covered by the responded + // account range which should be picked up in next account wave. + if len(res.hashes) > 0 { + // The hash of last delivered account in the response + last := res.hashes[len(res.hashes)-1] + for hash := range res.task.SubTasks { + if hash.Cmp(last) > 0 { + log.Debug("Keeping suspended storage retrieval", "account", hash) + continue + } + if _, ok := resumed[hash]; !ok { + log.Warn("Aborting suspended storage retrieval", "account", hash) + delete(res.task.SubTasks, hash) + largeStorageDiscardGauge.Inc(1) + } + } + } + // If the account range contained no contracts, or all have been fully filled + // beforehand, short circuit storage filling and forward to the next task + if res.task.pend == 0 { + s.forwardAccountTask(res.task) + return + } + // Some accounts are incomplete, leave as is for the storage and contract + // task assigners to pick up and fill +} + +// processBytecodeResponse integrates an already validated bytecode response +// into the account tasks. +func (s *SyncerV2) processBytecodeResponse(res *bytecodeResponseV2) { + batch := s.db.NewBatch() + + var codes uint64 + for i, hash := range res.hashes { + code := res.codes[i] + + // If the bytecode was not delivered, reschedule it + if code == nil { + res.task.codeTasks[hash] = struct{}{} + continue + } + // Code was delivered, mark it not needed any more + for j, account := range res.task.res.accounts { + if res.task.needCode[j] && hash == common.BytesToHash(account.CodeHash) { + res.task.needCode[j] = false + res.task.pend-- + } + } + // Push the bytecode into a database batch + codes++ + rawdb.WriteCode(batch, hash, code) + } + bytes := common.StorageSize(batch.ValueSize()) + if err := batch.Write(); err != nil { + log.Crit("Failed to persist bytecodes", "err", err) + } + s.bytecodeSynced += codes + s.bytecodeBytes += bytes + + log.Debug("Persisted set of bytecodes", "count", codes, "bytes", bytes) + + // If this delivery completed the last pending task, forward the account task + // to the next chunk + if res.task.pend == 0 { + s.forwardAccountTask(res.task) + return + } + // Some accounts are still incomplete, leave as is for the storage and contract + // task assigners to pick up and fill. +} + +// processStorageResponse integrates an already validated storage response +// into the account tasks. +func (s *SyncerV2) processStorageResponse(res *storageResponseV2) { + // Switch the subtask from pending to idle + if res.subTask != nil { + res.subTask.req = nil + } + batch := ethdb.HookedBatch{ + Batch: s.db.NewBatch(), + OnPut: func(key []byte, value []byte) { + s.storageBytes += common.StorageSize(len(key) + len(value)) + }, + } + var ( + slots int + oldStorageBytes = s.storageBytes + ) + // Iterate over all the accounts and reconstruct their storage tries from the + // delivered slots + for i, account := range res.accounts { + // If the account was not delivered, reschedule it + if i >= len(res.hashes) { + res.mainTask.stateTasks[account] = res.roots[i] + continue + } + // State was delivered, if complete mark as not needed any more, otherwise + // mark the account as needing healing + for j, hash := range res.mainTask.res.hashes { + if account != hash { + continue + } + acc := res.mainTask.res.accounts[j] + + // If the packet contains multiple contract storage slots, all + // but the last are surely complete. The last contract may be + // chunked, so check it's continuation flag. + if res.subTask == nil && res.mainTask.needState[j] && (i < len(res.hashes)-1 || !res.cont) { + res.mainTask.needState[j] = false + res.mainTask.pend-- + res.mainTask.stateCompleted[account] = struct{}{} // mark it as completed + smallStorageGauge.Inc(1) + } + // If the last contract was chunked, we need to switch to large + // contract handling mode + if res.subTask == nil && i == len(res.hashes)-1 && res.cont { + // If we haven't yet started a large-contract retrieval, create + // the subtasks for it within the main account task + if tasks, ok := res.mainTask.SubTasks[account]; !ok { + var ( + keys = res.hashes[i] + chunks = uint64(storageConcurrency) + lastKey common.Hash + ) + if len(keys) > 0 { + lastKey = keys[len(keys)-1] + } + // If the number of slots remaining is low, decrease the + // number of chunks. Somewhere on the order of 10-15K slots + // fit into a packet of 500KB. A key/slot pair is maximum 64 + // bytes, so pessimistically maxRequestSize/64 = 8K. + // + // Chunk so that at least 2 packets are needed to fill a task. + if estimate, err := estimateRemainingSlots(len(keys), lastKey); err == nil { + if n := estimate / (2 * (maxRequestSize / 64)); n+1 < chunks { + chunks = n + 1 + } + log.Debug("Chunked large contract", "initiators", len(keys), "tail", lastKey, "remaining", estimate, "chunks", chunks) + } else { + log.Debug("Chunked large contract", "initiators", len(keys), "tail", lastKey, "chunks", chunks) + } + r := newHashRange(lastKey, chunks) + if chunks == 1 { + smallStorageGauge.Inc(1) + } else { + largeStorageGauge.Inc(1) + } + tasks = append(tasks, &storageTaskV2{ + Next: common.Hash{}, + Last: r.End(), + root: acc.Root, + }) + for r.Next() { + tasks = append(tasks, &storageTaskV2{ + Next: r.Start(), + Last: r.End(), + root: acc.Root, + }) + } + for _, task := range tasks { + log.Debug("Created storage sync task", "account", account, "root", acc.Root, "from", task.Next, "last", task.Last) + } + res.mainTask.SubTasks[account] = tasks + + // Since we've just created the sub-tasks, this response + // is surely for the first one (zero origin) + res.subTask = tasks[0] + } + } + // If we're in large contract delivery mode, forward the subtask + if res.subTask != nil { + // Ensure the response doesn't overflow into the subsequent task + last := res.subTask.Last.Big() + // Find the first overflowing key. While at it, mark res as complete + // if we find the range to include or pass the 'last' + index := sort.Search(len(res.hashes[i]), func(k int) bool { + cmp := res.hashes[i][k].Big().Cmp(last) + if cmp >= 0 { + res.cont = false + } + return cmp > 0 + }) + if index >= 0 { + // cut off excess + res.hashes[i] = res.hashes[i][:index] + res.slots[i] = res.slots[i][:index] + } + // Forward the relevant storage chunk (even if created just now) + if res.cont { + res.subTask.Next = incHash(res.hashes[i][len(res.hashes[i])-1]) + } else { + res.subTask.done = true + } + } + } + // Iterate over all the complete contracts, reconstruct the trie nodes and + // push them to disk. If the contract is chunked, the trie nodes will be + // reconstructed later. + slots += len(res.hashes[i]) + + // Persist the received storage segments. These flat state may be outdated + // during the sync, but it will be fixed by the BAL-healing. + for j := 0; j < len(res.hashes[i]); j++ { + rawdb.WriteStorageSnapshot(batch, account, res.hashes[i][j], res.slots[i][j]) + } + } + // Flush anything written just now and update the stats + if err := batch.Write(); err != nil { + log.Crit("Failed to persist storage slots", "err", err) + } + s.storageSynced += uint64(slots) + + log.Debug("Persisted set of storage slots", "accounts", len(res.hashes), "slots", slots, "bytes", s.storageBytes-oldStorageBytes) + + // If this delivery completed the last pending task, forward the account task + // to the next chunk + if res.mainTask.pend == 0 { + s.forwardAccountTask(res.mainTask) + return + } + // Some accounts are still incomplete, leave as is for the storage and contract + // task assigners to pick up and fill. +} + +// forwardAccountTask takes a filled account task and persists anything available +// into the database, after which it forwards the next account marker so that the +// task's next chunk may be filled. +func (s *SyncerV2) forwardAccountTask(task *accountTaskV2) { + // Remove any pending delivery + res := task.res + if res == nil { + return // nothing to forward + } + task.res = nil + + // Persist the received account segments. These flat state maybe + // outdated during the sync, but it can be fixed later during the + // snapshot generation. + oldAccountBytes := s.accountBytes + + batch := ethdb.HookedBatch{ + Batch: s.db.NewBatch(), + OnPut: func(key []byte, value []byte) { + s.accountBytes += common.StorageSize(len(key) + len(value)) + }, + } + for i, hash := range res.hashes { + if task.needCode[i] || task.needState[i] { + break + } + slim := types.SlimAccountRLP(*res.accounts[i]) + rawdb.WriteAccountSnapshot(batch, hash, slim) + } + // Flush anything written just now and update the stats + if err := batch.Write(); err != nil { + log.Crit("Failed to persist accounts", "err", err) + } + s.accountSynced += uint64(len(res.accounts)) + + // Task filling persisted, push it the chunk marker forward to the first + // account still missing data. + for i, hash := range res.hashes { + if task.needCode[i] || task.needState[i] { + return + } + task.Next = incHash(hash) + + // Remove the completion flag once the account range is pushed + // forward. The leftover accounts will be skipped in the next + // cycle. + delete(task.stateCompleted, hash) + } + // All accounts marked as complete, track if the entire task is done + task.done = !res.cont + + // Error out if there is any leftover completion flag. + if task.done && len(task.stateCompleted) != 0 { + panic(fmt.Errorf("storage completion flags should be emptied, %d left", len(task.stateCompleted))) + } + log.Debug("Persisted range of accounts", "accounts", len(res.accounts), "bytes", s.accountBytes-oldAccountBytes) +} + +// OnAccounts is a callback method to invoke when a range of accounts are +// received from a remote peer. +func (s *SyncerV2) OnAccounts(peer SyncPeerV2, id uint64, hashes []common.Hash, accounts [][]byte, proof [][]byte) error { + size := common.StorageSize(len(hashes) * common.HashLength) + for _, account := range accounts { + size += common.StorageSize(len(account)) + } + for _, node := range proof { + size += common.StorageSize(len(node)) + } + logger := peer.Log().New("reqid", id) + logger.Trace("Delivering range of accounts", "hashes", len(hashes), "accounts", len(accounts), "proofs", len(proof), "bytes", size) + + // Whether or not the response is valid, we can mark the peer as idle and + // notify the scheduler to assign a new task. If the response is invalid, + // we'll drop the peer in a bit. + defer func() { + s.lock.Lock() + defer s.lock.Unlock() + if _, ok := s.peers[peer.ID()]; ok { + s.accountIdlers[peer.ID()] = struct{}{} + } + select { + case s.update <- struct{}{}: + default: + } + }() + s.lock.Lock() + // Ensure the response is for a valid request + req, ok := s.accountReqs[id] + if !ok { + // Request stale, perhaps the peer timed out but came through in the end + logger.Warn("Unexpected account range packet") + s.lock.Unlock() + return nil + } + delete(s.accountReqs, id) + s.rates.Update(peer.ID(), AccountRangeMsg, time.Since(req.time), int(size)) + + // Clean up the request timeout timer, we'll see how to proceed further based + // on the actual delivered content + if !req.timeout.Stop() { + // The timeout is already triggered, and this request will be reverted+rescheduled + s.lock.Unlock() + return nil + } + // Response is valid, but check if peer is signalling that it does not have + // the requested data. For account range queries that means the state being + // retrieved was either already pruned remotely, or the peer is not yet + // synced to our head. + if len(hashes) == 0 && len(accounts) == 0 && len(proof) == 0 { + logger.Debug("Peer rejected account range request", "root", s.root) + s.statelessPeers[peer.ID()] = struct{}{} + s.lock.Unlock() + + // Signal this request as failed, and ready for rescheduling + s.scheduleRevertAccountRequest(req) + return nil + } + root := s.root + s.lock.Unlock() + + // Reconstruct a partial trie from the response and verify it + keys := make([][]byte, len(hashes)) + for i, key := range hashes { + keys[i] = common.CopyBytes(key[:]) + } + nodes := make(trienode.ProofList, len(proof)) + for i, node := range proof { + nodes[i] = node + } + cont, err := trie.VerifyRangeProof(root, req.origin[:], keys, accounts, nodes.Set()) + if err != nil { + logger.Warn("Account range failed proof", "err", err) + // Signal this request as failed, and ready for rescheduling + s.scheduleRevertAccountRequest(req) + return err + } + accs := make([]*types.StateAccount, len(accounts)) + for i, account := range accounts { + acc := new(types.StateAccount) + if err := rlp.DecodeBytes(account, acc); err != nil { + panic(err) // We created these blobs, we must be able to decode them + } + accs[i] = acc + } + response := &accountResponseV2{ + task: req.task, + hashes: hashes, + accounts: accs, + cont: cont, + } + select { + case req.deliver <- response: + case <-req.cancel: + case <-req.stale: + } + return nil +} + +// OnByteCodes is a callback method to invoke when a batch of contract +// bytes codes are received from a remote peer in the syncing phase. +func (s *SyncerV2) OnByteCodes(peer SyncPeerV2, id uint64, bytecodes [][]byte) error { + var size common.StorageSize + for _, code := range bytecodes { + size += common.StorageSize(len(code)) + } + logger := peer.Log().New("reqid", id) + logger.Trace("Delivering set of bytecodes", "bytecodes", len(bytecodes), "bytes", size) + + // Whether or not the response is valid, we can mark the peer as idle and + // notify the scheduler to assign a new task. If the response is invalid, + // we'll drop the peer in a bit. + defer func() { + s.lock.Lock() + defer s.lock.Unlock() + if _, ok := s.peers[peer.ID()]; ok { + s.bytecodeIdlers[peer.ID()] = struct{}{} + } + select { + case s.update <- struct{}{}: + default: + } + }() + s.lock.Lock() + // Ensure the response is for a valid request + req, ok := s.bytecodeReqs[id] + if !ok { + // Request stale, perhaps the peer timed out but came through in the end + logger.Warn("Unexpected bytecode packet") + s.lock.Unlock() + return nil + } + delete(s.bytecodeReqs, id) + s.rates.Update(peer.ID(), ByteCodesMsg, time.Since(req.time), len(bytecodes)) + + // Clean up the request timeout timer, we'll see how to proceed further based + // on the actual delivered content + if !req.timeout.Stop() { + // The timeout is already triggered, and this request will be reverted+rescheduled + s.lock.Unlock() + return nil + } + + // Response is valid, but check if peer is signalling that it does not have + // the requested data. For bytecode range queries that means the peer is not + // yet synced. + if len(bytecodes) == 0 { + logger.Debug("Peer rejected bytecode request") + s.statelessPeers[peer.ID()] = struct{}{} + s.lock.Unlock() + + // Signal this request as failed, and ready for rescheduling + s.scheduleRevertBytecodeRequest(req) + return nil + } + s.lock.Unlock() + + // Cross reference the requested bytecodes with the response to find gaps + // that the serving node is missing + hasher := crypto.NewKeccakState() + hash := make([]byte, 32) + + codes := make([][]byte, len(req.hashes)) + for i, j := 0, 0; i < len(bytecodes); i++ { + // Find the next hash that we've been served, leaving misses with nils + hasher.Reset() + hasher.Write(bytecodes[i]) + hasher.Read(hash) + + for j < len(req.hashes) && !bytes.Equal(hash, req.hashes[j][:]) { + j++ + } + if j < len(req.hashes) { + codes[j] = bytecodes[i] + j++ + continue + } + // We've either ran out of hashes, or got unrequested data + logger.Warn("Unexpected bytecodes", "count", len(bytecodes)-i) + // Signal this request as failed, and ready for rescheduling + s.scheduleRevertBytecodeRequest(req) + return errors.New("unexpected bytecode") + } + // Response validated, send it to the scheduler for filling + response := &bytecodeResponseV2{ + task: req.task, + hashes: req.hashes, + codes: codes, + } + select { + case req.deliver <- response: + case <-req.cancel: + case <-req.stale: + } + return nil +} + +// OnStorage is a callback method to invoke when ranges of storage slots +// are received from a remote peer. +func (s *SyncerV2) OnStorage(peer SyncPeerV2, id uint64, hashes [][]common.Hash, slots [][][]byte, proof [][]byte) error { + // Gather some trace stats to aid in debugging issues + var ( + hashCount int + slotCount int + size common.StorageSize + ) + for _, hashset := range hashes { + size += common.StorageSize(common.HashLength * len(hashset)) + hashCount += len(hashset) + } + for _, slotset := range slots { + for _, slot := range slotset { + size += common.StorageSize(len(slot)) + } + slotCount += len(slotset) + } + for _, node := range proof { + size += common.StorageSize(len(node)) + } + logger := peer.Log().New("reqid", id) + logger.Trace("Delivering ranges of storage slots", "accounts", len(hashes), "hashes", hashCount, "slots", slotCount, "proofs", len(proof), "size", size) + + // Whether or not the response is valid, we can mark the peer as idle and + // notify the scheduler to assign a new task. If the response is invalid, + // we'll drop the peer in a bit. + defer func() { + s.lock.Lock() + defer s.lock.Unlock() + if _, ok := s.peers[peer.ID()]; ok { + s.storageIdlers[peer.ID()] = struct{}{} + } + select { + case s.update <- struct{}{}: + default: + } + }() + s.lock.Lock() + // Ensure the response is for a valid request + req, ok := s.storageReqs[id] + if !ok { + // Request stale, perhaps the peer timed out but came through in the end + logger.Warn("Unexpected storage ranges packet") + s.lock.Unlock() + return nil + } + delete(s.storageReqs, id) + s.rates.Update(peer.ID(), StorageRangesMsg, time.Since(req.time), int(size)) + + // Clean up the request timeout timer, we'll see how to proceed further based + // on the actual delivered content + if !req.timeout.Stop() { + // The timeout is already triggered, and this request will be reverted+rescheduled + s.lock.Unlock() + return nil + } + + // Reject the response if the hash sets and slot sets don't match, or if the + // peer sent more data than requested. + if len(hashes) != len(slots) { + s.lock.Unlock() + s.scheduleRevertStorageRequest(req) // reschedule request + logger.Warn("Hash and slot set size mismatch", "hashset", len(hashes), "slotset", len(slots)) + return errors.New("hash and slot set size mismatch") + } + if len(hashes) > len(req.accounts) { + s.lock.Unlock() + s.scheduleRevertStorageRequest(req) // reschedule request + logger.Warn("Hash set larger than requested", "hashset", len(hashes), "requested", len(req.accounts)) + return errors.New("hash set larger than requested") + } + // Response is valid, but check if peer is signalling that it does not have + // the requested data. For storage range queries that means the state being + // retrieved was either already pruned remotely, or the peer is not yet + // synced to our head. + if len(hashes) == 0 && len(proof) == 0 { + logger.Debug("Peer rejected storage request") + s.statelessPeers[peer.ID()] = struct{}{} + s.lock.Unlock() + s.scheduleRevertStorageRequest(req) // reschedule request + return nil + } + s.lock.Unlock() + + // Reconstruct the partial tries from the response and verify them + var cont bool + + // If a proof was attached while the response is empty, it indicates that the + // requested range specified with 'origin' is empty. Construct an empty state + // response locally to finalize the range. + if len(hashes) == 0 && len(proof) > 0 { + hashes = append(hashes, []common.Hash{}) + slots = append(slots, [][]byte{}) + } + for i := 0; i < len(hashes); i++ { + // Convert the keys and proofs into an internal format + keys := make([][]byte, len(hashes[i])) + for j, key := range hashes[i] { + keys[j] = common.CopyBytes(key[:]) + } + nodes := make(trienode.ProofList, 0, len(proof)) + if i == len(hashes)-1 { + for _, node := range proof { + nodes = append(nodes, node) + } + } + var err error + if len(nodes) == 0 { + // No proof has been attached, the response must cover the entire key + // space and hash to the origin root. + _, err = trie.VerifyRangeProof(req.roots[i], nil, keys, slots[i], nil) + if err != nil { + s.scheduleRevertStorageRequest(req) // reschedule request + logger.Warn("Storage slots failed proof", "err", err) + return err + } + } else { + // A proof was attached, the response is only partial, check that the + // returned data is indeed part of the storage trie + proofdb := nodes.Set() + + cont, err = trie.VerifyRangeProof(req.roots[i], req.origin[:], keys, slots[i], proofdb) + if err != nil { + s.scheduleRevertStorageRequest(req) // reschedule request + logger.Warn("Storage range failed proof", "err", err) + return err + } + } + } + // Partial tries reconstructed, send them to the scheduler for storage filling + response := &storageResponseV2{ + mainTask: req.mainTask, + subTask: req.subTask, + accounts: req.accounts, + roots: req.roots, + hashes: hashes, + slots: slots, + cont: cont, + } + select { + case req.deliver <- response: + case <-req.cancel: + case <-req.stale: + } + return nil +} + +// report calculates various status reports and provides it to the user. +func (s *SyncerV2) report(force bool) { + if len(s.tasks) > 0 { + s.reportSyncProgressV2(force) + return + } +} + +// reportSyncProgressV2 calculates various status reports and provides it to the user. +func (s *SyncerV2) reportSyncProgressV2(force bool) { + // Don't report all the events, just occasionally + if !force && time.Since(s.logTime) < 8*time.Second { + return + } + // Don't report anything until we have a meaningful progress + synced := s.accountBytes + s.bytecodeBytes + s.storageBytes + if synced == 0 { + return + } + accountGaps := new(big.Int) + for _, task := range s.tasks { + accountGaps.Add(accountGaps, new(big.Int).Sub(task.Last.Big(), task.Next.Big())) + } + accountFills := new(big.Int).Sub(hashSpace, accountGaps) + if accountFills.BitLen() == 0 { + return + } + s.logTime = time.Now() + estBytes := float64(new(big.Int).Div( + new(big.Int).Mul(new(big.Int).SetUint64(uint64(synced)), hashSpace), + accountFills, + ).Uint64()) + + // Don't report anything until we have a meaningful progress + if estBytes < 1.0 { + return + } + // Cap the estimated state size using the synced size to avoid negative values + if estBytes < float64(synced) { + estBytes = float64(synced) + } + elapsed := time.Since(s.startTime) + estTime := elapsed / time.Duration(synced) * time.Duration(estBytes) + + // Create a mega progress report + var ( + progress = fmt.Sprintf("%.2f%%", float64(synced)*100/estBytes) + accounts = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.accountSynced), s.accountBytes.TerminalString()) + storage = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.storageSynced), s.storageBytes.TerminalString()) + bytecode = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(s.bytecodeSynced), s.bytecodeBytes.TerminalString()) + ) + log.Info("Syncing: state download in progress", "synced", progress, "state", synced, + "accounts", accounts, "slots", storage, "codes", bytecode, "eta", common.PrettyDuration(estTime-elapsed)) +} diff --git a/eth/protocols/snap/syncv2_test.go b/eth/protocols/snap/syncv2_test.go new file mode 100644 index 0000000000..d303d84c09 --- /dev/null +++ b/eth/protocols/snap/syncv2_test.go @@ -0,0 +1,1164 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snap + +import ( + "bytes" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/trie/trienode" +) + +// SyncerV2 (skeleton) only downloads the flat state (accounts, storage slots, +// bytecodes) and does not perform trie generation or state healing. These tests +// verify that, in a single uninterrupted sync cycle, the syncer fully downloads +// all the expected flat state from the source peer(s). + +type ( + accountHandlerFuncV2 func(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error + storageHandlerFuncV2 func(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error + codeHandlerFuncV2 func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error +) + +type testPeerV2 struct { + id string + test *testing.T + remote *SyncerV2 + logger log.Logger + accountTrie *trie.Trie + accountValues []*kv + storageTries map[common.Hash]*trie.Trie + storageValues map[common.Hash][]*kv + + accountRequestHandler accountHandlerFuncV2 + storageRequestHandler storageHandlerFuncV2 + codeRequestHandler codeHandlerFuncV2 + term func() + + // counters + nAccountRequests atomic.Int64 + nStorageRequests atomic.Int64 + nBytecodeRequests atomic.Int64 +} + +func newTestPeerV2(id string, t *testing.T, term func()) *testPeerV2 { + return &testPeerV2{ + id: id, + test: t, + logger: log.New("id", id), + accountRequestHandler: defaultAccountRequestHandlerV2, + storageRequestHandler: defaultStorageRequestHandlerV2, + codeRequestHandler: defaultCodeRequestHandlerV2, + term: term, + } +} + +func (t *testPeerV2) setStorageTries(tries map[common.Hash]*trie.Trie) { + t.storageTries = make(map[common.Hash]*trie.Trie) + for root, trie := range tries { + t.storageTries[root] = trie.Copy() + } +} + +func (t *testPeerV2) ID() string { return t.id } +func (t *testPeerV2) Log() log.Logger { return t.logger } + +func (t *testPeerV2) Stats() string { + return fmt.Sprintf(`Account requests: %d +Storage requests: %d +Bytecode requests: %d +`, t.nAccountRequests.Load(), t.nStorageRequests.Load(), t.nBytecodeRequests.Load()) +} + +func (t *testPeerV2) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes int) error { + t.logger.Trace("Fetching range of accounts", "reqid", id, "root", root, "origin", origin, "limit", limit, "bytes", common.StorageSize(bytes)) + t.nAccountRequests.Add(1) + go t.accountRequestHandler(t, id, root, origin, limit, bytes) + return nil +} + +func (t *testPeerV2) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes int) error { + t.nStorageRequests.Add(1) + if len(accounts) == 1 && origin != nil { + t.logger.Trace("Fetching range of large storage slots", "reqid", id, "root", root, "account", accounts[0], "origin", common.BytesToHash(origin), "limit", common.BytesToHash(limit), "bytes", common.StorageSize(bytes)) + } else { + t.logger.Trace("Fetching ranges of small storage slots", "reqid", id, "root", root, "accounts", len(accounts), "first", accounts[0], "bytes", common.StorageSize(bytes)) + } + go t.storageRequestHandler(t, id, root, accounts, origin, limit, bytes) + return nil +} + +func (t *testPeerV2) RequestByteCodes(id uint64, hashes []common.Hash, bytes int) error { + t.nBytecodeRequests.Add(1) + t.logger.Trace("Fetching set of byte codes", "reqid", id, "hashes", len(hashes), "bytes", common.StorageSize(bytes)) + go t.codeRequestHandler(t, id, hashes, bytes) + return nil +} + +func createAccountRequestResponseV2(t *testPeerV2, root common.Hash, origin common.Hash, limit common.Hash, cap int) (keys []common.Hash, vals [][]byte, proofs [][]byte) { + var size int + if limit == (common.Hash{}) { + limit = common.MaxHash + } + for _, entry := range t.accountValues { + if size > cap { + break + } + if bytes.Compare(origin[:], entry.k) <= 0 { + keys = append(keys, common.BytesToHash(entry.k)) + vals = append(vals, entry.v) + size += 32 + len(entry.v) + } + if bytes.Compare(entry.k, limit[:]) >= 0 { + break + } + } + proof := trienode.NewProofSet() + if err := t.accountTrie.Prove(origin[:], proof); err != nil { + t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err) + } + if len(keys) > 0 { + lastK := (keys[len(keys)-1])[:] + if err := t.accountTrie.Prove(lastK, proof); err != nil { + t.logger.Error("Could not prove last item", "error", err) + } + } + return keys, vals, proof.List() +} + +func createStorageRequestResponseV2(t *testPeerV2, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) { + var size int + for _, account := range accounts { + var originHash common.Hash + if len(origin) > 0 { + originHash = common.BytesToHash(origin) + } + var limitHash = common.MaxHash + if len(limit) > 0 { + limitHash = common.BytesToHash(limit) + } + var ( + keys []common.Hash + vals [][]byte + abort bool + ) + for _, entry := range t.storageValues[account] { + if size >= max { + abort = true + break + } + if bytes.Compare(entry.k, originHash[:]) < 0 { + continue + } + keys = append(keys, common.BytesToHash(entry.k)) + vals = append(vals, entry.v) + size += 32 + len(entry.v) + if bytes.Compare(entry.k, limitHash[:]) >= 0 { + break + } + } + if len(keys) > 0 { + hashes = append(hashes, keys) + slots = append(slots, vals) + } + if originHash != (common.Hash{}) || (abort && len(keys) > 0) { + proof := trienode.NewProofSet() + stTrie := t.storageTries[account] + + if err := stTrie.Prove(originHash[:], proof); err != nil { + t.logger.Error("Could not prove inexistence of origin", "origin", originHash, "error", err) + } + if len(keys) > 0 { + lastK := (keys[len(keys)-1])[:] + if err := stTrie.Prove(lastK, proof); err != nil { + t.logger.Error("Could not prove last item", "error", err) + } + } + proofs = append(proofs, proof.List()...) + break + } + } + return hashes, slots, proofs +} + +func createStorageRequestResponseAlwaysProveV2(t *testPeerV2, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max int) (hashes [][]common.Hash, slots [][][]byte, proofs [][]byte) { + var size int + max = max * 3 / 4 + + var origin common.Hash + if len(bOrigin) > 0 { + origin = common.BytesToHash(bOrigin) + } + var exit bool + for i, account := range accounts { + var keys []common.Hash + var vals [][]byte + for _, entry := range t.storageValues[account] { + if bytes.Compare(entry.k, origin[:]) < 0 { + exit = true + } + keys = append(keys, common.BytesToHash(entry.k)) + vals = append(vals, entry.v) + size += 32 + len(entry.v) + if size > max { + exit = true + } + } + if i == len(accounts)-1 { + exit = true + } + hashes = append(hashes, keys) + slots = append(slots, vals) + + if exit { + proof := trienode.NewProofSet() + stTrie := t.storageTries[account] + + if err := stTrie.Prove(origin[:], proof); err != nil { + t.logger.Error("Could not prove inexistence of origin", "origin", origin, "error", err) + } + if len(keys) > 0 { + lastK := (keys[len(keys)-1])[:] + if err := stTrie.Prove(lastK, proof); err != nil { + t.logger.Error("Could not prove last item", "error", err) + } + } + proofs = append(proofs, proof.List()...) + break + } + } + return hashes, slots, proofs +} + +// defaultAccountRequestHandlerV2 is a well-behaving handler for AccountRangeRequests. +func defaultAccountRequestHandlerV2(t *testPeerV2, id uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + keys, vals, proofs := createAccountRequestResponseV2(t, root, origin, limit, cap) + if err := t.remote.OnAccounts(t, id, keys, vals, proofs); err != nil { + t.test.Errorf("Remote side rejected our delivery: %v", err) + t.term() + return err + } + return nil +} + +func defaultStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, bOrigin, bLimit []byte, max int) error { + hashes, slots, proofs := createStorageRequestResponseV2(t, root, accounts, bOrigin, bLimit, max) + if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { + t.test.Errorf("Remote side rejected our delivery: %v", err) + t.term() + } + return nil +} + +func defaultCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error { + var bytecodes [][]byte + for _, h := range hashes { + bytecodes = append(bytecodes, getCodeByHash(h)) + } + if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { + t.test.Errorf("Remote side rejected our delivery: %v", err) + t.term() + } + return nil +} + +// Misbehaving handlers. + +func emptyRequestAccountRangeFnV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + t.remote.OnAccounts(t, requestId, nil, nil, nil) + return nil +} + +func nonResponsiveRequestAccountRangeFnV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + return nil +} + +func emptyStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + t.remote.OnStorage(t, requestId, nil, nil, nil) + return nil +} + +func nonResponsiveStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + return nil +} + +func proofHappyStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + hashes, slots, proofs := createStorageRequestResponseAlwaysProveV2(t, root, accounts, origin, limit, max) + if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { + t.test.Errorf("Remote side rejected our delivery: %v", err) + t.term() + } + return nil +} + +func corruptCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error { + var bytecodes [][]byte + for _, h := range hashes { + bytecodes = append(bytecodes, h[:]) + } + if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { + t.logger.Info("remote error on delivery (as expected)", "error", err) + t.remote.Unregister(t.id) + } + return nil +} + +func cappedCodeRequestHandlerV2(t *testPeerV2, id uint64, hashes []common.Hash, max int) error { + var bytecodes [][]byte + for _, h := range hashes[:1] { + bytecodes = append(bytecodes, getCodeByHash(h)) + } + if err := t.remote.OnByteCodes(t, id, bytecodes); err != nil { + t.test.Errorf("Remote side rejected our delivery: %v", err) + t.term() + } + return nil +} + +func starvingStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + return defaultStorageRequestHandlerV2(t, requestId, root, accounts, origin, limit, 500) +} + +func starvingAccountRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + return defaultAccountRequestHandlerV2(t, requestId, root, origin, limit, 500) +} + +func corruptAccountRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + hashes, accounts, proofs := createAccountRequestResponseV2(t, root, origin, limit, cap) + if len(proofs) > 0 { + proofs = proofs[1:] + } + if err := t.remote.OnAccounts(t, requestId, hashes, accounts, proofs); err != nil { + t.logger.Info("remote error on delivery (as expected)", "error", err) + t.remote.Unregister(t.id) + } + return nil +} + +func corruptStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + hashes, slots, proofs := createStorageRequestResponseV2(t, root, accounts, origin, limit, max) + if len(proofs) > 0 { + proofs = proofs[1:] + } + if err := t.remote.OnStorage(t, requestId, hashes, slots, proofs); err != nil { + t.logger.Info("remote error on delivery (as expected)", "error", err) + t.remote.Unregister(t.id) + } + return nil +} + +func noProofStorageRequestHandlerV2(t *testPeerV2, requestId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + hashes, slots, _ := createStorageRequestResponseV2(t, root, accounts, origin, limit, max) + if err := t.remote.OnStorage(t, requestId, hashes, slots, nil); err != nil { + t.logger.Info("remote error on delivery (as expected)", "error", err) + t.remote.Unregister(t.id) + } + return nil +} + +func setupSyncerV2(scheme string, peers ...*testPeerV2) *SyncerV2 { + stateDb := rawdb.NewMemoryDatabase() + syncer := NewSyncerV2(stateDb, scheme) + for _, peer := range peers { + syncer.Register(peer) + peer.remote = syncer + } + return syncer +} + +// verifyFlatState checks that the database contains the snapshot entries for +// every expected account and storage slot, plus the bytecode for every account +// that has one. Trie node presence is intentionally not checked: SyncerV2 only +// downloads flat state. +func verifyFlatState(t *testing.T, db ethdb.KeyValueStore, accountValues []*kv, storageValues map[common.Hash][]*kv) { + t.Helper() + + for _, entry := range accountValues { + hash := common.BytesToHash(entry.k) + got := rawdb.ReadAccountSnapshot(db, hash) + if got == nil { + t.Fatalf("missing account snapshot for %x", hash) + } + var acc types.StateAccount + if err := rlp.DecodeBytes(entry.v, &acc); err != nil { + t.Fatalf("failed to decode source account %x: %v", hash, err) + } + want := types.SlimAccountRLP(acc) + if !bytes.Equal(got, want) { + t.Fatalf("account snapshot mismatch for %x:\n got %x\n want %x", hash, got, want) + } + if !bytes.Equal(acc.CodeHash, types.EmptyCodeHash.Bytes()) { + if !rawdb.HasCode(db, common.BytesToHash(acc.CodeHash)) { + t.Fatalf("missing code for hash %x (account %x)", acc.CodeHash, hash) + } + } + } + var accounts, slots int + for _, entry := range accountValues { + accounts++ + account := common.BytesToHash(entry.k) + for _, slot := range storageValues[account] { + slotHash := common.BytesToHash(slot.k) + got := rawdb.ReadStorageSnapshot(db, account, slotHash) + if got == nil { + t.Fatalf("missing storage snapshot for account %x slot %x", account, slotHash) + } + if !bytes.Equal(got, slot.v) { + t.Fatalf("storage snapshot mismatch for account %x slot %x:\n got %x\n want %x", account, slotHash, got, slot.v) + } + slots++ + } + } + t.Logf("flat state verified: accounts=%d slots=%d", accounts, slots) +} + +// TestSyncV2 tests a basic sync with one peer. +func TestSyncV2(t *testing.T) { + t.Parallel() + testSyncV2(t, rawdb.HashScheme) + testSyncV2(t, rawdb.PathScheme) +} + +func testSyncV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + return source + } + syncer := setupSyncerV2(nodeScheme, mkSource("source")) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncTinyTriePanicV2 tests a basic sync with one peer and a tiny trie. +func TestSyncTinyTriePanicV2(t *testing.T) { + t.Parallel() + testSyncTinyTriePanicV2(t, rawdb.HashScheme) + testSyncTinyTriePanicV2(t, rawdb.PathScheme) +} + +func testSyncTinyTriePanicV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(1, scheme) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + return source + } + syncer := setupSyncerV2(nodeScheme, mkSource("source")) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestMultiSyncV2 tests a basic sync with multiple peers. +func TestMultiSyncV2(t *testing.T) { + t.Parallel() + testMultiSyncV2(t, rawdb.HashScheme) + testMultiSyncV2(t, rawdb.PathScheme) +} + +func testMultiSyncV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + return source + } + syncer := setupSyncerV2(nodeScheme, mkSource("sourceA"), mkSource("sourceB")) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncWithStorageV2 tests basic sync using accounts + storage + code. +func TestSyncWithStorageV2(t *testing.T) { + t.Parallel() + testSyncWithStorageV2(t, rawdb.HashScheme) + testSyncWithStorageV2(t, rawdb.PathScheme) +} + +func testSyncWithStorageV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, false) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + return source + } + syncer := setupSyncerV2(scheme, mkSource("sourceA")) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestMultiSyncManyUselessV2 keeps one good peer and several that return empty. +func TestMultiSyncManyUselessV2(t *testing.T) { + t.Parallel() + testMultiSyncManyUselessV2(t, rawdb.HashScheme) + testMultiSyncManyUselessV2(t, rawdb.PathScheme) +} + +func testMultiSyncManyUselessV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) + + mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + if noAccount { + source.accountRequestHandler = emptyRequestAccountRangeFnV2 + } + if noStorage { + source.storageRequestHandler = emptyStorageRequestHandlerV2 + } + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("full", false, false), + mkSource("noAccounts", true, false), + mkSource("noStorage", false, true), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestMultiSyncManyUselessWithLowTimeoutV2 is the same as above but with a very +// low timeout, exercising the timeout/reschedule paths. +func TestMultiSyncManyUselessWithLowTimeoutV2(t *testing.T) { + t.Parallel() + testMultiSyncManyUselessWithLowTimeoutV2(t, rawdb.HashScheme) + testMultiSyncManyUselessWithLowTimeoutV2(t, rawdb.PathScheme) +} + +func testMultiSyncManyUselessWithLowTimeoutV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) + + mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + if !noAccount { + source.accountRequestHandler = emptyRequestAccountRangeFnV2 + } + if !noStorage { + source.storageRequestHandler = emptyStorageRequestHandlerV2 + } + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("full", true, true), + mkSource("noAccounts", false, true), + mkSource("noStorage", true, false), + ) + syncer.rates.OverrideTTLLimit = time.Millisecond + + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestMultiSyncManyUnresponsiveV2 keeps one good peer and several that don't +// respond at all. +func TestMultiSyncManyUnresponsiveV2(t *testing.T) { + t.Parallel() + testMultiSyncManyUnresponsiveV2(t, rawdb.HashScheme) + testMultiSyncManyUnresponsiveV2(t, rawdb.PathScheme) +} + +func testMultiSyncManyUnresponsiveV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) + + mkSource := func(name string, noAccount, noStorage bool) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + if noAccount { + source.accountRequestHandler = nonResponsiveRequestAccountRangeFnV2 + } + if noStorage { + source.storageRequestHandler = nonResponsiveStorageRequestHandlerV2 + } + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("full", false, false), + mkSource("noAccounts", true, false), + mkSource("noStorage", false, true), + ) + syncer.rates.OverrideTTLLimit = time.Millisecond + + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestSyncBoundaryAccountTrieV2 tests sync against a few normal peers, but the +// account trie has a few boundary elements. +func TestSyncBoundaryAccountTrieV2(t *testing.T) { + t.Parallel() + testSyncBoundaryAccountTrieV2(t, rawdb.HashScheme) + testSyncBoundaryAccountTrieV2(t, rawdb.PathScheme) +} + +func testSyncBoundaryAccountTrieV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeBoundaryAccountTrie(scheme, 3000) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + return source + } + syncer := setupSyncerV2( + nodeScheme, + mkSource("peer-a"), + mkSource("peer-b"), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncNoStorageAndOneCappedPeerV2 tests sync using accounts and no storage, +// where one peer is consistently returning very small results. +func TestSyncNoStorageAndOneCappedPeerV2(t *testing.T) { + t.Parallel() + testSyncNoStorageAndOneCappedPeerV2(t, rawdb.HashScheme) + testSyncNoStorageAndOneCappedPeerV2(t, rawdb.PathScheme) +} + +func testSyncNoStorageAndOneCappedPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) + + mkSource := func(name string, slow bool) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + if slow { + source.accountRequestHandler = starvingAccountRequestHandlerV2 + } + return source + } + + syncer := setupSyncerV2( + nodeScheme, + mkSource("nice-a", false), + mkSource("nice-b", false), + mkSource("nice-c", false), + mkSource("capped", true), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncNoStorageAndOneCodeCorruptPeerV2 has one peer that doesn't deliver +// code requests properly. +func TestSyncNoStorageAndOneCodeCorruptPeerV2(t *testing.T) { + t.Parallel() + testSyncNoStorageAndOneCodeCorruptPeerV2(t, rawdb.HashScheme) + testSyncNoStorageAndOneCodeCorruptPeerV2(t, rawdb.PathScheme) +} + +func testSyncNoStorageAndOneCodeCorruptPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) + + mkSource := func(name string, codeFn codeHandlerFuncV2) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.codeRequestHandler = codeFn + return source + } + syncer := setupSyncerV2( + nodeScheme, + mkSource("capped", cappedCodeRequestHandlerV2), + mkSource("corrupt", corruptCodeRequestHandlerV2), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +func TestSyncNoStorageAndOneAccountCorruptPeerV2(t *testing.T) { + t.Parallel() + testSyncNoStorageAndOneAccountCorruptPeerV2(t, rawdb.HashScheme) + testSyncNoStorageAndOneAccountCorruptPeerV2(t, rawdb.PathScheme) +} + +func testSyncNoStorageAndOneAccountCorruptPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) + + mkSource := func(name string, accFn accountHandlerFuncV2) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.accountRequestHandler = accFn + return source + } + syncer := setupSyncerV2( + nodeScheme, + mkSource("capped", starvingAccountRequestHandlerV2), + mkSource("corrupt", corruptAccountRequestHandlerV2), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncNoStorageAndOneCodeCappedPeerV2 has one peer that delivers code +// hashes one by one. +func TestSyncNoStorageAndOneCodeCappedPeerV2(t *testing.T) { + t.Parallel() + testSyncNoStorageAndOneCodeCappedPeerV2(t, rawdb.HashScheme) + testSyncNoStorageAndOneCodeCappedPeerV2(t, rawdb.PathScheme) +} + +func testSyncNoStorageAndOneCodeCappedPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(3000, scheme) + + mkSource := func(name string, codeFn codeHandlerFuncV2) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.codeRequestHandler = codeFn + return source + } + var counter int + syncer := setupSyncerV2( + nodeScheme, + mkSource("capped", func(t *testPeerV2, id uint64, hashes []common.Hash, max int) error { + counter++ + return cappedCodeRequestHandlerV2(t, id, hashes, max) + }), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + + if threshold := 100; counter > threshold { + t.Logf("Error, expected < %d invocations, got %d", threshold, counter) + } + verifyFlatState(t, syncer.db, elems, nil) +} + +// TestSyncBoundaryStorageTrieV2 tests sync against a few normal peers, but the +// storage trie has a few boundary elements. +func TestSyncBoundaryStorageTrieV2(t *testing.T) { + t.Parallel() + testSyncBoundaryStorageTrieV2(t, rawdb.HashScheme) + testSyncBoundaryStorageTrieV2(t, rawdb.PathScheme) +} + +func testSyncBoundaryStorageTrieV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true, false) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("peer-a"), + mkSource("peer-b"), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestSyncWithStorageAndOneCappedPeerV2 tests sync using accounts + storage, +// where one peer is consistently returning very small results. +func TestSyncWithStorageAndOneCappedPeerV2(t *testing.T) { + t.Parallel() + testSyncWithStorageAndOneCappedPeerV2(t, rawdb.HashScheme) + testSyncWithStorageAndOneCappedPeerV2(t, rawdb.PathScheme) +} + +func testSyncWithStorageAndOneCappedPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 1000, false, false, false) + + mkSource := func(name string, slow bool) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + if slow { + source.storageRequestHandler = starvingStorageRequestHandlerV2 + } + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("nice-a", false), + mkSource("slow", true), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestSyncWithStorageAndCorruptPeerV2 tests sync using accounts + storage, +// where one peer is sometimes sending bad proofs. +func TestSyncWithStorageAndCorruptPeerV2(t *testing.T) { + t.Parallel() + testSyncWithStorageAndCorruptPeerV2(t, rawdb.HashScheme) + testSyncWithStorageAndCorruptPeerV2(t, rawdb.PathScheme) +} + +func testSyncWithStorageAndCorruptPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) + + mkSource := func(name string, handler storageHandlerFuncV2) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + source.storageRequestHandler = handler + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("nice-a", defaultStorageRequestHandlerV2), + mkSource("nice-b", defaultStorageRequestHandlerV2), + mkSource("nice-c", defaultStorageRequestHandlerV2), + mkSource("corrupt", corruptStorageRequestHandlerV2), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +func TestSyncWithStorageAndNonProvingPeerV2(t *testing.T) { + t.Parallel() + testSyncWithStorageAndNonProvingPeerV2(t, rawdb.HashScheme) + testSyncWithStorageAndNonProvingPeerV2(t, rawdb.PathScheme) +} + +func testSyncWithStorageAndNonProvingPeerV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) + + mkSource := func(name string, handler storageHandlerFuncV2) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + source.storageRequestHandler = handler + return source + } + syncer := setupSyncerV2( + scheme, + mkSource("nice-a", defaultStorageRequestHandlerV2), + mkSource("nice-b", defaultStorageRequestHandlerV2), + mkSource("nice-c", defaultStorageRequestHandlerV2), + mkSource("corrupt", noProofStorageRequestHandlerV2), + ) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestSyncWithStorageMisbehavingProveV2 tests basic sync using accounts + +// storage + code against a peer that insists on delivering full storage sets +// _and_ proofs. +func TestSyncWithStorageMisbehavingProveV2(t *testing.T) { + t.Parallel() + testSyncWithStorageMisbehavingProveV2(t, rawdb.HashScheme) + testSyncWithStorageMisbehavingProveV2(t, rawdb.PathScheme) +} + +func testSyncWithStorageMisbehavingProveV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorageWithUniqueStorage(scheme, 10, 30, false) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + source.storageRequestHandler = proofHappyStorageRequestHandlerV2 + return source + } + syncer := setupSyncerV2(nodeScheme, mkSource("sourceA")) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + verifyFlatState(t, syncer.db, elems, storageElems) +} + +// TestSyncWithUnevenStorageV2 tests sync where the storage trie is not even +// and with a few empty ranges. +func TestSyncWithUnevenStorageV2(t *testing.T) { + t.Parallel() + testSyncWithUnevenStorageV2(t, rawdb.HashScheme) + testSyncWithUnevenStorageV2(t, rawdb.PathScheme) +} + +func testSyncWithUnevenStorageV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + accountTrie, accounts, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 256, false, false, true) + + mkSource := func(name string) *testPeerV2 { + source := newTestPeerV2(name, t, term) + source.accountTrie = accountTrie.Copy() + source.accountValues = accounts + source.setStorageTries(storageTries) + source.storageValues = storageElems + source.storageRequestHandler = func(t *testPeerV2, reqId uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, max int) error { + return defaultStorageRequestHandlerV2(t, reqId, root, accounts, origin, limit, 128) + } + return source + } + syncer := setupSyncerV2(scheme, mkSource("source")) + if err := syncer.Sync(accountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + verifyFlatState(t, syncer.db, accounts, storageElems) +} + +// TestSyncBloatedProofV2 tests a scenario where the peer ships only one value +// but inflates the proof with the entire trie. If the attack is successful the +// remote side does not do any follow-up requests. +func TestSyncBloatedProofV2(t *testing.T) { + t.Parallel() + testSyncBloatedProofV2(t, rawdb.HashScheme) + testSyncBloatedProofV2(t, rawdb.PathScheme) +} + +func testSyncBloatedProofV2(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { once.Do(func() { close(cancel) }) } + ) + nodeScheme, sourceAccountTrie, elems := makeAccountTrieNoStorage(100, scheme) + source := newTestPeerV2("source", t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + + source.accountRequestHandler = func(t *testPeerV2, requestId uint64, root common.Hash, origin common.Hash, limit common.Hash, cap int) error { + var ( + keys []common.Hash + vals [][]byte + ) + for _, entry := range t.accountValues { + if bytes.Compare(entry.k, origin[:]) < 0 { + continue + } + if bytes.Compare(entry.k, limit[:]) > 0 { + continue + } + keys = append(keys, common.BytesToHash(entry.k)) + vals = append(vals, entry.v) + } + proof := trienode.NewProofSet() + if err := t.accountTrie.Prove(origin[:], proof); err != nil { + t.logger.Error("Could not prove origin", "origin", origin, "error", err) + } + for _, entry := range t.accountValues { + if err := t.accountTrie.Prove(entry.k, proof); err != nil { + t.logger.Error("Could not prove item", "error", err) + } + } + if len(keys) > 2 { + keys = append(keys[:1], keys[2:]...) + vals = append(vals[:1], vals[2:]...) + } + if err := t.remote.OnAccounts(t, requestId, keys, vals, proof.List()); err != nil { + t.logger.Info("remote error on delivery (as expected)", "error", err) + t.term() + } + return nil + } + syncer := setupSyncerV2(nodeScheme, source) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err == nil { + t.Fatal("No error returned from incomplete/cancelled sync") + } +} From 19f5fe079b462c303cafae084236a137b1594ea0 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:13:06 -0500 Subject: [PATCH 61/76] rpc, internal/telemetry: trace JSON-RPC response writes (#35049) The per-call SERVER span ended inside `handleCall()`, so the JSON-RPC response write happened after the span closed. For large responses like `engine_getBlobsV*`, that write time was missing from traces. - Extend the SERVER span past `writeJSON`. - For batches, add a top-level `jsonrpc.batch` SERVER span (with `rpc.batch.size`) covering the whole batch including `callBuffer.write`. - Add `rpc.writeJSON` span around the non-batch response write. - Add `rpc.writeJSONBatch` span around the batch response write. - Add `rpc.httpWrite` span around the actual HTTP write, separating JSON encoding from network write. - Add additional telemetry helpers. --------- Co-authored-by: Felix Lange --- core/state_processor.go | 2 +- eth/catalyst/api.go | 10 +- eth/catalyst/simulated_beacon.go | 8 +- internal/telemetry/telemetry.go | 46 ++++- miner/payload_building.go | 4 +- miner/worker.go | 6 +- rpc/handler.go | 160 ++++++++++----- rpc/http.go | 22 +- rpc/json.go | 12 +- rpc/service.go | 12 +- rpc/tracing_test.go | 337 +++++++++++++++++++++++++++++-- rpc/websocket.go | 4 +- 12 files changed, 512 insertions(+), 111 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 5092379056..5f43206eb4 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -104,7 +104,7 @@ func (p *StateProcessor) Process(ctx context.Context, block *types.Block, stated statedb.SetTxContext(tx.Hash(), i, uint32(i+1)) _, _, spanEnd := telemetry.StartSpan(ctx, "core.ApplyTransactionWithEVM", telemetry.StringAttribute("tx.hash", tx.Hash().Hex()), - telemetry.Int64Attribute("tx.index", int64(i)), + telemetry.IntAttribute("tx.index", i), ) receipt, bal, err := ApplyTransactionWithEVM(msg, gp, statedb, blockNumber, blockHash, context.Time, tx, evm) if err != nil { diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 71e92e315d..1de2c80848 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -556,12 +556,12 @@ func (api *ConsensusAPI) GetBlobsV1(ctx context.Context, hashes []common.Hash) ( var ( filled int attrs = []telemetry.Attribute{ - telemetry.Int64Attribute("blobs.requested", int64(len(hashes))), + telemetry.IntAttribute("blobs.requested", len(hashes)), } ) ctx, span, spanEnd := telemetry.StartSpan(ctx, "engine.getBlobsV1", attrs...) defer func() { - span.SetAttributes(telemetry.Int64Attribute("blobs.filled", int64(filled))) + span.SetAttributes(telemetry.IntAttribute("blobs.filled", filled)) spanEnd(&err) }() @@ -643,12 +643,12 @@ func (api *ConsensusAPI) getBlobs(ctx context.Context, hashes []common.Hash, v2 var ( filled int attrs = []telemetry.Attribute{ - telemetry.Int64Attribute("blobs.requested", int64(len(hashes))), + telemetry.IntAttribute("blobs.requested", len(hashes)), } ) ctx, span, spanEnd := telemetry.StartSpan(ctx, "engine.getBlobs", attrs...) defer func() { - span.SetAttributes(telemetry.Int64Attribute("blobs.filled", int64(filled))) + span.SetAttributes(telemetry.IntAttribute("blobs.filled", filled)) spanEnd(&err) }() @@ -833,7 +833,7 @@ func (api *ConsensusAPI) newPayload(ctx context.Context, params engine.Executabl var attrs = []telemetry.Attribute{ telemetry.Int64Attribute("block.number", int64(params.Number)), telemetry.StringAttribute("block.hash", params.BlockHash.Hex()), - telemetry.Int64Attribute("tx.count", int64(len(params.Transactions))), + telemetry.IntAttribute("tx.count", len(params.Transactions)), } ctx, _, spanEnd := telemetry.StartSpan(ctx, "engine.newPayload", attrs...) defer spanEnd(&err) diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index 8a77cd8abe..7120c14501 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -215,7 +215,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u // Create a server span for forkchoiceUpdated with payload attributes, // simulating an incoming engine API request from a real consensus client. - fcCtx, fcSpanEnd := telemetry.StartServerSpan(context.Background(), tracer, telemetry.RPCInfo{ + fcCtx, fcSpanEnd := telemetry.StartCallServerSpan(context.Background(), tracer, telemetry.RPCInfo{ System: "jsonrpc", Service: "engine", Method: "forkchoiceUpdatedV" + fmt.Sprintf("%d", version), @@ -237,7 +237,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u // Create a server span for getPayload, simulating the consensus client // coming back to retrieve the built payload. - _, gpSpanEnd := telemetry.StartServerSpan(context.Background(), tracer, telemetry.RPCInfo{ + _, gpSpanEnd := telemetry.StartCallServerSpan(context.Background(), tracer, telemetry.RPCInfo{ System: "jsonrpc", Service: "engine", Method: "getPayloadV" + fmt.Sprintf("%d", version), @@ -286,7 +286,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u // Create a server span for newPayload, simulating the consensus client // sending the execution payload for validation. - npCtx, npSpanEnd := telemetry.StartServerSpan(context.Background(), tracer, telemetry.RPCInfo{ + npCtx, npSpanEnd := telemetry.StartCallServerSpan(context.Background(), tracer, telemetry.RPCInfo{ System: "jsonrpc", Service: "engine", Method: "newPayloadV" + fmt.Sprintf("%d", version), @@ -302,7 +302,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u // Create a server span for the final forkchoiceUpdated (no payload attributes), // which sets the new block as the canonical chain head. - fcuCtx, fcuSpanEnd := telemetry.StartServerSpan(context.Background(), tracer, telemetry.RPCInfo{ + fcuCtx, fcuSpanEnd := telemetry.StartCallServerSpan(context.Background(), tracer, telemetry.RPCInfo{ System: "jsonrpc", Service: "engine", Method: "forkchoiceUpdatedV" + fmt.Sprintf("%d", version), diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 27fe9b0a7a..ed598064b3 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -40,6 +40,11 @@ func Int64Attribute(key string, val int64) Attribute { return attribute.Int64(key, val) } +// IntAttribute creates an attribute with an int value. +func IntAttribute(key string, val int) Attribute { + return attribute.Int(key, val) +} + // BoolAttribute creates an attribute with a bool value. func BoolAttribute(key string, val bool) Attribute { return attribute.Bool(key, val) @@ -60,6 +65,13 @@ func StartSpanWithTracer(ctx context.Context, tracer trace.Tracer, name string, return startSpan(ctx, tracer, trace.SpanKindInternal, name, attributes...) } +// TracerFromContext returns a Tracer from the TracerProvider associated with the +// parent span in ctx. If ctx has no parent span, the returned tracer comes from +// the no-op provider, so spans created with it will not be exported. +func TracerFromContext(ctx context.Context) trace.Tracer { + return trace.SpanFromContext(ctx).TracerProvider().Tracer("") +} + // RPCInfo contains information about the RPC request. type RPCInfo struct { System string @@ -68,11 +80,11 @@ type RPCInfo struct { RequestID string } -// StartServerSpan creates a SpanKind=SERVER span at the JSON-RPC boundary. +// StartCallServerSpan creates a SpanKind=SERVER span for a JSON-RPC call. // The span name is formatted as $rpcSystem.$rpcService/$rpcMethod // (e.g. "jsonrpc.engine/newPayloadV4") which follows the Open Telemetry // semantic convensions: https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#span-name. -func StartServerSpan(ctx context.Context, tracer trace.Tracer, rpc RPCInfo, others ...Attribute) (context.Context, func(*error)) { +func StartCallServerSpan(ctx context.Context, tracer trace.Tracer, rpc RPCInfo, others ...Attribute) (context.Context, func(*error)) { var ( name = fmt.Sprintf("%s.%s/%s", rpc.System, rpc.Service, rpc.Method) attributes = append([]Attribute{ @@ -88,6 +100,36 @@ func StartServerSpan(ctx context.Context, tracer trace.Tracer, rpc RPCInfo, othe return ctx, end } +// StartBatchServerSpan creates a SpanKind=SERVER span representing a batched request. +// The span name is "$system.batch" (e.g. "jsonrpc.batch") and per-call spans are nested under it. +// batchSize is exposed as rpc.batch.size. +func StartBatchServerSpan(ctx context.Context, tracer trace.Tracer, system string, batchSize int, others ...Attribute) (context.Context, func(*error)) { + attributes := append([]Attribute{ + semconv.RPCSystemKey.String(system), + IntAttribute("rpc.batch.size", batchSize), + }, others...) + ctx, _, end := startSpan(ctx, tracer, trace.SpanKindServer, system+".batch", attributes...) + return ctx, end +} + +// StartBatchCallSpan creates a SpanKind=INTERNAL span for an individual RPC call as part of a batch. +// This carries the same name and attributes as StartCallServerSpan. +func StartBatchCallSpan(ctx context.Context, tracer trace.Tracer, rpc RPCInfo, others ...Attribute) (context.Context, func(*error)) { + var ( + name = fmt.Sprintf("%s.%s/%s", rpc.System, rpc.Service, rpc.Method) + attributes = append([]Attribute{ + semconv.RPCSystemKey.String(rpc.System), + semconv.RPCServiceKey.String(rpc.Service), + semconv.RPCMethodKey.String(rpc.Method), + semconv.RPCJSONRPCRequestID(rpc.RequestID), + }, + others..., + ) + ) + ctx, _, end := startSpan(ctx, tracer, trace.SpanKindInternal, name, attributes...) + return ctx, end +} + // startSpan creates a span with the given kind. func startSpan(ctx context.Context, tracer trace.Tracer, kind trace.SpanKind, spanName string, attributes ...Attribute) (context.Context, trace.Span, func(*error)) { ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(kind)) diff --git a/miner/payload_building.go b/miner/payload_building.go index db8126828a..a2cc8df9d0 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -217,7 +217,7 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope { func (miner *Miner) runBuildIteration(ctx context.Context, start time.Time, iteration int, payload *Payload, params *generateParams, witness bool) { ctx, span, spanEnd := telemetry.StartSpan(ctx, "miner.buildIteration", - telemetry.Int64Attribute("iteration", int64(iteration)), + telemetry.IntAttribute("iteration", iteration), ) var err error defer spanEnd(&err) @@ -271,7 +271,7 @@ func (miner *Miner) buildPayload(ctx context.Context, args *BuildPayloadArgs, wi telemetry.Int64Attribute("block.number", int64(empty.block.NumberU64())), ) defer func() { - bSpan.SetAttributes(telemetry.Int64Attribute("iterations.total", int64(iteration))) + bSpan.SetAttributes(telemetry.IntAttribute("iterations.total", iteration)) bSpanEnd(nil) }() diff --git a/miner/worker.go b/miner/worker.go index 21bc95cf92..b0e144c0ab 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -137,7 +137,7 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams, defer func() { if result != nil && result.err == nil { span.SetAttributes( - telemetry.Int64Attribute("txs.count", int64(len(result.block.Transactions()))), + telemetry.IntAttribute("txs.count", len(result.block.Transactions())), telemetry.Int64Attribute("gas.used", int64(result.block.GasUsed())), telemetry.StringAttribute("fees", result.fees.String()), ) @@ -572,8 +572,8 @@ func (miner *Miner) fillTransactions(ctx context.Context, interrupt *atomic.Int3 } pendingBlobTxs, blobTxCount := miner.txpool.Pending(filter) span.SetAttributes( - telemetry.Int64Attribute("pending.plain.count", int64(plainTxCount)), - telemetry.Int64Attribute("pending.blob.count", int64(blobTxCount)), + telemetry.IntAttribute("pending.plain.count", plainTxCount), + telemetry.IntAttribute("pending.blob.count", blobTxCount), ) // Split the pending transactions into locals and remotes. diff --git a/rpc/handler.go b/rpc/handler.go index a9ffdc7071..e39ca78e2e 100644 --- a/rpc/handler.go +++ b/rpc/handler.go @@ -169,40 +169,49 @@ func (b *batchCallBuffer) doWrite(ctx context.Context, conn jsonWriter, isErrorR } b.wrote = true // can only write once if len(b.resp) > 0 { - conn.writeJSONBatch(ctx, b.resp, isErrorResponse) + spanCtx, _, spanEnd := telemetry.StartSpanWithTracer(ctx, telemetry.TracerFromContext(ctx), "rpc.writeJSONBatch") + err := conn.writeJSONBatch(spanCtx, b.resp, isErrorResponse) + spanEnd(&err) } } // handleBatch executes all messages in a batch and returns the responses. func (h *handler) handleBatch(msgs []*jsonrpcMessage) { - // Emit error response for empty batches: - if len(msgs) == 0 { - h.startCallProc(func(cp *callProc) { - resp := errorMessage(&invalidRequestError{"empty batch"}) - h.conn.writeJSON(cp.ctx, resp, true) + // For valid batches, filter response messages and subscription notifications + // out of msgs here. + var calls []*jsonrpcMessage + valid := len(msgs) > 0 && (h.batchRequestLimit == 0 || len(msgs) <= h.batchRequestLimit) + if valid { + calls = make([]*jsonrpcMessage, 0, len(msgs)) + h.handleResponses(msgs, func(msg *jsonrpcMessage) { + calls = append(calls, msg) }) - return - } - // Apply limit on total number of requests. - if h.batchRequestLimit != 0 && len(msgs) > h.batchRequestLimit { - h.startCallProc(func(cp *callProc) { - h.respondWithBatchTooLarge(cp, msgs) - }) - return - } - - // Handle non-call messages first. - // Here we need to find the requestOp that sent the request batch. - calls := make([]*jsonrpcMessage, 0, len(msgs)) - h.handleResponses(msgs, func(msg *jsonrpcMessage) { - calls = append(calls, msg) - }) - if len(calls) == 0 { - return + if len(calls) == 0 { + // Batch was entirely responses to our own requests; nothing to dispatch. + return + } } // Process calls on a goroutine because they may block indefinitely: h.startCallProc(func(cp *callProc) { + // Top-level batch SERVER span. + var batchSpanEnd func(*error) + cp.ctx, batchSpanEnd = telemetry.StartBatchServerSpan(cp.ctx, h.tracer(), "jsonrpc", len(msgs)) + var spanErr error + defer batchSpanEnd(&spanErr) + + switch { + case len(msgs) == 0: + spanErr = &invalidRequestError{"empty batch"} + resp := errorMessage(spanErr) + h.conn.writeJSON(cp.ctx, resp, true) + return + case h.batchRequestLimit != 0 && len(msgs) > h.batchRequestLimit: + spanErr = errors.New(errMsgBatchTooLarge) + h.respondWithBatchTooLarge(cp, msgs) + return + } + cp.isBatch = true var ( timer *time.Timer @@ -212,35 +221,50 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) { cp.ctx, cancel = context.WithCancel(cp.ctx) defer cancel() + batchCtx := cp.ctx // Cancel the request context after timeout and send an error response. Since the // currently-running method might not return immediately on timeout, we must wait // for the timeout concurrently with processing the request. - if timeout, ok := ContextRequestTimeout(cp.ctx); ok { + if timeout, ok := ContextRequestTimeout(batchCtx); ok { timer = time.AfterFunc(timeout, func() { cancel() err := &internalServerError{errcodeTimeout, errMsgTimeout} - callBuffer.respondWithError(cp.ctx, h.conn, err) + callBuffer.respondWithError(batchCtx, h.conn, err) }) } responseBytes := 0 for { // No need to handle rest of calls if timed out. - if cp.ctx.Err() != nil { + if batchCtx.Err() != nil { break } msg := callBuffer.nextCall() if msg == nil { break } + + // Per-call INTERNAL span as a child of the batch SERVER span. + var callSpanEnd func(*error) + cp.ctx, callSpanEnd = telemetry.StartBatchCallSpan(batchCtx, h.tracer(), rpcInfoFromMessage(msg)) resp := h.handleCallMsg(cp, msg) + var callErr error + if resp != nil && resp.Error != nil { + callErr = resp.decodeError() + } + callSpanEnd(&callErr) + + // Notifications don't get a response written into the batch reply. + if msg.isNotification() { + resp = nil + } callBuffer.pushResponse(resp) if resp != nil && h.batchResponseMaxSize != 0 { responseBytes += len(resp.Result) + len(resp.Error) if responseBytes > h.batchResponseMaxSize { err := &internalServerError{errcodeResponseTooLarge, errMsgResponseTooLarge} - callBuffer.respondWithError(cp.ctx, h.conn, err) + callBuffer.respondWithError(batchCtx, h.conn, err) break } } @@ -250,7 +274,7 @@ func (h *handler) handleBatch(msgs []*jsonrpcMessage) { } h.addSubscriptions(cp.notifiers) - callBuffer.write(cp.ctx, h.conn) + callBuffer.write(batchCtx, h.conn) for _, n := range cp.notifiers { n.activate() } @@ -283,22 +307,32 @@ func (h *handler) handleMsg(msg *jsonrpcMessage) { func (h *handler) handleNonBatchCall(cp *callProc, msg *jsonrpcMessage) { var ( - responded sync.Once - timer *time.Timer - cancel context.CancelFunc + responded sync.Once + timer *time.Timer + cancel context.CancelFunc + responseError error ) - cp.ctx, cancel = context.WithCancel(cp.ctx) - defer cancel() + + // Set up the SERVER span for tracing. + var serverSpanEnd func(*error) + cp.ctx, serverSpanEnd = telemetry.StartCallServerSpan(cp.ctx, h.tracer(), rpcInfoFromMessage(msg)) + defer serverSpanEnd(&responseError) // Cancel the request context after timeout and send an error response. Since the // running method might not return immediately on timeout, we must wait for the // timeout concurrently with processing the request. + outerCtx := cp.ctx + cp.ctx, cancel = context.WithCancel(cp.ctx) + defer cancel() if timeout, ok := ContextRequestTimeout(cp.ctx); ok { timer = time.AfterFunc(timeout, func() { cancel() responded.Do(func() { + responseError = errors.New(errMsgTimeout) + writeCtx, _, writeSpanEnd := telemetry.StartSpanWithTracer(outerCtx, h.tracer(), "rpc.writeJSON") resp := msg.errorResponse(&internalServerError{errcodeTimeout, errMsgTimeout}) - h.conn.writeJSON(cp.ctx, resp, true) + err := h.conn.writeJSON(writeCtx, resp, true) + writeSpanEnd(&err) }) }) } @@ -310,9 +344,22 @@ func (h *handler) handleNonBatchCall(cp *callProc, msg *jsonrpcMessage) { h.addSubscriptions(cp.notifiers) if answer != nil { responded.Do(func() { - h.conn.writeJSON(cp.ctx, answer, false) + if answer.Error != nil { + responseError = answer.decodeError() + } + // Notifications don't get a response written, but their errors are + // still recorded on the SERVER span via responseError above. + if msg.isNotification() { + return + } + writeCtx, _, writeSpanEnd := telemetry.StartSpanWithTracer(outerCtx, h.tracer(), "rpc.writeJSON") + err := h.conn.writeJSON(writeCtx, answer, false) + writeSpanEnd(&err) }) } + + // Enable notification sending of subscriptions, since the response with + // subscription ID has now been sent. for _, n := range cp.notifiers { n.activate() } @@ -472,9 +519,11 @@ func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMess start := time.Now() switch { case msg.isNotification(): - h.handleCall(ctx, msg) + // Notifications don't get a response written to the client, but the + // answer is returned so the caller can record errors on the SERVER span. + resp := h.handleCall(ctx, msg) h.log.Debug("Served "+msg.Method, "duration", time.Since(start)) - return nil + return resp case msg.isCall(): resp := h.handleCall(ctx, msg) @@ -516,28 +565,15 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage } return h.runMethod(cp.ctx, msg, h.unsubscribeCb, args) } - callb, service, method := h.reg.callback(msg.Method) + callb := h.reg.callback(msg.Method) // If the method is not found, return an error. if callb == nil { return msg.errorResponse(&methodNotFoundError{method: msg.Method}) } - // Start root span for the request. - rpcInfo := telemetry.RPCInfo{ - System: "jsonrpc", - Service: service, - Method: method, - RequestID: string(msg.ID), - } - attrib := []telemetry.Attribute{ - telemetry.BoolAttribute("rpc.batch", cp.isBatch), - } - ctx, spanEnd := telemetry.StartServerSpan(cp.ctx, h.tracer(), rpcInfo, attrib...) - defer spanEnd(nil) // don't propagate errors to parent spans - // Start tracing span before parsing arguments. - _, _, pSpanEnd := telemetry.StartSpanWithTracer(ctx, h.tracer(), "rpc.parsePositionalArguments") + _, _, pSpanEnd := telemetry.StartSpanWithTracer(cp.ctx, h.tracer(), "rpc.parsePositionalArguments") args, pErr := parsePositionalArguments(msg.Params, callb.argTypes) pSpanEnd(&pErr) if pErr != nil { @@ -546,11 +582,11 @@ func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage start := time.Now() // Start tracing span before running the method. - rctx, _, rSpanEnd := telemetry.StartSpanWithTracer(ctx, h.tracer(), "rpc.runMethod") + rctx, _, rSpanEnd := telemetry.StartSpanWithTracer(cp.ctx, h.tracer(), "rpc.runMethod") answer := h.runMethod(rctx, msg, callb, args) var rErr error if answer.Error != nil { - rErr = errors.New(answer.decodeError().Message) + rErr = answer.decodeError() } rSpanEnd(&rErr) @@ -603,6 +639,18 @@ func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMes return h.runMethod(ctx, msg, callb, args) } +// rpcInfoFromMessage builds the RPCInfo for a SERVER/INTERNAL RPC span from a +// JSON-RPC message. +func rpcInfoFromMessage(msg *jsonrpcMessage) telemetry.RPCInfo { + info := telemetry.RPCInfo{System: "jsonrpc", RequestID: string(msg.ID)} + if service, method, ok := serviceAndMethod(msg.Method); ok { + info.Service, info.Method = service, method + } else { + info.Method = msg.Method + } + return info +} + // tracer returns the OpenTelemetry Tracer for RPC call tracing. func (h *handler) tracer() trace.Tracer { if h.tracerProvider == nil { @@ -623,7 +671,7 @@ func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *cal _, _, spanEnd := telemetry.StartSpanWithTracer(ctx, h.tracer(), "rpc.encodeJSONResponse", attributes...) response := msg.response(result) if response.Error != nil { - err = errors.New(response.decodeError().Message) + err = response.decodeError() } spanEnd(&err) return response diff --git a/rpc/http.go b/rpc/http.go index 93f5e26c30..2bd761e9cd 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -31,6 +31,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/internal/telemetry" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" ) @@ -269,13 +270,13 @@ func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) Serve conn := &httpServerConn{Reader: body, Writer: w, r: r} var buf []byte - encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + encodeMsg := func(ctx context.Context, msg *jsonrpcMessage, isError bool) error { buf = appendMessage(buf[:0], msg) - return httpWriteResult(w, buf, isError) + return httpWrite(ctx, w, buf, isError) } - encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + encodeBatch := func(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { buf = appendBatch(buf[:0], msgs) - return httpWriteResult(w, buf, isError) + return httpWrite(ctx, w, buf, isError) } dec := json.NewDecoder(conn) @@ -284,16 +285,17 @@ func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) Serve return NewFuncCodec(conn, encodeMsg, encodeBatch, dec.Decode) } -// httpWriteResult writes pre-encoded response data over HTTP. -// For error responses, it sets Content-Length and flushes to ensure the response -// is fully written before any HTTP server write timeout occurs. -func httpWriteResult(w http.ResponseWriter, data []byte, isError bool) error { +// httpWrite writes pre-encoded response data over HTTP. +func httpWrite(ctx context.Context, w http.ResponseWriter, data []byte, isError bool) (err error) { + _, _, spanEnd := telemetry.StartSpanWithTracer(ctx, telemetry.TracerFromContext(ctx), "rpc.httpWrite") + defer spanEnd(&err) + w.Header().Set("content-length", strconv.Itoa(len(data))) if !isError { // Normal path, just send the response and let the HTTP server decide // when to flush. - _, err := w.Write(data) + _, err = w.Write(data) return err } @@ -309,7 +311,7 @@ func httpWriteResult(w http.ResponseWriter, data []byte, isError bool) error { // the final chunk is missing. To do this, we set TE = identity, which is a signal // recognized by outer handlers to avoid compression. w.Header().Set("transfer-encoding", "identity") - _, err := w.Write(data) + _, err = w.Write(data) if f, ok := w.(http.Flusher); ok { f.Flush() } diff --git a/rpc/json.go b/rpc/json.go index 9813acae73..b2a961d109 100644 --- a/rpc/json.go +++ b/rpc/json.go @@ -205,9 +205,9 @@ type jsonCodec struct { conn deadlineCloser } -type encodeMsgFunc = func(msg *jsonrpcMessage, isError bool) error +type encodeMsgFunc = func(ctx context.Context, msg *jsonrpcMessage, isError bool) error -type encodeBatchFunc = func(msgs []*jsonrpcMessage, isError bool) error +type encodeBatchFunc = func(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error type decodeFunc = func(v interface{}) error @@ -234,13 +234,13 @@ func NewCodec(conn Conn) ServerCodec { dec := json.NewDecoder(conn) dec.UseNumber() var buf []byte - encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + encodeMsg := func(ctx context.Context, msg *jsonrpcMessage, isError bool) error { buf = appendMessage(buf[:0], msg) buf = append(buf, '\n') _, err := conn.Write(buf) return err } - encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + encodeBatch := func(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { buf = appendBatch(buf[:0], msgs) buf = append(buf, '\n') _, err := conn.Write(buf) @@ -325,7 +325,7 @@ func (c *jsonCodec) writeJSON(ctx context.Context, msg *jsonrpcMessage, isError deadline = time.Now().Add(defaultWriteTimeout) } c.conn.SetWriteDeadline(deadline) - return c.encodeMsg(msg, isError) + return c.encodeMsg(ctx, msg, isError) } func (c *jsonCodec) writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { @@ -337,7 +337,7 @@ func (c *jsonCodec) writeJSONBatch(ctx context.Context, msgs []*jsonrpcMessage, deadline = time.Now().Add(defaultWriteTimeout) } c.conn.SetWriteDeadline(deadline) - return c.encodeBatch(msgs, isError) + return c.encodeBatch(ctx, msgs, isError) } func (c *jsonCodec) close() { diff --git a/rpc/service.go b/rpc/service.go index 8462a5a59a..b64f43b82d 100644 --- a/rpc/service.go +++ b/rpc/service.go @@ -91,15 +91,19 @@ func (r *serviceRegistry) registerName(name string, rcvr interface{}) error { return nil } +func serviceAndMethod(name string) (service, method string, ok bool) { + return strings.Cut(name, serviceMethodSeparator) +} + // callback returns the callback corresponding to the given RPC method name. -func (r *serviceRegistry) callback(method string) (cb *callback, service, methodName string) { - before, after, found := strings.Cut(method, serviceMethodSeparator) +func (r *serviceRegistry) callback(name string) (cb *callback) { + s, m, found := serviceAndMethod(name) if !found { - return nil, "", "" + return nil } r.mu.Lock() defer r.mu.Unlock() - return r.services[before].callbacks[after], before, after + return r.services[s].callbacks[m] } // subscription returns a subscription callback in the given service. diff --git a/rpc/tracing_test.go b/rpc/tracing_test.go index 5a04c901fd..302dff1384 100644 --- a/rpc/tracing_test.go +++ b/rpc/tracing_test.go @@ -18,8 +18,13 @@ package rpc import ( "context" + "io" + "net/http" "net/http/httptest" + "strconv" + "strings" "testing" + "time" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -27,6 +32,7 @@ import ( "go.opentelemetry.io/otel/propagation" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" + "go.opentelemetry.io/otel/trace" ) // attributeMap converts a slice of attributes to a map. @@ -105,15 +111,30 @@ func TestTracingHTTP(t *testing.T) { t.Fatal("no spans were emitted") } var rpcSpan *tracetest.SpanStub + var writeJSONSpan *tracetest.SpanStub + var httpWriteSpan *tracetest.SpanStub for i := range spans { - if spans[i].Name == "jsonrpc.test/echo" { + switch spans[i].Name { + case "jsonrpc.test/echo": rpcSpan = &spans[i] - break + case "rpc.writeJSON": + writeJSONSpan = &spans[i] + case "rpc.httpWrite": + httpWriteSpan = &spans[i] } } if rpcSpan == nil { t.Fatalf("jsonrpc.test/echo span not found") } + if writeJSONSpan == nil { + t.Fatalf("rpc.writeJSON span not found") + } + if httpWriteSpan == nil { + t.Fatalf("rpc.httpWrite span not found") + } + if got, want := httpWriteSpan.Parent.SpanID(), writeJSONSpan.SpanContext.SpanID(); got != want { + t.Errorf("rpc.httpWrite parent: got %s, want rpc.writeJSON (%s)", got, want) + } // Verify span attributes. attrs := attributeMap(rpcSpan.Attributes) @@ -167,13 +188,13 @@ func TestTracingHTTPErrorRecording(t *testing.T) { } spans := exporter.GetSpans() - // Only the runMethod span should have error status. + // The runMethod span and the SERVER span should both have error status. if len(spans) == 0 { t.Fatal("no spans were emitted") } for _, span := range spans { switch span.Name { - case "rpc.runMethod": + case "rpc.runMethod", "jsonrpc.test/returnError": if span.Status.Code != codes.Error { t.Errorf("expected %s span status Error, got %v", span.Name, span.Status.Code) } @@ -214,7 +235,11 @@ func TestTracingBatchHTTP(t *testing.T) { t.Fatalf("batch RPC call failed: %v", err) } - // Flush and verify we emitted spans for each batch element. + // Flush and verify the batch trace shape: + // jsonrpc.batch (SERVER, rpc.batch.size=N) + // - jsonrpc.test/echo (INTERNAL, x N) + // - rpc.writeJSONBatch (INTERNAL) + // - rpc.httpWriteResult (INTERNAL) if err := tracer.ForceFlush(context.Background()); err != nil { t.Fatalf("failed to flush: %v", err) } @@ -222,20 +247,68 @@ func TestTracingBatchHTTP(t *testing.T) { if len(spans) == 0 { t.Fatal("no spans were emitted") } - var found int + var ( + batchSpan *tracetest.SpanStub + callSpans []*tracetest.SpanStub + writeJSONBatchSpan *tracetest.SpanStub + httpWriteSpan *tracetest.SpanStub + ) for i := range spans { - if spans[i].Name == "jsonrpc.test/echo" { - attrs := attributeMap(spans[i].Attributes) - if attrs["rpc.system"] == "jsonrpc" && - attrs["rpc.service"] == "test" && - attrs["rpc.method"] == "echo" && - attrs["rpc.batch"] == "true" { - found++ - } + switch spans[i].Name { + case "jsonrpc.batch": + batchSpan = &spans[i] + case "jsonrpc.test/echo": + callSpans = append(callSpans, &spans[i]) + case "rpc.writeJSONBatch": + writeJSONBatchSpan = &spans[i] + case "rpc.httpWrite": + httpWriteSpan = &spans[i] } } - if found != len(batch) { - t.Fatalf("expected %d matching batch spans, got %d", len(batch), found) + if batchSpan == nil { + t.Fatal("jsonrpc.batch span not found") + } + if got, want := len(callSpans), len(batch); got != want { + t.Fatalf("got %d per-call spans, want %d", got, want) + } + if writeJSONBatchSpan == nil { + t.Fatal("rpc.writeJSONBatch span not found") + } + if httpWriteSpan == nil { + t.Fatal("rpc.httpWrite span not found") + } + + // Batch span: SERVER kind, rpc.batch.size=N. + if batchSpan.SpanKind != trace.SpanKindServer { + t.Errorf("jsonrpc.batch: got kind %v, want SERVER", batchSpan.SpanKind) + } + batchAttrs := attributeMap(batchSpan.Attributes) + if got, want := batchAttrs["rpc.batch.size"], strconv.Itoa(len(batch)); got != want { + t.Errorf("jsonrpc.batch rpc.batch.size: got %q, want %q", got, want) + } + + // Per-call spans: INTERNAL kind, parented to the batch span, carry rpc.* attrs. + for _, s := range callSpans { + if s.SpanKind != trace.SpanKindInternal { + t.Errorf("jsonrpc.test/echo: got kind %v, want INTERNAL", s.SpanKind) + } + if got, want := s.Parent.SpanID(), batchSpan.SpanContext.SpanID(); got != want { + t.Errorf("jsonrpc.test/echo parent: got %s, want %s (batch)", got, want) + } + attrs := attributeMap(s.Attributes) + if attrs["rpc.system"] != "jsonrpc" || attrs["rpc.service"] != "test" || attrs["rpc.method"] != "echo" { + t.Errorf("jsonrpc.test/echo attrs missing rpc.system/service/method: %v", attrs) + } + } + + // writeJSONBatch parented to the batch span. + if got, want := writeJSONBatchSpan.Parent.SpanID(), batchSpan.SpanContext.SpanID(); got != want { + t.Errorf("rpc.writeJSONBatch parent: got %s, want %s (batch)", got, want) + } + + // httpWriteResult parented to writeJSONBatch. + if got, want := httpWriteSpan.Parent.SpanID(), writeJSONBatchSpan.SpanContext.SpanID(); got != want { + t.Errorf("rpc.httpWriteResult parent: got %s, want %s (rpc.writeJSONBatch)", got, want) } } @@ -266,3 +339,235 @@ func TestTracingSubscribeUnsubscribe(t *testing.T) { t.Errorf("expected no spans for subscribe/unsubscribe, got %d", len(spans)) } } + +// postJSONRPC sends a raw JSON body to the given test server and discards the +// response body. Used to send messages the typed RPC client can't construct, +// like notifications (no "id" field). +func postJSONRPC(t *testing.T, url, body string) { + t.Helper() + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body)) + if err != nil { + t.Fatalf("new request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request: %v", err) + } + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() +} + +// TestTracingHTTPNotification verifies that a JSON-RPC notification emits the +// SERVER span (with error captured when applicable) but no rpc.writeJSON span, +// since notifications do not get a response written. +func TestTracingHTTPNotification(t *testing.T) { + t.Parallel() + server, tracer, exporter := newTracingServer(t) + httpsrv := httptest.NewServer(server) + t.Cleanup(httpsrv.Close) + + // Successful notification (no "id"): should produce a SERVER span without error, + // and no rpc.writeJSON span. + postJSONRPC(t, httpsrv.URL, `{"jsonrpc":"2.0","method":"test_echo","params":["hi",1,{"S":"x"}]}`) + + // Notification with unknown method: SERVER span should be present with error status. + postJSONRPC(t, httpsrv.URL, `{"jsonrpc":"2.0","method":"test_doesNotExist"}`) + + if err := tracer.ForceFlush(context.Background()); err != nil { + t.Fatalf("failed to flush: %v", err) + } + spans := exporter.GetSpans() + + var ( + echoSpan *tracetest.SpanStub + unknownSpan *tracetest.SpanStub + writeJSONFound bool + ) + for i := range spans { + switch spans[i].Name { + case "jsonrpc.test/echo": + echoSpan = &spans[i] + case "jsonrpc.test/doesNotExist": + unknownSpan = &spans[i] + case "rpc.writeJSON": + writeJSONFound = true + } + } + if echoSpan == nil { + t.Fatal("jsonrpc.test/echo span not found for successful notification") + } + if echoSpan.Status.Code == codes.Error { + t.Errorf("successful notification: expected no error status, got %v", echoSpan.Status) + } + if unknownSpan == nil { + t.Fatal("jsonrpc.test/doesNotExist span not found for unknown-method notification") + } + if unknownSpan.Status.Code != codes.Error { + t.Errorf("unknown-method notification: expected error status, got %v", unknownSpan.Status.Code) + } + if writeJSONFound { + t.Error("notifications should not produce an rpc.writeJSON span") + } +} + +// TestTracingBatchHTTPErrorCapture verifies that errors on individual calls +// inside a batch are recorded on the per-call INTERNAL span, including the +// pre-dispatch cases (method not found / invalid params) where runMethod +// never runs. +func TestTracingBatchHTTPErrorCapture(t *testing.T) { + t.Parallel() + server, tracer, exporter := newTracingServer(t) + httpsrv := httptest.NewServer(server) + t.Cleanup(httpsrv.Close) + + // A batch with: one valid call, one unknown method, one method that + // returns an error from its handler. + body := `[ + {"jsonrpc":"2.0","id":1,"method":"test_echo","params":["x",1,{"S":"a"}]}, + {"jsonrpc":"2.0","id":2,"method":"test_doesNotExist"}, + {"jsonrpc":"2.0","id":3,"method":"test_returnError"} + ]` + postJSONRPC(t, httpsrv.URL, body) + + if err := tracer.ForceFlush(context.Background()); err != nil { + t.Fatalf("failed to flush: %v", err) + } + spans := exporter.GetSpans() + + byName := make(map[string]*tracetest.SpanStub) + for i := range spans { + byName[spans[i].Name] = &spans[i] + } + + if byName["jsonrpc.batch"] == nil { + t.Fatal("jsonrpc.batch span not found") + } + if echo := byName["jsonrpc.test/echo"]; echo == nil { + t.Fatal("jsonrpc.test/echo span not found") + } else if echo.Status.Code == codes.Error { + t.Errorf("test/echo: unexpected error status %v", echo.Status) + } + if missing := byName["jsonrpc.test/doesNotExist"]; missing == nil { + t.Fatal("jsonrpc.test/doesNotExist span not found (method-not-found should still get a per-call span)") + } else if missing.Status.Code != codes.Error { + t.Errorf("test/doesNotExist: expected error status, got %v", missing.Status.Code) + } + if ret := byName["jsonrpc.test/returnError"]; ret == nil { + t.Fatal("jsonrpc.test/returnError span not found") + } else if ret.Status.Code != codes.Error { + t.Errorf("test/returnError: expected error status, got %v", ret.Status.Code) + } +} + +// TestTracingBatchHTTPEmpty verifies that an empty batch still emits a +// SERVER span, with rpc.batch.size=0 and error status. +func TestTracingBatchHTTPEmpty(t *testing.T) { + t.Parallel() + server, tracer, exporter := newTracingServer(t) + httpsrv := httptest.NewServer(server) + t.Cleanup(httpsrv.Close) + + postJSONRPC(t, httpsrv.URL, `[]`) + + if err := tracer.ForceFlush(context.Background()); err != nil { + t.Fatalf("failed to flush: %v", err) + } + spans := exporter.GetSpans() + + var batchSpan *tracetest.SpanStub + for i := range spans { + if spans[i].Name == "jsonrpc.batch" { + batchSpan = &spans[i] + } + } + if batchSpan == nil { + t.Fatal("jsonrpc.batch span not found for empty batch") + } + if batchSpan.Status.Code != codes.Error { + t.Errorf("empty batch: expected error status, got %v", batchSpan.Status.Code) + } + attrs := attributeMap(batchSpan.Attributes) + if got, want := attrs["rpc.batch.size"], "0"; got != want { + t.Errorf("empty batch rpc.batch.size: got %q, want %q", got, want) + } +} + +// TestTracingBatchHTTPTooLarge verifies that a batch exceeding the server's +// item limit emits a SERVER span with rpc.batch.size=N and error status. +func TestTracingBatchHTTPTooLarge(t *testing.T) { + t.Parallel() + server, tracer, exporter := newTracingServer(t) + server.SetBatchLimits(2, 100000) // limit to 2 items + httpsrv := httptest.NewServer(server) + t.Cleanup(httpsrv.Close) + + // 3 items > limit of 2. + body := `[ + {"jsonrpc":"2.0","id":1,"method":"test_echo","params":["a",1,{"S":"x"}]}, + {"jsonrpc":"2.0","id":2,"method":"test_echo","params":["b",2,{"S":"y"}]}, + {"jsonrpc":"2.0","id":3,"method":"test_echo","params":["c",3,{"S":"z"}]} + ]` + postJSONRPC(t, httpsrv.URL, body) + + if err := tracer.ForceFlush(context.Background()); err != nil { + t.Fatalf("failed to flush: %v", err) + } + spans := exporter.GetSpans() + + var batchSpan *tracetest.SpanStub + for i := range spans { + if spans[i].Name == "jsonrpc.batch" { + batchSpan = &spans[i] + } + } + if batchSpan == nil { + t.Fatal("jsonrpc.batch span not found for too-large batch") + } + if batchSpan.Status.Code != codes.Error { + t.Errorf("batch-too-large: expected error status, got %v", batchSpan.Status.Code) + } + attrs := attributeMap(batchSpan.Attributes) + if got, want := attrs["rpc.batch.size"], "3"; got != want { + t.Errorf("batch-too-large rpc.batch.size: got %q, want %q", got, want) + } +} + +// TestTracingHTTPTimeout verifies that when a non-batch call exceeds the HTTP +// server's WriteTimeout, the SERVER span ends with error status (carrying the +// timeout error message). +func TestTracingHTTPTimeout(t *testing.T) { + t.Parallel() + server, tracer, exporter := newTracingServer(t) + + // Configure a short WriteTimeout so the internal request timer fires + // quickly. ContextRequestTimeout subtracts 100ms from WriteTimeout, so + // 250ms here gives ~150ms before the timeout response is sent. + httpsrv := httptest.NewUnstartedServer(server) + httpsrv.Config.WriteTimeout = 250 * time.Millisecond + httpsrv.Start() + t.Cleanup(httpsrv.Close) + + // test_block waits on ctx.Done() and returns an error. The internal + // timer cancels ctx, so test_block unblocks shortly after the timeout + // response goes out. + postJSONRPC(t, httpsrv.URL, `{"jsonrpc":"2.0","id":1,"method":"test_block"}`) + + if err := tracer.ForceFlush(context.Background()); err != nil { + t.Fatalf("failed to flush: %v", err) + } + spans := exporter.GetSpans() + + var serverSpan *tracetest.SpanStub + for i := range spans { + if spans[i].Name == "jsonrpc.test/block" { + serverSpan = &spans[i] + } + } + if serverSpan == nil { + t.Fatal("jsonrpc.test/block span not found") + } + if serverSpan.Status.Code != codes.Error { + t.Errorf("timeout: expected SERVER span error status, got %v (%q)", serverSpan.Status.Code, serverSpan.Status.Description) + } +} diff --git a/rpc/websocket.go b/rpc/websocket.go index e70498873a..5e1e09c89d 100644 --- a/rpc/websocket.go +++ b/rpc/websocket.go @@ -294,11 +294,11 @@ type websocketCodec struct { func newWebsocketCodec(conn *websocket.Conn, host string, req http.Header, readLimit int64) ServerCodec { conn.SetReadLimit(readLimit) var buf []byte - encodeMsg := func(msg *jsonrpcMessage, isError bool) error { + encodeMsg := func(ctx context.Context, msg *jsonrpcMessage, isError bool) error { buf = appendMessage(buf[:0], msg) return conn.WriteMessage(websocket.TextMessage, buf) } - encodeBatch := func(msgs []*jsonrpcMessage, isError bool) error { + encodeBatch := func(ctx context.Context, msgs []*jsonrpcMessage, isError bool) error { buf = appendBatch(buf[:0], msgs) return conn.WriteMessage(websocket.TextMessage, buf) } From 38667bc64eb568bdb1632fe9134d189adf3cefe9 Mon Sep 17 00:00:00 2001 From: cui Date: Tue, 2 Jun 2026 23:13:36 +0800 Subject: [PATCH 62/76] p2p/nat: server list contains IPv6 servers (#35084) stun-list.txt includes 10 bracketd IPv6 server, but the dial network is fixed to "udp4" --- p2p/nat/nat_test.go | 1 + p2p/nat/stun.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/p2p/nat/nat_test.go b/p2p/nat/nat_test.go index 8dd5644fd6..551ec75d25 100644 --- a/p2p/nat/nat_test.go +++ b/p2p/nat/nat_test.go @@ -73,6 +73,7 @@ func TestParseStun(t *testing.T) { }{ {"stun", &stun{serverList: strings.Split(stunDefaultServers, "\n")}}, {"stun:1.2.3.4:1234", &stun{serverList: []string{"1.2.3.4:1234"}}}, + {"stun:[2001:db8::1]:3478", &stun{serverList: []string{"[2001:db8::1]:3478"}}}, } for _, tc := range testcases { diff --git a/p2p/nat/stun.go b/p2p/nat/stun.go index 30d2bc80d0..60c2b920a0 100644 --- a/p2p/nat/stun.go +++ b/p2p/nat/stun.go @@ -45,7 +45,7 @@ func newSTUN(serverAddr string) (Interface, error) { if serverAddr == "" { s.serverList = strings.Split(stunDefaultServers, "\n") } else { - _, err := net.ResolveUDPAddr("udp4", serverAddr) + _, err := net.ResolveUDPAddr("udp", serverAddr) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func (s *stun) externalIP(server string) (net.IP, error) { } log.Trace("Attempting STUN binding request", "server", server) - conn, err := stunV3.Dial("udp4", server) + conn, err := stunV3.Dial("udp", server) if err != nil { return nil, err } From e514ede4946e6fdc0c9b095a52878d731df82149 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:50:57 -0500 Subject: [PATCH 63/76] rpc: fix flaky otel tests (#35101) The response can reach the client before the deferred spanEnd fires, so call `httpsrv.Close()` before GetSpans is called. --- rpc/tracing_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rpc/tracing_test.go b/rpc/tracing_test.go index 302dff1384..58dc2f1758 100644 --- a/rpc/tracing_test.go +++ b/rpc/tracing_test.go @@ -470,6 +470,10 @@ func TestTracingBatchHTTPEmpty(t *testing.T) { postJSONRPC(t, httpsrv.URL, `[]`) + // Wait for the in-flight request to finish so the deferred spanEnd fires + // before GetSpans is called. + httpsrv.Close() + if err := tracer.ForceFlush(context.Background()); err != nil { t.Fatalf("failed to flush: %v", err) } @@ -510,6 +514,10 @@ func TestTracingBatchHTTPTooLarge(t *testing.T) { ]` postJSONRPC(t, httpsrv.URL, body) + // Wait for the in-flight request to finish so the deferred spanEnd fires + // before GetSpans is called. + httpsrv.Close() + if err := tracer.ForceFlush(context.Background()); err != nil { t.Fatalf("failed to flush: %v", err) } @@ -553,6 +561,10 @@ func TestTracingHTTPTimeout(t *testing.T) { // response goes out. postJSONRPC(t, httpsrv.URL, `{"jsonrpc":"2.0","id":1,"method":"test_block"}`) + // Wait for the in-flight request to finish so the deferred spanEnd fires + // before GetSpans is called. + httpsrv.Close() + if err := tracer.ForceFlush(context.Background()); err != nil { t.Fatalf("failed to flush: %v", err) } From f4393173f2eba189368c4e4f96d25f69368e0c82 Mon Sep 17 00:00:00 2001 From: Jonny Rhea <5555162+jrhea@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:08:09 -0500 Subject: [PATCH 64/76] triedb: reconcile stale storage roots in GenerateTrie, add cancel support (#34807) Rewrites triedb.GenerateTrie as a single partitioned pass that reconciles stale account.Root fields and rebuilds the trie at the same time, with 16-way parallelism and crash resume baked in. --------- Co-authored-by: Gary Rong --- cmd/geth/snapshot.go | 186 +++++ core/rawdb/accessors_snapshot.go | 42 ++ core/rawdb/database.go | 2 + core/rawdb/schema.go | 9 + trie/node.go | 77 +++ trie/stacktrie.go | 8 + trie/stacktrie_partial.go | 91 +++ trie/stacktrie_partial_test.go | 288 ++++++++ triedb/generate.go | 580 ++++++++++++++-- triedb/generate_test.go | 654 +++++++++++++++++- triedb/internal/conversion.go | 33 +- triedb/internal/conversion_test.go | 55 ++ .../{pathdb => internal}/holdable_iterator.go | 24 +- .../holdable_iterator_test.go | 14 +- triedb/pathdb/context.go | 23 +- triedb/pathdb/verifier.go | 4 +- 16 files changed, 1968 insertions(+), 122 deletions(-) create mode 100644 trie/stacktrie_partial.go create mode 100644 trie/stacktrie_partial_test.go create mode 100644 triedb/internal/conversion_test.go rename triedb/{pathdb => internal}/holdable_iterator.go (82%) rename triedb/{pathdb => internal}/holdable_iterator_test.go (92%) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index d168ee1d7d..08454f579e 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -22,11 +22,18 @@ import ( "encoding/json" "errors" "fmt" + "net/http" + _ "net/http/pprof" "os" + "os/signal" + "path/filepath" + "runtime" "slices" "sort" + "syscall" "time" + pebbleimpl "github.com/cockroachdb/pebble" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" @@ -36,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb/pebble" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" @@ -80,6 +88,33 @@ geth snapshot verify-state will traverse the whole accounts and storages set based on the specified snapshot and recalculate the root hash of state for verification. In other words, this command does the snapshot to trie conversion. +`, + }, + { + Name: "generate-trie", + Usage: "Benchmark triedb.GenerateTrie against a hard-linked checkpoint of the chaindata", + ArgsUsage: "[]", + Action: benchGenerateTrie, + Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags, []cli.Flag{ + &cli.StringFlag{ + Name: "checkpoint", + Usage: "Directory for the pebble checkpoint (default: /.gentrie-bench-)", + }, + &cli.BoolFlag{ + Name: "keep", + Usage: "Keep the checkpoint directory after the run (debugging)", + }, + &cli.BoolFlag{ + Name: "pprof", + Usage: "Serve pprof profiles on localhost:6060 (block + mutex profiles enabled)", + }, + }), + Description: ` +geth snapshot generate-trie [] + +Runs triedb.GenerateTrie against a hard-linked pebble checkpoint of the +chaindata. Checkpoint is removed on exit unless --keep is set. Defaults +to the snapshot root if is not given. `, }, { @@ -289,6 +324,157 @@ func verifyState(ctx *cli.Context) error { } } +// benchGenerateTrie runs triedb.GenerateTrie against a hard-linked checkpoint +// of the chaindata so the source datadir is never written to. +func benchGenerateTrie(ctx *cli.Context) error { + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + if ctx.Bool("pprof") { + runtime.SetBlockProfileRate(1) + runtime.SetMutexProfileFraction(1) + go func() { + log.Info("pprof listening", "addr", ":6060") + if err := http.ListenAndServe(":6060", nil); err != nil { + log.Warn("pprof server stopped", "err", err) + } + }() + } + + // Resolve source chaindata path (handles network-specific subdirs). + srcDir := stack.ResolvePath("chaindata") + if fi, err := os.Stat(srcDir); err != nil { + return fmt.Errorf("chaindata not found at %s: %w", srcDir, err) + } else if !fi.IsDir() { + return fmt.Errorf("%s is not a directory", srcDir) + } + + // Default to snapshot root, not head: that's what GenerateTrie actually + // reconstructs from flat state. On a fully-synced node they match. + var root common.Hash + if ctx.NArg() == 1 { + r, err := parseRoot(ctx.Args().First()) + if err != nil { + return fmt.Errorf("parse root: %w", err) + } + root = r + } else { + chaindb := utils.MakeChainDatabase(ctx, stack, true) + snapRoot := rawdb.ReadSnapshotRoot(chaindb) + head := rawdb.ReadHeadBlock(chaindb) + chaindb.Close() + switch { + case snapRoot != (common.Hash{}): + root = snapRoot + log.Info("using snapshot root", "root", root) + case head != nil: + root = head.Root() + log.Info("using head block root", "number", head.Number(), "root", root) + default: + return errors.New("no snapshot or head block found; pass explicitly") + } + } + + // Default checkpoint sits next to chaindata so hard links work. + ckpt := ctx.String("checkpoint") + if ckpt == "" { + ts := time.Now().Format("20060102-150405") + ckpt = filepath.Join(filepath.Dir(srcDir), fmt.Sprintf(".gentrie-bench-%s", ts)) + } + if _, err := os.Stat(ckpt); err == nil { + return fmt.Errorf("checkpoint dir %s already exists; remove it or pass --checkpoint to a fresh path", ckpt) + } + + log.Info("creating pebble checkpoint", "src", srcDir, "dst", ckpt) + checkpointStart := time.Now() + if err := makeCheckpoint(srcDir, ckpt); err != nil { + return fmt.Errorf("checkpoint failed: %w", err) + } + log.Info("checkpoint created", "elapsed", time.Since(checkpointStart)) + + // Clean up the checkpoint on exit, including Ctrl-C. + keep := ctx.Bool("keep") + cleanup := func() { + if keep { + log.Info("keeping checkpoint", "path", ckpt) + return + } + log.Info("removing checkpoint", "path", ckpt) + if err := os.RemoveAll(ckpt); err != nil { + log.Error("failed to remove checkpoint", "err", err) + } + } + defer cleanup() + + cancelCh := make(chan struct{}) + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(sigCh) + go func() { + <-sigCh + log.Warn("interrupt received; cancelling GenerateTrie") + close(cancelCh) + }() + + // Open the checkpoint writable. Reuse source ancient. Checkpoint only + // hard-links the pebble SSTs (not the freezer), and GenerateTrie never + // writes to ancient, so sharing it is safe. + srcAncient := stack.ResolveAncient("chaindata", "") + kv, err := pebble.New(ckpt, 4096, 1024, "gentrie-bench", false) + if err != nil { + return fmt.Errorf("open checkpoint: %w", err) + } + chaindb, err := rawdb.Open(kv, rawdb.OpenOptions{ + Ancient: srcAncient, + MetricsNamespace: "gentrie-bench", + }) + if err != nil { + kv.Close() + return fmt.Errorf("rawdb.Open checkpoint: %w", err) + } + defer chaindb.Close() + + // Pick up the trie scheme already in use (path or hash). + triedbInst := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false) + scheme := triedbInst.Scheme() + triedbInst.Close() + + log.Info("running GenerateTrie", "scheme", scheme, "root", root) + runStart := time.Now() + stats, err := triedb.GenerateTrie(chaindb, scheme, root, cancelCh) + elapsed := time.Since(runStart) + + status := "root matched" + if err != nil { + status = fmt.Sprintf("failed (%s)", err) + log.Error("GenerateTrie failed", "elapsed", elapsed, "err", err) + } + + fmt.Printf("\n=== generate-trie benchmark ===\n") + fmt.Printf("scheme: %s\n", scheme) + fmt.Printf("root: %s\n", root.Hex()) + fmt.Printf("status: %s\n", status) + fmt.Printf("accounts: %d (%d updated)\n", stats.Scanned, stats.Updated) + fmt.Printf("wall time: %s\n", elapsed) + return err +} + +// makeCheckpoint opens srcDir as a pebble database and writes a hard-linked +// checkpoint to dstDir. Source is closed on return. +// +// Opens read-write so pebble can finalize its startup (WAL replay, fresh +// OPTIONS file) before checkpointing. Read-only mode skips that step, and +// Checkpoint then fails trying to hard-link the missing OPTIONS file. The +// read-write open does no more than a normal geth startup would. +func makeCheckpoint(srcDir, dstDir string) error { + db, err := pebbleimpl.Open(srcDir, &pebbleimpl.Options{}) + if err != nil { + return fmt.Errorf("open source pebble: %w", err) + } + defer db.Close() + return db.Checkpoint(dstDir) +} + // checkDanglingStorage iterates the snap storage data, and verifies that all // storage also has corresponding account data. func checkDanglingStorage(ctx *cli.Context) error { diff --git a/core/rawdb/accessors_snapshot.go b/core/rawdb/accessors_snapshot.go index 5cea581fcd..24259dbc70 100644 --- a/core/rawdb/accessors_snapshot.go +++ b/core/rawdb/accessors_snapshot.go @@ -208,3 +208,45 @@ func WriteSnapshotSyncStatus(db ethdb.KeyValueWriter, status []byte) { log.Crit("Failed to store snapshot sync status", "err", err) } } + +// ReadGenerateTriePartitionDone returns the raw subtree root blob for a +// partition that has previously completed. +func ReadGenerateTriePartitionDone(db ethdb.KeyValueReader, partition byte) ([]byte, bool) { + data, err := db.Get(generateTriePartitionDoneKey(partition)) + if err != nil { + return nil, false + } + if len(data) == 0 { + return nil, false + } + switch data[0] { + case 0x00: + // Partition is done and it is empty. + return nil, true + case 0x01: + // Partition is done and the blob follows. + return data[1:], true + default: + return nil, false + } +} + +// WriteGenerateTriePartitionDone records a completed partition. +func WriteGenerateTriePartitionDone(db ethdb.KeyValueWriter, partition byte, blob []byte) { + var value []byte + if blob == nil { + value = []byte{0x00} + } else { + value = append([]byte{0x01}, blob...) + } + if err := db.Put(generateTriePartitionDoneKey(partition), value); err != nil { + log.Crit("Failed to store generate-trie done marker", "err", err) + } +} + +// DeleteGenerateTriePartitionDone removes a partition's done marker. +func DeleteGenerateTriePartitionDone(db ethdb.KeyValueWriter, partition byte) { + if err := db.Delete(generateTriePartitionDoneKey(partition)); err != nil { + log.Crit("Failed to remove generate-trie done marker", "err", err) + } +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 57abcdb25d..56ab72d2b7 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -563,6 +563,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { } // Metadata keys + case bytes.HasPrefix(key, generateTriePartitionDonePrefix) && len(key) == len(generateTriePartitionDonePrefix)+1: + metadata.add(size) case slices.ContainsFunc(knownMetadataKeys, func(x []byte) bool { return bytes.Equal(x, key) }): metadata.add(size) diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 54c76143b4..da5f9608bf 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -104,6 +104,10 @@ var ( // snapSyncStatusFlagKey flags that status of snap sync. snapSyncStatusFlagKey = []byte("SnapSyncStatus") + // generateTriePartitionDonePrefix stores the subtree root hash of each + // triedb.GenerateTrie partition once it finishes. + generateTriePartitionDonePrefix = []byte("gtd") // generateTriePartitionDonePrefix + partition byte -> subtree root hash + // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td (deprecated) @@ -465,3 +469,8 @@ func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID func transitionStateKey(hash common.Hash) []byte { return append(VerkleTransitionStatePrefix, hash.Bytes()...) } + +// generateTriePartitionDoneKey = generateTriePartitionDonePrefix + partition (single byte). +func generateTriePartitionDoneKey(partition byte) []byte { + return append(generateTriePartitionDonePrefix, partition) +} diff --git a/trie/node.go b/trie/node.go index 7022116048..b5094ff4b7 100644 --- a/trie/node.go +++ b/trie/node.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" ) @@ -335,3 +336,79 @@ func wrapError(err error, ctx string) error { func (err *decodeError) Error() string { return fmt.Sprintf("%v (decode path: %s)", err.what, strings.Join(err.stack, "<-")) } + +// MountPartitionRoot folds the leading nibble n back into the root of a +// partition subtree that was built with that nibble stripped (see +// PartialStackTrie). It returns the node, and its hash, that becomes the +// canonical trie root when partition n turns out to be the only populated +// partition, so the top-level branch collapses into the subtree itself. +// +// The subtree root blob is one of: +// +// - a branch: the canonical root is a freshly constructed extension carrying +// the single nibble n and pointing at the branch by hash. The branch stays +// at the path the partition already wrote it to ([n]). +// +// - a short node (extension or leaf): its hex key is extended from [k...] to +// [n, k...], preserving the leaf terminator if present, and the child/value +// element is reused verbatim. +// +// isOrphaned reports whether a short node was folded. When true, the node the +// caller persisted at path [n] is no longer referenced by the returned root and +// should be deleted. It is false for the branch case, where [n] stays referenced. +func MountPartitionRoot(blob []byte, n byte) (hash common.Hash, writeBlob []byte, isOrphaned bool, err error) { + elems, err := decodeNodeElements(blob) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("decode partition root: %w", err) + } + switch len(elems) { + case 17: + // Branch root: wrap it in an extension carrying the single nibble n, + // referencing the branch by its 32-byte hash. + keyRLP, err := rlp.EncodeToBytes(hexToCompact([]byte{n})) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("encode extension key: %w", err) + } + childRLP, err := rlp.EncodeToBytes(crypto.Keccak256(blob)) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("encode child ref: %w", err) + } + writeBlob, err = encodeNodeElements([][]byte{keyRLP, childRLP}) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("encode extension node: %w", err) + } + return crypto.Keccak256Hash(writeBlob), writeBlob, false, nil + + case 2: + // Short node (extension/leaf): prepend n to its hex key. compactToHex + // retains the leaf terminator, so hexToCompact restores the right type. + compactKey, _, err := rlp.SplitString(elems[0]) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("parse compact key: %w", err) + } + hex := append([]byte{n}, compactToHex(compactKey)...) + keyRLP, err := rlp.EncodeToBytes(hexToCompact(hex)) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("encode mounted key: %w", err) + } + writeBlob, err = encodeNodeElements([][]byte{keyRLP, elems[1]}) + if err != nil { + return common.Hash{}, nil, false, fmt.Errorf("encode mounted node: %w", err) + } + return crypto.Keccak256Hash(writeBlob), writeBlob, true, nil + + default: + return common.Hash{}, nil, false, fmt.Errorf("unexpected partition root element count: %d", len(elems)) + } +} + +// AssembleBranch constructs a fullNode (17-slot branch) from the given +// children and returns its RLP encoding and 32-byte hash. +func AssembleBranch(children [17][]byte) ([]byte, common.Hash, error) { + fn := &fullnodeEncoder{Children: children} + w := rlp.NewEncoderBuffer(nil) + fn.encode(w) + blob := w.ToBytes() + w.Flush() + return blob, crypto.Keccak256Hash(blob), nil +} diff --git a/trie/stacktrie.go b/trie/stacktrie.go index 18fe1eea78..6bb5421e9c 100644 --- a/trie/stacktrie.go +++ b/trie/stacktrie.go @@ -85,6 +85,14 @@ func (t *StackTrie) Update(key, value []byte) error { } t.grow(key) k := writeHexKey(t.kBuf, key) + return t.update(k, value) +} + +// update inserts a (hex-key, value) pair into the stack trie. The key must be +// in hex (nibble) form without the terminator flag, and the value must be +// non-empty. It is shared by Update and the partition builder, which feeds a +// key with its leading nibble stripped. +func (t *StackTrie) update(k, value []byte) error { if bytes.Compare(t.last, k) >= 0 { return errors.New("non-ascending key order") } diff --git a/trie/stacktrie_partial.go b/trie/stacktrie_partial.go new file mode 100644 index 0000000000..8b309d75f4 --- /dev/null +++ b/trie/stacktrie_partial.go @@ -0,0 +1,91 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" +) + +// PartialStackTrie builds the subtrie of a single first-nibble partition of a +// larger trie on top of a StackTrie. It is used for parallel trie generation, +// where the key space is split into 16 partitions by the first nibble and each +// partition is built independently before being mounted under a common root. +// +// Two adjustments make the produced subtrie line up with its position in the +// full trie: +// +// - Keys are inserted with their leading nibble stripped. That nibble is +// implied by the partition's slot in the parent branch, so duplicating it +// inside the keys would corrupt every node hash below the root. +// +// - Paths reported to onTrieNode are prefixed with the partition nibble, so +// a node's path matches its absolute position in the full trie. This is +// required by the path-based storage scheme, which keys nodes by path. +// +// The hashes themselves are independent of the absolute path, so prefixing the +// path does not change any node hash. +// +// All inserted keys must share the same leading nibble equal to `nibble`; the +// caller guarantees this by construction (e.g. by partitioning a hash range). +type PartialStackTrie struct { + nibble byte + inner *StackTrie + pathBuf []byte // reusable buffer for the nibble-prefixed path +} + +// NewPartialStackTrie creates a partition builder for the given leading nibble. +// The onTrieNode callback, if non-nil, is invoked for every committed node with +// its absolute path already prefixed with the partition nibble. +func NewPartialStackTrie(nibble byte, onTrieNode OnTrieNode) *PartialStackTrie { + p := &PartialStackTrie{nibble: nibble} + p.inner = NewStackTrie(func(path []byte, hash common.Hash, blob []byte) { + if onTrieNode == nil { + return + } + // Prefix the path with the partition nibble. The buffer is reused across + // calls, so the callback must consume it synchronously. + p.pathBuf = append(p.pathBuf[:0], nibble) + p.pathBuf = append(p.pathBuf, path...) + onTrieNode(p.pathBuf, hash, blob) + }) + return p +} + +// Update inserts a (key, value) pair, stripping the key's leading nibble, which +// is implied by the partition. The key must begin with the partition nibble. +func (p *PartialStackTrie) Update(key, value []byte) error { + if len(value) == 0 { + return errors.New("trying to insert empty (deletion)") + } + t := p.inner + t.grow(key) + k := writeHexKey(t.kBuf, key) + + if k[0] != p.nibble { + return fmt.Errorf("unexpected nibble %v, expected %x", k[0], p.nibble) + } + return t.update(k[1:], value) +} + +// Hash returns the root hash of the partition subtrie (built with the leading +// nibble stripped). It is the reference the parent branch mounts in slot `nibble`. +func (p *PartialStackTrie) Hash() common.Hash { + return p.inner.Hash() +} diff --git a/trie/stacktrie_partial_test.go b/trie/stacktrie_partial_test.go new file mode 100644 index 0000000000..5102ba4f4c --- /dev/null +++ b/trie/stacktrie_partial_test.go @@ -0,0 +1,288 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package trie + +import ( + "bytes" + "sort" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" +) + +// mkKey builds a 32-byte key from a leading hex string, right-padded with zeros +// (e.g. "3a" -> 0x3a000...0). The first nibble is prefixHex[0]. +func mkKey(prefixHex string) []byte { + return common.HexToHash(prefixHex + strings.Repeat("0", 64-len(prefixHex))).Bytes() +} + +// sortedPairs turns key prefixes into 32-byte (key, value) slices sorted by key, +// as StackTrie requires. Values are distinct and 32 bytes long. +func sortedPairs(prefixes []string) (keys, vals [][]byte) { + type kv struct{ k, v []byte } + ps := make([]kv, len(prefixes)) + for i, p := range prefixes { + ps[i] = kv{mkKey(p), bytes.Repeat([]byte{byte(i + 1)}, 32)} + } + sort.Slice(ps, func(i, j int) bool { return bytes.Compare(ps[i].k, ps[j].k) < 0 }) + for _, p := range ps { + keys = append(keys, p.k) + vals = append(vals, p.v) + } + return keys, vals +} + +// partitionRoot builds partition n over the given keys and returns its subtree +// root blob (the node emitted at path [n]). +func partitionRoot(t *testing.T, n byte, keys, vals [][]byte) []byte { + t.Helper() + var root []byte + pst := NewPartialStackTrie(n, func(path []byte, _ common.Hash, blob []byte) { + if len(path) == 1 { + root = common.CopyBytes(blob) + } + }) + for i := range keys { + if err := pst.Update(keys[i], vals[i]); err != nil { + t.Fatalf("partition update: %v", err) + } + } + pst.Hash() + return root +} + +type nodeRec struct { + hash common.Hash + blob []byte +} + +// collect builds a trie via the given updater and records every committed node +// keyed by its path. +func collect(update func(onNode OnTrieNode)) map[string]nodeRec { + nodes := make(map[string]nodeRec) + update(func(path []byte, hash common.Hash, blob []byte) { + nodes[string(path)] = nodeRec{hash, common.CopyBytes(blob)} + }) + return nodes +} + +// nodeKind decodes a node blob into "branch", "extension" or "leaf". +func nodeKind(t *testing.T, blob []byte) string { + t.Helper() + elems, err := decodeNodeElements(blob) + if err != nil { + t.Fatalf("decode node: %v", err) + } + switch len(elems) { + case 17: + return "branch" + case 2: + key, _, err := rlp.SplitString(elems[0]) + if err != nil { + t.Fatalf("split key: %v", err) + } + if hasTerm(compactToHex(key)) { + return "leaf" + } + return "extension" + default: + t.Fatalf("unexpected element count %d", len(elems)) + return "" + } +} + +// TestPartialStackTrieMatchesFullSubtree proves that, for every shape the +// partition subtree root can take, the nodes emitted by a PartialStackTrie for +// partition n are byte-for-byte identical (path, hash, blob) to the [n]-subtree +// of the full trie built from the same keys. +func TestPartialStackTrieMatchesFullSubtree(t *testing.T) { + const n = byte(3) + + // A single key in another partition (first nibble 9 > 3, so it sorts last) + // forces the full trie's root to be a branch, giving a clean [n]-subtree. + otherKey := mkKey("9") + otherVal := bytes.Repeat([]byte{0xff}, 32) + + cases := []struct { + name string + keys []string // partition-n key prefixes (first nibble must be 3) + wantRoot string // expected shape of the partition subtree root + }{ + {"single-leaf", []string{"3abc"}, "leaf"}, + {"branch-root", []string{"30", "37", "3a"}, "branch"}, + {"extension-root", []string{"3110", "3115", "311a"}, "extension"}, + {"mixed", []string{"30", "3105", "310a", "3f00", "3f0f"}, "branch"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + keys, vals := sortedPairs(tc.keys) + + // Reference: full trie over the partition-n keys plus the other-partition key. + full := collect(func(onNode OnTrieNode) { + st := NewStackTrie(onNode) + for i := range keys { + if err := st.Update(keys[i], vals[i]); err != nil { + t.Fatalf("full update: %v", err) + } + } + if err := st.Update(otherKey, otherVal); err != nil { + t.Fatalf("full update (other): %v", err) + } + st.Hash() + }) + + // Subject: PartialStackTrie over just the partition-n keys. + var partRoot common.Hash + part := collect(func(onNode OnTrieNode) { + pst := NewPartialStackTrie(n, onNode) + for i := range keys { + if err := pst.Update(keys[i], vals[i]); err != nil { + t.Fatalf("partial update: %v", err) + } + } + partRoot = pst.Hash() + }) + + // The subtree root must live at path [n] in the full trie (i.e. it is + // hash-referenced, not inlined) and its hash must match Hash(). + rootRec, ok := full[string([]byte{n})] + if !ok { + t.Fatalf("full trie has no node at path [%d]", n) + } + if rootRec.hash != partRoot { + t.Fatalf("partition root %x != full subtree root %x", partRoot, rootRec.hash) + } + if got := nodeKind(t, rootRec.blob); got != tc.wantRoot { + t.Fatalf("subtree root kind = %s, want %s", got, tc.wantRoot) + } + + // Every full-trie node under [n] must equal the partition's node, and + // the partition must emit no node outside [n]. + want := make(map[string]nodeRec) + for p, rec := range full { + if len(p) >= 1 && p[0] == n { + want[p] = rec + } + } + if len(want) != len(part) { + t.Fatalf("node count: full subtree=%d, partition=%d", len(want), len(part)) + } + for p, rec := range want { + got, ok := part[p] + if !ok { + t.Fatalf("partition missing node at path %x", []byte(p)) + } + if got.hash != rec.hash || !bytes.Equal(got.blob, rec.blob) { + t.Fatalf("node mismatch at path %x", []byte(p)) + } + } + }) + } +} + +// TestPartialStackTrieWrongNibble checks the guard that rejects a key whose +// leading nibble does not belong to the partition. +func TestPartialStackTrieWrongNibble(t *testing.T) { + pst := NewPartialStackTrie(3, nil) + if err := pst.Update(mkKey("4abc"), []byte{0x01}); err == nil { + t.Fatal("expected error for key outside the partition, got nil") + } +} + +// TestMountPartitionRoot checks that folding the leading nibble back into a +// single partition's subtree root reproduces the canonical trie root, for every +// root shape (leaf, extension, branch). The branch case is the one not reachable +// through the triedb single-partition tests. +func TestMountPartitionRoot(t *testing.T) { + const n = byte(3) + cases := []struct { + name string + keys []string + wantOrphaned bool + }{ + {"leaf", []string{"3abc"}, true}, + {"extension", []string{"3110", "3115", "311a"}, true}, + {"branch", []string{"30", "37", "3a"}, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + keys, vals := sortedPairs(tc.keys) + + // Canonical root: a plain trie over the same keys. They all share + // nibble n, so there is no top-level branch to collapse. + ref := NewStackTrie(nil) + for i := range keys { + if err := ref.Update(keys[i], vals[i]); err != nil { + t.Fatalf("ref update: %v", err) + } + } + want := ref.Hash() + + got, blob, isOrphaned, err := MountPartitionRoot(partitionRoot(t, n, keys, vals), n) + if err != nil { + t.Fatalf("MountPartitionRoot: %v", err) + } + if isOrphaned != tc.wantOrphaned { + t.Fatalf("isOrphaned = %v, want %v", isOrphaned, tc.wantOrphaned) + } + if got != want { + t.Fatalf("mounted root %x, want %x", got, want) + } + if crypto.Keccak256Hash(blob) != got { + t.Fatalf("returned blob does not hash to the returned root") + } + }) + } +} + +// TestAssembleBranch checks that packing partition subtree-root hashes into a +// top-level branch reproduces the canonical root of the union of those keys. +func TestAssembleBranch(t *testing.T) { + keys3, vals3 := sortedPairs([]string{"30", "37", "3a"}) + keys7, vals7 := sortedPairs([]string{"71", "75"}) + + // Canonical root over both partitions (all "3..." sort before all "7..."). + ref := NewStackTrie(nil) + for i := range keys3 { + if err := ref.Update(keys3[i], vals3[i]); err != nil { + t.Fatalf("ref update: %v", err) + } + } + for i := range keys7 { + if err := ref.Update(keys7[i], vals7[i]); err != nil { + t.Fatalf("ref update: %v", err) + } + } + want := ref.Hash() + + var children [17][]byte + children[3] = crypto.Keccak256(partitionRoot(t, 3, keys3, vals3)) + children[7] = crypto.Keccak256(partitionRoot(t, 7, keys7, vals7)) + blob, got, err := AssembleBranch(children) + if err != nil { + t.Fatalf("AssembleBranch: %v", err) + } + if got != want { + t.Fatalf("assembled root %x, want %x", got, want) + } + if crypto.Keccak256Hash(blob) != got { + t.Fatalf("returned blob does not hash to the returned root") + } +} diff --git a/triedb/generate.go b/triedb/generate.go index 259e139848..b6906f4c63 100644 --- a/triedb/generate.go +++ b/triedb/generate.go @@ -17,92 +17,540 @@ package triedb import ( + "bytes" + "context" + "encoding/binary" "fmt" + "math/big" + "sync/atomic" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb/internal" + "golang.org/x/sync/errgroup" ) -// kvAccountIterator wraps an ethdb.Iterator to iterate over account snapshot -// entries in the database, implementing internal.AccountIterator. -type kvAccountIterator struct { - it ethdb.Iterator - hash common.Hash +// ErrCancelled is returned when GenerateTrie is aborted via its cancel +// channel before completing. +var ErrCancelled = internal.ErrCancelled + +// GenerateStats reports per-run counters from GenerateTrie. Scanned is +// the number of accounts walked, Updated is how many had a stale Root +// field that was rewritten to match the recomputed storage root, and +// Deleted is the number of dangling storage slots removed. +type GenerateStats struct { + Scanned int64 + Updated int64 + Deleted int64 } -func newKVAccountIterator(db ethdb.Iteratee) *kvAccountIterator { - it := rawdb.NewKeyLengthIterator( - db.NewIterator(rawdb.SnapshotAccountPrefix, nil), - len(rawdb.SnapshotAccountPrefix)+common.HashLength, - ) - return &kvAccountIterator{it: it} +// numPartitions is the number of slices the account hash space is divided +// into by GenerateTrie. +const numPartitions = 16 + +// Each partition covers 1/16 of the account hash space. We track progress +// by interpreting the top 8 bytes of an account hash as a uint64, so each +// partition spans 2^64 / 16 = 2^60. partitionFinished is stored in a +// partition's position when it completes. +const ( + partitionRangeSize = uint64(1) << 60 + partitionFinished = ^uint64(0) +) + +// rangeIterators bundles the per-partition account and storage iterators. +type rangeIterators struct { + db ethdb.Database + acct *internal.HoldableIterator + stor *internal.HoldableIterator } -func (it *kvAccountIterator) Next() bool { - if !it.it.Next() { - return false +func openRangeIterators(db ethdb.Database, start common.Hash) *rangeIterators { + return &rangeIterators{ + db: db, + acct: openFlatIterator(db, rawdb.SnapshotAccountPrefix, start[:], common.HashLength), + stor: openFlatIterator(db, rawdb.SnapshotStoragePrefix, start[:], 2*common.HashLength), } - key := it.it.Key() - copy(it.hash[:], key[len(rawdb.SnapshotAccountPrefix):]) - return true } -func (it *kvAccountIterator) Hash() common.Hash { return it.hash } -func (it *kvAccountIterator) Account() []byte { return it.it.Value() } -func (it *kvAccountIterator) Error() error { return it.it.Error() } -func (it *kvAccountIterator) Release() { it.it.Release() } - -// kvStorageIterator wraps an ethdb.Iterator to iterate over storage snapshot -// entries for a specific account, implementing internal.StorageIterator. -type kvStorageIterator struct { - it ethdb.Iterator - hash common.Hash +// reopen releases both iterators and reopens them at their current +// positions. Invoked after each batch flush so pebble compactions aren't +// blocked by long-lived iterator snapshots. Follows the same pattern as +// triedb/pathdb/context.go. +func (r *rangeIterators) reopen() { + r.acct = reopenFlatIterator(r.db, r.acct, rawdb.SnapshotAccountPrefix, common.HashLength) + r.stor = reopenFlatIterator(r.db, r.stor, rawdb.SnapshotStoragePrefix, 2*common.HashLength) } -func newKVStorageIterator(db ethdb.Iteratee, accountHash common.Hash) *kvStorageIterator { - it := rawdb.IterateStorageSnapshots(db, accountHash) - return &kvStorageIterator{it: it} +func (r *rangeIterators) release() { + r.acct.Release() + r.stor.Release() } -func (it *kvStorageIterator) Next() bool { - if !it.it.Next() { - return false +// flushIfFull writes and resets the batch once it grows past IdealBatchSize, +// then reopens the iterators. +func (r *rangeIterators) flushIfFull(batch ethdb.Batch, where string) error { + if batch.ValueSize() <= ethdb.IdealBatchSize { + return nil } - key := it.it.Key() - copy(it.hash[:], key[len(rawdb.SnapshotStoragePrefix)+common.HashLength:]) - return true -} - -func (it *kvStorageIterator) Hash() common.Hash { return it.hash } -func (it *kvStorageIterator) Slot() []byte { return it.it.Value() } -func (it *kvStorageIterator) Error() error { return it.it.Error() } -func (it *kvStorageIterator) Release() { it.it.Release() } - -// GenerateTrie rebuilds all tries (storage + account) from flat snapshot data -// in the database. It reads account and storage snapshots from the KV store, -// builds tries using StackTrie with streaming node writes, and verifies the -// computed state root matches the expected root. -func GenerateTrie(db ethdb.Database, scheme string, root common.Hash) error { - acctIt := newKVAccountIterator(db) - defer acctIt.Release() - - got, err := internal.GenerateTrieRoot(db, scheme, acctIt, common.Hash{}, internal.StackTrieGenerate, func(dst ethdb.KeyValueWriter, accountHash, codeHash common.Hash, stat *internal.GenerateStats) (common.Hash, error) { - storageIt := newKVStorageIterator(db, accountHash) - defer storageIt.Release() - - hash, err := internal.GenerateTrieRoot(dst, scheme, storageIt, accountHash, internal.StackTrieGenerate, nil, stat, false) - if err != nil { - return common.Hash{}, err - } - return hash, nil - }, internal.NewGenerateStats(), true) - if err != nil { - return err - } - if got != root { - return fmt.Errorf("state root mismatch: got %x, want %x", got, root) + if err := batch.Write(); err != nil { + return fmt.Errorf("flush batch (%s): %w", where, err) } + batch.Reset() + r.reopen() return nil } + +// openFlatIterator opens a length-filtered HoldableIterator over a snapshot +// prefix, seeked to the given start key (relative to the prefix). +func openFlatIterator(db ethdb.Database, prefix, start []byte, suffixLen int) *internal.HoldableIterator { + it := db.NewIterator(prefix, start) + return internal.NewHoldableIterator(rawdb.NewKeyLengthIterator(it, len(prefix)+suffixLen)) +} + +// reopenFlatIterator releases `old` and returns a new HoldableIterator +// positioned at the same key, or an empty iterator if `old` is exhausted. +func reopenFlatIterator(db ethdb.Database, old *internal.HoldableIterator, prefix []byte, suffixLen int) *internal.HoldableIterator { + if !old.Next() { + old.Release() + return internal.NewHoldableIterator(memorydb.New().NewIterator(nil, nil)) + } + // pebble's Key() slice is invalidated by Release. Copy first so the new + // iterator's lower bound isn't seeded from freed memory. + next := common.CopyBytes(old.Key()) + old.Release() + return openFlatIterator(db, prefix, next[len(prefix):], suffixLen) +} + +// generatePartition walks accounts whose first nibble equals `partition`, +// reconciling each account's Root with its flat storage and building +// both per-account storage subtries and the partition's slice of the +// account trie. Returns the partition's stripped subtree root blob, or +// nil if the partition had no accounts at all. +func generatePartition(ctx context.Context, cancel <-chan struct{}, db ethdb.Database, scheme string, partition byte, rangeStart, rangeEnd common.Hash, scanned, updated, deleted *atomic.Int64, pos *atomic.Uint64) ([]byte, error) { + iters := openRangeIterators(db, rangeStart) + defer iters.release() + + batch := db.NewBatchWithSize(ethdb.IdealBatchSize) + + // Account-trie builder for this partition. It is fed account keys with + // their leading nibble stripped and emits nodes at their absolute path + // (prefixed with the partition nibble), so they line up with the full + // trie without any post-hoc surgery. + // + // The subtree root is the only node emitted at path [partition]; we both + // persist it (so the top-level branch can reference it) and capture its + // bytes for assembleRoot, which needs them to either reference it or, + // in the single-partition case, fold the leading nibble back in. + var root []byte + acctTrie := trie.NewPartialStackTrie(partition, func(path []byte, hash common.Hash, blob []byte) { + if len(path) == 1 { + root = common.CopyBytes(blob) + } + rawdb.WriteTrieNode(batch, common.Hash{}, path, hash, blob, scheme) + }) + + // Iterate through all the accounts. + for iters.acct.Next() { + select { + case <-cancel: + return nil, ErrCancelled + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + key := iters.acct.Key() + var accountHash common.Hash + copy(accountHash[:], key[len(rawdb.SnapshotAccountPrefix):]) + if bytes.Compare(accountHash[:], rangeEnd[:]) > 0 { + break + } + scanned.Add(1) + pos.Store(binary.BigEndian.Uint64(accountHash[:8])) + + // Decode the account object + account, err := types.FullAccount(iters.acct.Value()) + if err != nil { + return nil, fmt.Errorf("decode account %x: %w", accountHash, err) + } + + // Build the account's storage trie from the flat storage snapshot. + // StackTrie's onTrieNode callback persists nodes as they finalize. + storageTrie := trie.NewStackTrie(func(path []byte, hash common.Hash, blob []byte) { + rawdb.WriteTrieNode(batch, accountHash, path, hash, blob, scheme) + }) + + // Compute the storage root by consuming matching slots from the + // shared storage iterator. The inner loop terminates on Hold() + // (slot belongs to a later account) or exhaustion. + lastDanglingAccount := make([]byte, common.HashLength) + for iters.stor.Next() { + // Re-check cancel. + select { + case <-cancel: + return nil, ErrCancelled + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + var ( + sk = iters.stor.Key() + storageAccount = sk[len(rawdb.SnapshotStoragePrefix) : len(rawdb.SnapshotStoragePrefix)+common.HashLength] + cmp = bytes.Compare(storageAccount, accountHash[:]) + ) + // The slot belongs to an account whose hash is smaller than the one + // currently being processed. This should be theoretically impossible, + // so log it loudly and delete the dangling entry from the flat state. + if cmp < 0 { + if !bytes.Equal(lastDanglingAccount, storageAccount) { + copy(lastDanglingAccount, storageAccount) + log.Error("Unexpected storage entries for dangling account", "expected", accountHash, "got", common.BytesToHash(storageAccount)) + } + deleted.Add(1) + slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:] + rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(storageAccount), common.BytesToHash(slotHash)) + if err := iters.flushIfFull(batch, "dangling"); err != nil { + return nil, err + } + continue + } + + // The slot belongs to a later account. We're done with the current + // account's slots, but we don't want to lose this slot. The slot might + // belong to the next iteration of the account for-loop (or a later one). + // Hold() the iterator so the next Next() call will re-serve this same + // entry instead of advancing past it. + if cmp > 0 { + iters.stor.Hold() + break + } + + // The slot belongs to this account so we add it to the StackTrie. + slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:] + if err := storageTrie.Update(slotHash, iters.stor.Value()); err != nil { + return nil, fmt.Errorf("storage stack trie update for %x: %w", accountHash, err) + } + if err := iters.flushIfFull(batch, "storage"); err != nil { + return nil, err + } + } + if err := iters.stor.Error(); err != nil { + return nil, fmt.Errorf("storage iterator: %w", err) + } + computed := storageTrie.Hash() + + // If account.Root was stale, rewrite the flat-state entry. Then feed + // the account, now with the correct Root, into this partition's + // account trie. + if computed != account.Root { + account.Root = computed + rawdb.WriteAccountSnapshot(batch, accountHash, types.SlimAccountRLP(*account)) + updated.Add(1) + } + fullAccount, err := rlp.EncodeToBytes(account) + if err != nil { + return nil, fmt.Errorf("encode account %x: %w", accountHash, err) + } + if err := acctTrie.Update(accountHash[:], fullAccount); err != nil { + return nil, fmt.Errorf("account stack trie update for %x: %w", accountHash, err) + } + if err := iters.flushIfFull(batch, "account"); err != nil { + return nil, err + } + } + if err := iters.acct.Error(); err != nil { + return nil, fmt.Errorf("account iterator: %w", err) + } + + // The account iterator is exhausted (or has advanced past this partition), + // but the storage iterator may still hold slots whose account hash falls + // within this partition's range. Those slots belong to no existing account + // and should be cleared. + lastDanglingTail := make([]byte, common.HashLength) + for iters.stor.Next() { + select { + case <-cancel: + return nil, ErrCancelled + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + sk := iters.stor.Key() + acct := sk[len(rawdb.SnapshotStoragePrefix) : len(rawdb.SnapshotStoragePrefix)+common.HashLength] + if bytes.Compare(acct, rangeEnd[:]) > 0 { + break + } + if !bytes.Equal(lastDanglingTail, acct) { + copy(lastDanglingTail, acct) + log.Error("Unexpected storage entries for dangling account", "addrhash", common.BytesToHash(acct)) + } + deleted.Add(1) + slotHash := sk[len(rawdb.SnapshotStoragePrefix)+common.HashLength:] + rawdb.DeleteStorageSnapshot(batch, common.BytesToHash(acct), common.BytesToHash(slotHash)) + if err := iters.flushIfFull(batch, "dangling tail"); err != nil { + return nil, err + } + } + if err := iters.stor.Error(); err != nil { + return nil, fmt.Errorf("storage iterator (dangling): %w", err) + } + + // Finalize the partition's account trie. For a non-empty partition this + // emits the subtree root at path [partition], populating rootBlob. An empty + // partition never emits any node and leaves rootBlob at nil. + acctTrie.Hash() + + if err := batch.Write(); err != nil { + return nil, fmt.Errorf("final partition batch write: %w", err) + } + return root, nil +} + +// hashRanges returns hash pairs [start, end] that evenly partition the +// 256-bit hash space. The last partition absorbs the remainder so rounding +// doesn't leave hashes uncovered. +func hashRanges(total int) [][2]common.Hash { + step := new(big.Int).Sub( + new(big.Int).Div( + new(big.Int).Exp(common.Big2, common.Big256, nil), + big.NewInt(int64(total)), + ), + common.Big1, + ) + ranges := make([][2]common.Hash, total) + var next common.Hash + for i := range total { + last := common.BigToHash(new(big.Int).Add(next.Big(), step)) + if i == total-1 { + last = common.MaxHash + } + ranges[i] = [2]common.Hash{next, last} + next = common.BigToHash(new(big.Int).Add(last.Big(), common.Big1)) + } + return ranges +} + +// GenerateTrie rebuilds all tries (storage + account) from flat snapshot +// data in the database. The account hash space is partitioned into 16 +// slices aligned with the first-nibble branching of the MPT root. Each +// partition is processed by its own goroutine, which walks its slice, +// reconciles stale account.Root fields with flat storage, builds the +// per-account storage tries and the partition's slice of the account +// trie. Once every partition has produced its subtree root, the top-level +// branch is assembled and its hash verified against the expected root. +// +// Resume: on entry, any partition that has a "done" marker from a +// previous run is skipped. Its subtree blob is read from the marker +// and handed to assembleRoot directly. On a mid-run crash, only the +// in-flight partition(s) are redone. +func GenerateTrie(db ethdb.Database, scheme string, root common.Hash, cancel <-chan struct{}) (GenerateStats, error) { + var ( + start = time.Now() + scanned atomic.Int64 + updated atomic.Int64 + deleted atomic.Int64 + progress [numPartitions]atomic.Uint64 + progressDone = make(chan struct{}) + + // partitionBlobs[i] holds the root node for partition i, or nil if + // the partition is empty. + partitionBlobs [numPartitions][]byte + ) + go tickProgress(progressDone, start, &scanned, &updated, &progress) + defer close(progressDone) + + // For each partition, either skip (prior done marker found) or run + // it. Prior runs can leave the partition's raw root blob in the done + // marker. We recover it here so assembleRoot has everything it needs. + var ( + ranges = hashRanges(numPartitions) + eg, ctx = errgroup.WithContext(context.Background()) + ) + for i, r := range ranges { + partition := byte(i) + rangeStart, rangeEnd := r[0], r[1] + if blob, ok := rawdb.ReadGenerateTriePartitionDone(db, partition); ok { + partitionBlobs[partition] = blob + progress[partition].Store(partitionFinished) + continue + } + eg.Go(func() error { + start := time.Now() + blob, err := generatePartition(ctx, cancel, db, scheme, partition, rangeStart, rangeEnd, &scanned, &updated, &deleted, &progress[partition]) + if err != nil { + return err + } + log.Info("Partition done", "partition", partition, "elapsed", common.PrettyDuration(time.Since(start))) + + progress[partition].Store(partitionFinished) + partitionBlobs[partition] = blob + + // Record completion only after the partition's batch has + // flushed inside generatePartition, so this marker appears + // on disk only when every write the partition did is durable. + rawdb.WriteGenerateTriePartitionDone(db, partition, blob) + return nil + }) + } + + // Wait until all the partitions are fully generated + if err := eg.Wait(); err != nil { + return GenerateStats{}, err + } + + // Assemble the top-level root from the partition blobs, verify it + // matches the expected root, and clear all partition markers on + // success. + got, err := assembleRoot(db, scheme, partitionBlobs) + if err != nil { + return GenerateStats{}, fmt.Errorf("assemble root: %w", err) + } + if got != root { + return GenerateStats{}, fmt.Errorf("state root mismatch: got %x, want %x", got, root) + } + + // Clear the partition progress marker, ending the generation process. + batch := db.NewBatch() + for i := range numPartitions { + rawdb.DeleteGenerateTriePartitionDone(batch, byte(i)) + } + if err := batch.Write(); err != nil { + return GenerateStats{}, fmt.Errorf("clear partition markers: %w", err) + } + log.Info("Generated state trie", "scanned", scanned.Load(), "updated", updated.Load(), "dangling-slots", deleted.Load(), "elapsed", common.PrettyDuration(time.Since(start))) + return GenerateStats{ + Scanned: scanned.Load(), + Updated: updated.Load(), + Deleted: deleted.Load(), + }, nil +} + +// assembleRoot computes the canonical state root from the 16 partition subtree +// root blobs and persists the top-level node. Each partition was built with its +// leading nibble stripped, so its root blob is already the exact node the parent +// branch mounts in that slot, and the partition has already written it (and all +// its descendants) at their absolute paths. What's left depends on how many +// partitions ended up populated: +// +// - 0 populated: the state is empty, the root is types.EmptyRootHash and +// nothing is written. +// +// - 1 populated: there is no top-level branch; the canonical root is that +// lone partition's subtree with its leading nibble folded back in (see +// trie.MountPartitionRoot). The new root node is written. If the fold +// orphaned the old subtree root the partition left at [n], that node is +// also deleted. +// +// - 2+ populated: the canonical root is a 17-slot branch mounting each +// partition's subtree root by hash. The subtree roots are already on disk, +// so we only encode, hash, and persist the branch itself. +func assembleRoot(db ethdb.Database, scheme string, partitionBlobs [numPartitions][]byte) (common.Hash, error) { + var ( + populated int + partition int // last populated index, read only when populated == 1 + children [17][]byte + ) + + // Loop through all partitions and count how many are populated, while + // pre-filling the branch children array for the common 2+ case. + for i := range numPartitions { + if partitionBlobs[i] != nil { + populated++ + partition = i + children[i] = crypto.Keccak256(partitionBlobs[i]) + } + } + + // No populated partitions: the state is empty. + if populated == 0 { + return types.EmptyRootHash, nil + } + + // One populated partition: no top-level branch, so fold its leading nibble + // back into the subtree root. + if populated == 1 { + rootHash, rootBlob, isOrphaned, err := trie.MountPartitionRoot(partitionBlobs[partition], byte(partition)) + if err != nil { + return common.Hash{}, fmt.Errorf("mount partition %d: %w", partition, err) + } + batch := db.NewBatch() + rawdb.WriteTrieNode(batch, common.Hash{}, nil, rootHash, rootBlob, scheme) + if isOrphaned { + // The folded root at nil does not reference [partition], so the copy + // generatePartition wrote there is now unreferenced. Delete it so the + // on-disk node set matches the canonical trie. + staleHash := crypto.Keccak256Hash(partitionBlobs[partition]) + rawdb.DeleteTrieNode(batch, common.Hash{}, []byte{byte(partition)}, staleHash, scheme) + } + return rootHash, batch.Write() + } + + // populated >= 2: mount each partition's subtree root (already persisted at + // path [i]) into a 17-slot branch by hash, using the children array filled + // above. Those hash references are valid because account-trie subtree roots + // are always >= 32 bytes. + rootBlob, rootHash, err := trie.AssembleBranch(children) + if err != nil { + return common.Hash{}, err + } + rawdb.WriteTrieNode(db, common.Hash{}, nil, rootHash, rootBlob, scheme) + return rootHash, nil +} + +// tickProgress logs an aggregate progress line every 30 seconds until done +// is closed. Cheap: a handful of atomic loads and one log line per tick. +func tickProgress(done <-chan struct{}, start time.Time, scanned, updated *atomic.Int64, progress *[numPartitions]atomic.Uint64) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-done: + return + case <-ticker.C: + elapsed := time.Since(start) + fraction := progressFraction(progress) + eta := "n/a" + if fraction > 0.005 { + eta = common.PrettyDuration(time.Duration(float64(elapsed) * (1.0/fraction - 1.0))).String() + } + log.Info("Generating trie", + "progress", fmt.Sprintf("%.1f%%", fraction*100), "eta", eta, + "scanned", scanned.Load(), "updated", updated.Load(), + "elapsed", common.PrettyDuration(elapsed), + "acct/s", uint64(float64(scanned.Load())/elapsed.Seconds())) + } + } +} + +// progressFraction averages each partition's iterator position (as a fraction +// of its hash range) into an overall completion estimate in [0, 1]. Keccak +// hashes are uniform, so keyspace position is a good proxy for work done. +func progressFraction(progress *[numPartitions]atomic.Uint64) float64 { + var total float64 + for i := range numPartitions { + p := progress[i].Load() + switch { + case p == partitionFinished: + total += 1.0 + case p == 0: + // not started yet + default: + rangeStart := uint64(i) * partitionRangeSize + if p > rangeStart { + rel := p - rangeStart + if rel > partitionRangeSize { + rel = partitionRangeSize + } + total += float64(rel) / float64(partitionRangeSize) + } + } + } + return total / float64(numPartitions) +} diff --git a/triedb/generate_test.go b/triedb/generate_test.go index 42bccd9aa3..2a7d98bfaf 100644 --- a/triedb/generate_test.go +++ b/triedb/generate_test.go @@ -18,12 +18,17 @@ package triedb import ( "bytes" + "context" + "math/big" "sort" + "sync/atomic" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/holiman/uint256" @@ -60,8 +65,8 @@ func buildExpectedRoot(t *testing.T, accounts []testAccount) common.Hash { return acctTrie.Hash() } -// computeStorageRoot computes the storage trie root from sorted slots. -func computeStorageRoot(slots []testSlot) common.Hash { +// computeStorageRootFromSlots computes the storage trie root from sorted slots. +func computeStorageRootFromSlots(slots []testSlot) common.Hash { sort.Slice(slots, func(i, j int) bool { return bytes.Compare(slots[i].hash[:], slots[j].hash[:]) < 0 }) @@ -74,7 +79,7 @@ func computeStorageRoot(slots []testSlot) common.Hash { func TestGenerateTrieEmpty(t *testing.T) { db := rawdb.NewMemoryDatabase() - if err := GenerateTrie(db, rawdb.HashScheme, types.EmptyRootHash); err != nil { + if _, err := GenerateTrie(db, rawdb.HashScheme, types.EmptyRootHash, nil); err != nil { t.Fatalf("GenerateTrie on empty state failed: %v", err) } } @@ -107,19 +112,17 @@ func TestGenerateTrieAccountsOnly(t *testing.T) { } root := buildExpectedRoot(t, accounts) - if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil { + if _, err := GenerateTrie(db, rawdb.HashScheme, root, nil); err != nil { t.Fatalf("GenerateTrie failed: %v", err) } } func TestGenerateTrieWithStorage(t *testing.T) { - db := rawdb.NewMemoryDatabase() - slots := []testSlot{ {hash: common.HexToHash("0xaa"), value: []byte{0x01, 0x02, 0x03}}, {hash: common.HexToHash("0xbb"), value: []byte{0x04, 0x05, 0x06}}, } - storageRoot := computeStorageRoot(slots) + storageRoot := computeStorageRootFromSlots(slots) accounts := []testAccount{ { @@ -142,20 +145,24 @@ func TestGenerateTrieWithStorage(t *testing.T) { }, }, } - // Write account snapshots - for _, a := range accounts { - rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) - } - // Write storage snapshots - for _, a := range accounts { - for _, s := range a.storage { - rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value) - } - } root := buildExpectedRoot(t, accounts) - if err := GenerateTrie(db, rawdb.HashScheme, root); err != nil { - t.Fatalf("GenerateTrie failed: %v", err) + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} { + t.Run(scheme, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + for _, a := range accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + for _, s := range a.storage { + rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value) + } + } + if _, err := GenerateTrie(db, scheme, root, nil); err != nil { + t.Fatalf("GenerateTrie failed: %v", err) + } + if scheme == rawdb.PathScheme { + assertCanonicalNodes(t, db, accounts) + } + }) } } @@ -171,8 +178,615 @@ func TestGenerateTrieRootMismatch(t *testing.T) { rawdb.WriteAccountSnapshot(db, common.HexToHash("0x01"), types.SlimAccountRLP(acct)) wrongRoot := common.HexToHash("0xdeadbeef") - err := GenerateTrie(db, rawdb.HashScheme, wrongRoot) + _, err := GenerateTrie(db, rawdb.HashScheme, wrongRoot, nil) if err == nil { t.Fatal("expected error for root mismatch, got nil") } } + +// TestGenerateTrieFixesStaleRoots writes flat state with a mix of stale, +// empty, and correct account roots, then checks that GenerateTrie produces +// the expected state root. +func TestGenerateTrieFixesStaleRoots(t *testing.T) { + const n = 300 + accounts := make([]testAccount, 0, n) + for i := 0; i < n; i++ { + addr := common.BytesToAddress([]byte{byte(i >> 8), byte(i)}) + hash := crypto.Keccak256Hash(addr[:]) + + acc := testAccount{ + hash: hash, + account: types.StateAccount{ + Nonce: uint64(i), + Balance: uint256.NewInt(uint64(i + 1)), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + } + // Every third account has no storage; the rest get slots. + if i%3 != 0 { + acc.storage = []testSlot{ + {hash: common.BytesToHash([]byte{byte(i), 0xaa}), value: []byte{byte(i), 0x01}}, + {hash: common.BytesToHash([]byte{byte(i), 0xbb}), value: []byte{byte(i), 0x02}}, + } + acc.account.Root = computeStorageRootFromSlots(acc.storage) + } + accounts = append(accounts, acc) + } + // Expected state root with all Roots correct. + expectedRoot := buildExpectedRoot(t, accounts) + + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} { + t.Run(scheme, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + // Write flat state. Storage-bearing accounts rotate through three + // on-disk Root states that GenerateTrie's pre-pass must all bring + // into alignment: + // - stale non-empty Root + // - stale empty Root + // - correct Root + for i, a := range accounts { + for _, s := range a.storage { + rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value) + } + onDisk := a.account + if len(a.storage) > 0 { + switch i % 3 { + case 0: + onDisk.Root = common.BytesToHash([]byte{byte(i), 0xde, 0xad}) + case 1: + onDisk.Root = types.EmptyRootHash + } + } + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(onDisk)) + } + + if _, err := GenerateTrie(db, scheme, expectedRoot, nil); err != nil { + t.Fatalf("GenerateTrie failed: %v", err) + } + if scheme == rawdb.PathScheme { + assertCanonicalNodes(t, db, accounts) + } + }) + } +} + +// TestGenerateTrieCancel verifies GenerateTrie respects the cancel channel. +func TestGenerateTrieCancel(t *testing.T) { + t.Parallel() + db := rawdb.NewMemoryDatabase() + + for i := 0; i < 100; i++ { + addr := common.BytesToAddress([]byte{byte(i)}) + hash := crypto.Keccak256Hash(addr[:]) + rawdb.WriteAccountSnapshot(db, hash, types.SlimAccountRLP(types.StateAccount{ + Balance: uint256.NewInt(1), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + })) + } + + cancel := make(chan struct{}) + close(cancel) + if _, err := GenerateTrie(db, rawdb.HashScheme, common.Hash{}, cancel); err != ErrCancelled { + t.Fatalf("expected ErrCancelled, got %v", err) + } +} + +// TestGenerateTrieOrphanStorage exercises dangling-slot cleanup: flat storage +// entries for an accountHash that has no corresponding account snapshot must +// be deleted, regardless of whether they sit before, between, or after the +// live accounts within a partition. The state root must match and the +// Deleted counter must reflect every dangling entry. +func TestGenerateTrieOrphanStorage(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + // Two legitimate accounts in the same partition (first nibble 0x5) so + // orphans can be placed before, between, and after them in the shared + // per-partition storage iterator. + liveA := common.HexToHash("0x5300000000000000000000000000000000000000000000000000000000000000") + liveB := common.HexToHash("0x5900000000000000000000000000000000000000000000000000000000000000") + slotsA := []testSlot{{hash: common.HexToHash("0xaa"), value: []byte{0xa1}}} + slotsB := []testSlot{{hash: common.HexToHash("0xbb"), value: []byte{0xb1}}} + + accounts := []testAccount{ + { + hash: liveA, + account: types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1), + Root: computeStorageRootFromSlots(slotsA), + CodeHash: types.EmptyCodeHash.Bytes(), + }, + storage: slotsA, + }, + { + hash: liveB, + account: types.StateAccount{ + Nonce: 2, + Balance: uint256.NewInt(2), + Root: computeStorageRootFromSlots(slotsB), + CodeHash: types.EmptyCodeHash.Bytes(), + }, + storage: slotsB, + }, + } + for _, a := range accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + for _, s := range a.storage { + rawdb.WriteStorageSnapshot(db, a.hash, s.hash, s.value) + } + } + + // Dangling slots at three positions within partition 5: + // before liveA, between liveA and liveB, after liveB. + orphans := []struct { + account common.Hash + slots []testSlot + }{ + { + account: common.HexToHash("0x5000000000000000000000000000000000000000000000000000000000000000"), + slots: []testSlot{ + {hash: common.HexToHash("0x11"), value: []byte{0x01}}, + {hash: common.HexToHash("0x22"), value: []byte{0x02}}, + }, + }, + { + account: common.HexToHash("0x5600000000000000000000000000000000000000000000000000000000000000"), + slots: []testSlot{{hash: common.HexToHash("0x33"), value: []byte{0x03}}}, + }, + { + account: common.HexToHash("0x5d00000000000000000000000000000000000000000000000000000000000000"), + slots: []testSlot{ + {hash: common.HexToHash("0x44"), value: []byte{0x04}}, + {hash: common.HexToHash("0x55"), value: []byte{0x05}}, + }, + }, + } + var totalOrphans int64 + for _, o := range orphans { + for _, s := range o.slots { + rawdb.WriteStorageSnapshot(db, o.account, s.hash, s.value) + totalOrphans++ + } + } + + expectedRoot := buildExpectedRoot(t, accounts) + + stats, err := GenerateTrie(db, rawdb.HashScheme, expectedRoot, nil) + if err != nil { + t.Fatalf("GenerateTrie with orphan storage failed: %v", err) + } + if stats.Deleted != totalOrphans { + t.Errorf("Deleted counter = %d, want %d", stats.Deleted, totalOrphans) + } + for _, o := range orphans { + for _, s := range o.slots { + if v := rawdb.ReadStorageSnapshot(db, o.account, s.hash); v != nil { + t.Errorf("dangling slot %x/%x not cleared, got %x", o.account, s.hash, v) + } + } + } +} + +// TestGenerateTriePartialResume proves that the resume path actually +// fires when a partition's done marker is present. +func TestGenerateTriePartialResume(t *testing.T) { + // Build the account set. Empty storage keeps the test focused on the + // account-trie resume path. + const n = 200 + accounts := make([]testAccount, 0, n) + for i := 0; i < n; i++ { + addr := common.BytesToAddress([]byte{byte(i >> 8), byte(i)}) + hash := crypto.Keccak256Hash(addr[:]) + accounts = append(accounts, testAccount{ + hash: hash, + account: types.StateAccount{ + Nonce: uint64(i), + Balance: uint256.NewInt(uint64(i + 1)), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + }) + } + expectedRoot := buildExpectedRoot(t, accounts) + + for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} { + t.Run(scheme, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + // Step 1: write the account snapshots for this run. + for _, a := range accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + } + + // Step 2: run every partition once to populate trie nodes on disk + // and capture each partition's raw root blob. + var ( + scanned atomic.Int64 + updated atomic.Int64 + deleted atomic.Int64 + ) + ranges := hashRanges(numPartitions) + blobs := make([][]byte, numPartitions) + for i, r := range ranges { + var pos atomic.Uint64 + blob, err := generatePartition(context.Background(), nil, db, scheme, byte(i), r[0], r[1], &scanned, &updated, &deleted, &pos) + if err != nil { + t.Fatalf("pre-run partition %d: %v", i, err) + } + blobs[i] = blob + } + + // Step 3: pre-seed done markers for even partitions only. + for i := 0; i < numPartitions; i++ { + if i%2 == 0 { + rawdb.WriteGenerateTriePartitionDone(db, byte(i), blobs[i]) + } + } + + // Step 4: delete flat-state account snapshots for every account that + // lives in an even partition. After this, rerunning generatePartition for + // an even partition would find no accounts and produce a nil blob, + // so a correct final root requires the resume path. + numDeleted := 0 + for _, a := range accounts { + if (a.hash[0]>>4)%2 == 0 { + rawdb.DeleteAccountSnapshot(db, a.hash) + numDeleted++ + } + } + if numDeleted == 0 { + t.Fatal("test setup failure: no accounts fell in even partitions") + } + + // Step 5: run GenerateTrie. Success implies resume actually consulted + // the markers. Without it, even partitions would yield nil blobs and + // the root check inside GenerateTrie would fail. + if _, err := GenerateTrie(db, scheme, expectedRoot, nil); err != nil { + t.Fatalf("partial-resume GenerateTrie failed: %v", err) + } + + // All markers cleared on success. + for i := 0; i < numPartitions; i++ { + if _, ok := rawdb.ReadGenerateTriePartitionDone(db, byte(i)); ok { + t.Errorf("partition %d marker not cleared after successful resume", i) + } + } + if scheme == rawdb.PathScheme { + assertCanonicalNodes(t, db, accounts) + } + }) + } +} + +// TestHashRanges checks that hashRanges fully and contiguously covers the +// 256-bit hash space, with the last range absorbing the rounding remainder. +func TestHashRanges(t *testing.T) { + for _, total := range []int{1, 2, 16, 256} { + ranges := hashRanges(total) + if len(ranges) != total { + t.Fatalf("total=%d: got %d ranges, want %d", total, len(ranges), total) + } + if ranges[0][0] != (common.Hash{}) { + t.Errorf("total=%d: first range starts at %x, want zero", total, ranges[0][0]) + } + if ranges[total-1][1] != common.MaxHash { + t.Errorf("total=%d: last range ends at %x, want MaxHash", total, ranges[total-1][1]) + } + for i, r := range ranges { + if r[0].Big().Cmp(r[1].Big()) > 0 { + t.Errorf("total=%d: range %d malformed: start %x > end %x", total, i, r[0], r[1]) + } + if i == 0 { + continue + } + gap := new(big.Int).Sub(r[0].Big(), ranges[i-1][1].Big()) + if gap.Cmp(common.Big1) != 0 { + t.Errorf("total=%d: range %d not contiguous with %d (gap=%s)", total, i, i-1, gap) + } + } + } +} + +// TestGenerateTriePathSchemeNodeSet runs GenerateTrie on the path scheme and +// checks the persisted account-trie node set against a canonical StackTrie. A +// root-only check can't see the single-partition orphan, but a node-set diff can. +func TestGenerateTriePathSchemeNodeSet(t *testing.T) { + mkAccount := func(hashHex string) testAccount { + // Empty storage and no code, so the account trie is the only trie built + // and the canonical reference is a plain StackTrie over the accounts. + return testAccount{ + hash: common.HexToHash(hashHex), + account: types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash.Bytes(), + }, + } + } + + cases := []struct { + name string + accounts []testAccount + }{ + { + // One populated partition whose subtree root is a leaf. The node the + // partition wrote at [5] is left unreferenced, so GenerateTrie has to + // delete it. + name: "single account, leaf root", + accounts: []testAccount{mkAccount("0x5a00000000000000000000000000000000000000000000000000000000000000")}, + }, + { + // One populated partition whose subtree root is an extension. Like the + // leaf case, the node at [5] is left unreferenced and must be deleted. + name: "two accounts sharing two nibbles, extension root", + accounts: []testAccount{ + mkAccount("0x5300000000000000000000000000000000000000000000000000000000000000"), + mkAccount("0x5320000000000000000000000000000000000000000000000000000000000000"), + }, + }, + { + // One populated partition whose subtree root is a branch. Here [5] stays + // referenced by the new root, so nothing is orphaned. + name: "two accounts diverging at second nibble, branch root", + accounts: []testAccount{ + mkAccount("0x5a00000000000000000000000000000000000000000000000000000000000000"), + mkAccount("0x5f00000000000000000000000000000000000000000000000000000000000000"), + }, + }, + { + // Several populated partitions. Every [i] stays referenced by the top + // branch, so nothing is orphaned. + name: "accounts across multiple partitions", + accounts: []testAccount{ + mkAccount("0x1000000000000000000000000000000000000000000000000000000000000000"), + mkAccount("0x5a00000000000000000000000000000000000000000000000000000000000000"), + mkAccount("0x5f00000000000000000000000000000000000000000000000000000000000000"), + mkAccount("0xc000000000000000000000000000000000000000000000000000000000000000"), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + for _, a := range tc.accounts { + rawdb.WriteAccountSnapshot(db, a.hash, types.SlimAccountRLP(a.account)) + } + root := buildExpectedRoot(t, tc.accounts) + + if _, err := GenerateTrie(db, rawdb.PathScheme, root, nil); err != nil { + t.Fatalf("GenerateTrie (path scheme) failed: %v", err) + } + assertCanonicalNodes(t, db, tc.accounts) + }) + } +} + +// assertCanonicalNodes checks that the trie nodes persisted under the path +// scheme exactly match the canonical set: the account-trie nodes a StackTrie +// over the accounts emits, plus, per account with slots, the storage-trie nodes +// a StackTrie over those slots emits. accounts must carry their final Root +// values (post storage-root reconciliation). +func assertCanonicalNodes(t *testing.T, db ethdb.Database, accounts []testAccount) { + t.Helper() + + sorted := make([]testAccount, len(accounts)) + copy(sorted, accounts) + sort.Slice(sorted, func(i, j int) bool { + return bytes.Compare(sorted[i].hash[:], sorted[j].hash[:]) < 0 + }) + + // Canonical account-trie node paths. + wantAccount := make(map[string]struct{}) + acct := trie.NewStackTrie(func(path []byte, _ common.Hash, _ []byte) { + wantAccount[string(path)] = struct{}{} + }) + for i := range sorted { + data, err := rlp.EncodeToBytes(&sorted[i].account) + if err != nil { + t.Fatal(err) + } + if err := acct.Update(sorted[i].hash[:], data); err != nil { + t.Fatal(err) + } + } + acct.Hash() + + // Canonical storage-trie node keys (accountHash ++ path), one StackTrie per + // account that has slots. + wantStorage := make(map[string]struct{}) + for _, a := range accounts { + if len(a.storage) == 0 { + continue + } + slots := make([]testSlot, len(a.storage)) + copy(slots, a.storage) + sort.Slice(slots, func(i, j int) bool { + return bytes.Compare(slots[i].hash[:], slots[j].hash[:]) < 0 + }) + owner := a.hash + st := trie.NewStackTrie(func(path []byte, _ common.Hash, _ []byte) { + wantStorage[string(owner[:])+string(path)] = struct{}{} + }) + for _, s := range slots { + if err := st.Update(s.hash[:], s.value); err != nil { + t.Fatal(err) + } + } + st.Hash() + } + + assertSameNodeSet(t, "account", diskNodeKeys(db, rawdb.TrieNodeAccountPrefix), wantAccount) + assertSameNodeSet(t, "storage", diskNodeKeys(db, rawdb.TrieNodeStoragePrefix), wantStorage) +} + +// diskNodeKeys returns the set of path-scheme node keys with the given prefix +// stripped (account: hexPath; storage: accountHash ++ hexPath). +func diskNodeKeys(db ethdb.Database, prefix []byte) map[string]struct{} { + keys := make(map[string]struct{}) + it := db.NewIterator(prefix, nil) + defer it.Release() + for it.Next() { + keys[string(it.Key()[len(prefix):])] = struct{}{} + } + return keys +} + +// assertSameNodeSet fails if got and want differ, reporting each offending key. +func assertSameNodeSet(t *testing.T, label string, got, want map[string]struct{}) { + t.Helper() + for k := range got { + if _, ok := want[k]; !ok { + t.Errorf("%s-trie: extra node on disk at %x", label, k) + } + } + for k := range want { + if _, ok := got[k]; !ok { + t.Errorf("%s-trie: missing node on disk at %x", label, k) + } + } +} + +// peakBatch records the largest ValueSize the batch reaches before any flush. +type peakBatch struct { + ethdb.Batch + peak *int +} + +func (b *peakBatch) Write() error { + if s := b.ValueSize(); s > *b.peak { + *b.peak = s + } + return b.Batch.Write() +} + +// peakBatchDB hands out peakBatch batches so a test can observe how large the +// write batch grows between flushes. +type peakBatchDB struct { + ethdb.Database + peak *int +} + +func (d peakBatchDB) NewBatch() ethdb.Batch { + return &peakBatch{Batch: d.Database.NewBatch(), peak: d.peak} +} + +func (d peakBatchDB) NewBatchWithSize(size int) ethdb.Batch { + return &peakBatch{Batch: d.Database.NewBatchWithSize(size), peak: d.peak} +} + +// TestGenerateTrieBatchFlush drives each of generatePartition's batch-flush +// sites past IdealBatchSize and checks the write batch stays bounded (so the +// flush fired) without dropping or skipping any entry. +func TestGenerateTrieBatchFlush(t *testing.T) { + // h builds a unique partition-0 hash (leading nibble 0) from an int, used + // for both account hashes and storage slot hashes. + h := func(i int) common.Hash { + return common.BytesToHash([]byte{0x00, byte(i >> 16), byte(i >> 8), byte(i)}) + } + acct := func(root common.Hash) types.StateAccount { + return types.StateAccount{Nonce: 1, Balance: uint256.NewInt(1), Root: root, CodeHash: types.EmptyCodeHash.Bytes()} + } + // Each fixture writes this many entries into partition 0, enough that one + // flush site overflows IdealBatchSize several times over. + const n = 5000 + + cases := []struct { + name string + build func(db ethdb.Database) + wantScanned int64 + wantDeleted int64 + }{ + { + // Dangling account (no snapshot) sorting before a live account, so its + // slots are deleted inline (cmp < 0) while the live account is built. + name: "inline dangling deletes", + build: func(db ethdb.Database) { + for i := 0; i < n; i++ { + rawdb.WriteStorageSnapshot(db, h(1), h(i), []byte{0x01}) + } + rawdb.WriteAccountSnapshot(db, h(0xffffff), types.SlimAccountRLP(acct(types.EmptyRootHash))) + }, + wantScanned: 1, + wantDeleted: n, + }, + { + // Dangling account with no live account at all, so every slot is + // cleared by the tail loop after the account iterator is exhausted. + name: "tail dangling deletes", + build: func(db ethdb.Database) { + for i := 0; i < n; i++ { + rawdb.WriteStorageSnapshot(db, h(1), h(i), []byte{0x01}) + } + }, + wantScanned: 0, + wantDeleted: n, + }, + { + // One account whose storage trie alone overflows the batch, so the + // cmp == 0 storage path flushes mid-build. updated stays 0 only if + // every slot survived the flush and iterator reopen. + name: "single account, large storage", + build: func(db ethdb.Database) { + slots := make([]testSlot, n) + for i := range slots { + slots[i] = testSlot{hash: h(i), value: bytes.Repeat([]byte{byte(i)}, 32)} + } + rawdb.WriteAccountSnapshot(db, h(7), types.SlimAccountRLP(acct(computeStorageRootFromSlots(slots)))) + for _, s := range slots { + rawdb.WriteStorageSnapshot(db, h(7), s.hash, s.value) + } + }, + wantScanned: 1, + wantDeleted: 0, + }, + { + // Many empty-storage accounts so the account trie alone overflows the + // batch, exercising the per-account flush. A skipped account would not + // be counted in scanned. + name: "many accounts", + build: func(db ethdb.Database) { + for i := 0; i < n; i++ { + rawdb.WriteAccountSnapshot(db, h(i), types.SlimAccountRLP(acct(types.EmptyRootHash))) + } + }, + wantScanned: n, + wantDeleted: 0, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + db := rawdb.NewMemoryDatabase() + tc.build(db) + + peak := 0 + var scanned, updated, deleted atomic.Int64 + var pos atomic.Uint64 + ranges := hashRanges(numPartitions) + if _, err := generatePartition(context.Background(), nil, peakBatchDB{Database: db, peak: &peak}, + rawdb.HashScheme, 0, ranges[0][0], ranges[0][1], &scanned, &updated, &deleted, &pos); err != nil { + t.Fatalf("generatePartition: %v", err) + } + + if scanned.Load() != tc.wantScanned { + t.Errorf("scanned = %d, want %d (an account was skipped?)", scanned.Load(), tc.wantScanned) + } + if deleted.Load() != tc.wantDeleted { + t.Errorf("deleted = %d, want %d", deleted.Load(), tc.wantDeleted) + } + if updated.Load() != 0 { + t.Errorf("updated = %d, want 0 (a storage slot was dropped across a flush?)", updated.Load()) + } + // The batch must have stayed bounded. Without this site's flush its + // full write set (far larger than IdealBatchSize) buffers into one batch. + if peak > 2*ethdb.IdealBatchSize { + t.Errorf("peak batch size %d exceeded 2*IdealBatchSize (%d); flush did not fire", peak, 2*ethdb.IdealBatchSize) + } + }) + } +} diff --git a/triedb/internal/conversion.go b/triedb/internal/conversion.go index b331b63e21..8ab6c74268 100644 --- a/triedb/internal/conversion.go +++ b/triedb/internal/conversion.go @@ -21,6 +21,7 @@ package internal import ( "encoding/binary" + "errors" "fmt" "math" "runtime" @@ -36,6 +37,10 @@ import ( "github.com/ethereum/go-ethereum/trie" ) +// ErrCancelled is returned by GenerateTrieRoot when the cancel channel is +// closed mid-run. +var ErrCancelled = errors.New("cancelled") + // Iterator is an iterator to step over all the accounts or the specific // storage in a snapshot which may or may not be composed of multiple layers. type Iterator interface { @@ -228,7 +233,7 @@ func RunReport(stats *GenerateStats, stop chan bool) { // GenerateTrieRoot generates the trie hash based on the snapshot iterator. // It can be used for generating account trie, storage trie or even the // whole state which connects the accounts and the corresponding storages. -func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn TrieGeneratorFn, leafCallback LeafCallbackFn, stats *GenerateStats, report bool) (common.Hash, error) { +func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn TrieGeneratorFn, leafCallback LeafCallbackFn, stats *GenerateStats, report bool, cancel <-chan struct{}) (common.Hash, error) { var ( in = make(chan TrieKV) // chan to pass leaves out = make(chan common.Hash, 1) // chan to collect result @@ -279,6 +284,14 @@ func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou ) // Start to feed leaves for it.Next() { + // Top-of-loop cancel check. Cheap non-blocking peek so a closed + // cancel channel is observed without waiting for the blocking + // operations below. + select { + case <-cancel: + return stop(ErrCancelled) + default: + } if account == (common.Hash{}) { var ( err error @@ -291,8 +304,14 @@ func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou } } else { // Wait until the semaphore allows us to continue, aborting if - // a sub-task failed - if err := <-results; err != nil { + // a sub-task failed or the caller cancelled. + var err error + select { + case err = <-results: + case <-cancel: + return stop(ErrCancelled) + } + if err != nil { results <- nil // stop will drain the results, add a noop back for this error we just consumed return stop(err) } @@ -322,7 +341,13 @@ func GenerateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou } else { leaf = TrieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())} } - in <- leaf + // Escape on cancel so we don't deadlock if the generator goroutine is slow + // and the caller gave up. + select { + case in <- leaf: + case <-cancel: + return stop(ErrCancelled) + } // Accumulate the generation statistic if it's required. processed++ diff --git a/triedb/internal/conversion_test.go b/triedb/internal/conversion_test.go new file mode 100644 index 0000000000..0651ea1d91 --- /dev/null +++ b/triedb/internal/conversion_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package internal + +import ( + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// fakeStorageIterator is a StorageIterator over a fixed list of slots. +type fakeStorageIterator struct { + count int + idx int +} + +func (it *fakeStorageIterator) Next() bool { + if it.idx >= it.count { + return false + } + it.idx++ + return true +} +func (it *fakeStorageIterator) Error() error { return nil } +func (it *fakeStorageIterator) Hash() common.Hash { return common.BytesToHash([]byte{byte(it.idx)}) } +func (it *fakeStorageIterator) Slot() []byte { return []byte{byte(it.idx)} } +func (it *fakeStorageIterator) Release() {} + +// TestGenerateTrieRootCancel verifies that GenerateTrieRoot aborts with +// ErrCancelled when the cancel channel is closed. +func TestGenerateTrieRootCancel(t *testing.T) { + t.Parallel() + it := &fakeStorageIterator{count: 10_000} + cancel := make(chan struct{}) + close(cancel) + _, err := GenerateTrieRoot(nil, "", it, common.HexToHash("0xaa"), StackTrieGenerate, nil, nil, false, cancel) + if !errors.Is(err, ErrCancelled) { + t.Fatalf("expected ErrCancelled, got %v", err) + } +} diff --git a/triedb/pathdb/holdable_iterator.go b/triedb/internal/holdable_iterator.go similarity index 82% rename from triedb/pathdb/holdable_iterator.go rename to triedb/internal/holdable_iterator.go index 1f8e6b7068..7b0535e461 100644 --- a/triedb/pathdb/holdable_iterator.go +++ b/triedb/internal/holdable_iterator.go @@ -14,31 +14,31 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package pathdb +package internal import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb" ) -// holdableIterator is a wrapper of underlying database iterator. It extends +// HoldableIterator is a wrapper of underlying database iterator. It extends // the basic iterator interface by adding Hold which can hold the element // locally where the iterator is currently located and serve it up next time. -type holdableIterator struct { +type HoldableIterator struct { it ethdb.Iterator key []byte val []byte atHeld bool } -// newHoldableIterator initializes the holdableIterator with the given iterator. -func newHoldableIterator(it ethdb.Iterator) *holdableIterator { - return &holdableIterator{it: it} +// NewHoldableIterator initializes the HoldableIterator with the given iterator. +func NewHoldableIterator(it ethdb.Iterator) *HoldableIterator { + return &HoldableIterator{it: it} } // Hold holds the element locally where the iterator is currently located which // can be served up next time. -func (it *holdableIterator) Hold() { +func (it *HoldableIterator) Hold() { if it.it.Key() == nil { return // nothing to hold } @@ -49,7 +49,7 @@ func (it *holdableIterator) Hold() { // Next moves the iterator to the next key/value pair. It returns whether the // iterator is exhausted. -func (it *holdableIterator) Next() bool { +func (it *HoldableIterator) Next() bool { if !it.atHeld && it.key != nil { it.atHeld = true } else if it.atHeld { @@ -65,11 +65,11 @@ func (it *holdableIterator) Next() bool { // Error returns any accumulated error. Exhausting all the key/value pairs // is not considered to be an error. -func (it *holdableIterator) Error() error { return it.it.Error() } +func (it *HoldableIterator) Error() error { return it.it.Error() } // Release releases associated resources. Release should always succeed and can // be called multiple times without causing error. -func (it *holdableIterator) Release() { +func (it *HoldableIterator) Release() { it.atHeld = false it.key = nil it.val = nil @@ -79,7 +79,7 @@ func (it *holdableIterator) Release() { // Key returns the key of the current key/value pair, or nil if done. The caller // should not modify the contents of the returned slice, and its contents may // change on the next call to Next. -func (it *holdableIterator) Key() []byte { +func (it *HoldableIterator) Key() []byte { if it.key != nil { return it.key } @@ -89,7 +89,7 @@ func (it *holdableIterator) Key() []byte { // Value returns the value of the current key/value pair, or nil if done. The // caller should not modify the contents of the returned slice, and its contents // may change on the next call to Next. -func (it *holdableIterator) Value() []byte { +func (it *HoldableIterator) Value() []byte { if it.val != nil { return it.val } diff --git a/triedb/pathdb/holdable_iterator_test.go b/triedb/internal/holdable_iterator_test.go similarity index 92% rename from triedb/pathdb/holdable_iterator_test.go rename to triedb/internal/holdable_iterator_test.go index 2abc92e154..af6d7a34d6 100644 --- a/triedb/pathdb/holdable_iterator_test.go +++ b/triedb/internal/holdable_iterator_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package pathdb +package internal import ( "bytes" @@ -39,7 +39,7 @@ func TestIteratorHold(t *testing.T) { } } // Iterate over the database with the given configs and verify the results - it, idx := newHoldableIterator(db.NewIterator(nil, nil)), 0 + it, idx := NewHoldableIterator(db.NewIterator(nil, nil)), 0 // Nothing should be affected for calling Discard on non-initialized iterator it.Hold() @@ -108,20 +108,20 @@ func TestReopenIterator(t *testing.T) { } db = rawdb.NewMemoryDatabase() - reopen = func(db ethdb.KeyValueStore, iter *holdableIterator) *holdableIterator { + reopen = func(db ethdb.KeyValueStore, iter *HoldableIterator) *HoldableIterator { if !iter.Next() { iter.Release() - return newHoldableIterator(memorydb.New().NewIterator(nil, nil)) + return NewHoldableIterator(memorydb.New().NewIterator(nil, nil)) } next := iter.Key() iter.Release() - return newHoldableIterator(db.NewIterator(rawdb.SnapshotAccountPrefix, next[1:])) + return NewHoldableIterator(db.NewIterator(rawdb.SnapshotAccountPrefix, next[1:])) } ) for key, val := range content { rawdb.WriteAccountSnapshot(db, key, []byte(val)) } - checkVal := func(it *holdableIterator, index int) { + checkVal := func(it *HoldableIterator, index int) { if !bytes.Equal(it.Key(), append(rawdb.SnapshotAccountPrefix, order[index].Bytes()...)) { t.Fatalf("Unexpected data entry key, want %v got %v", order[index], it.Key()) } @@ -131,7 +131,7 @@ func TestReopenIterator(t *testing.T) { } // Iterate over the database with the given configs and verify the results dbIter := db.NewIterator(rawdb.SnapshotAccountPrefix, nil) - iter, idx := newHoldableIterator(rawdb.NewKeyLengthIterator(dbIter, 1+common.HashLength)), -1 + iter, idx := NewHoldableIterator(rawdb.NewKeyLengthIterator(dbIter, 1+common.HashLength)), -1 idx++ iter.Next() diff --git a/triedb/pathdb/context.go b/triedb/pathdb/context.go index a5704de81a..0554ee91bf 100644 --- a/triedb/pathdb/context.go +++ b/triedb/pathdb/context.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/triedb/internal" ) const ( @@ -91,12 +92,12 @@ func (gs *generatorStats) log(msg string, root common.Hash, marker []byte) { // current generation cycle. It must be recreated if the generation cycle is // restarted. type generatorContext struct { - root common.Hash // State root of the generation target - account *holdableIterator // Iterator of account snapshot data - storage *holdableIterator // Iterator of storage snapshot data - db ethdb.KeyValueStore // Key-value store containing the snapshot data - batch ethdb.Batch // Database batch for writing data atomically - logged time.Time // The timestamp when last generation progress was displayed + root common.Hash // State root of the generation target + account *internal.HoldableIterator // Iterator of account snapshot data + storage *internal.HoldableIterator // Iterator of storage snapshot data + db ethdb.KeyValueStore // Key-value store containing the snapshot data + batch ethdb.Batch // Database batch for writing data atomically + logged time.Time // The timestamp when last generation progress was displayed } // newGeneratorContext initializes the context for generation. @@ -119,11 +120,11 @@ func newGeneratorContext(root common.Hash, marker []byte, db ethdb.KeyValueStore func (ctx *generatorContext) openIterator(kind string, start []byte) { if kind == snapAccount { iter := ctx.db.NewIterator(rawdb.SnapshotAccountPrefix, start) - ctx.account = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+common.HashLength)) + ctx.account = internal.NewHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+common.HashLength)) return } iter := ctx.db.NewIterator(rawdb.SnapshotStoragePrefix, start) - ctx.storage = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+2*common.HashLength)) + ctx.storage = internal.NewHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+2*common.HashLength)) } // reopenIterator releases the specified snapshot iterator and re-open it @@ -140,10 +141,10 @@ func (ctx *generatorContext) reopenIterator(kind string) { // Iterator exhausted, release forever and create an already exhausted virtual iterator iter.Release() if kind == snapAccount { - ctx.account = newHoldableIterator(memorydb.New().NewIterator(nil, nil)) + ctx.account = internal.NewHoldableIterator(memorydb.New().NewIterator(nil, nil)) return } - ctx.storage = newHoldableIterator(memorydb.New().NewIterator(nil, nil)) + ctx.storage = internal.NewHoldableIterator(memorydb.New().NewIterator(nil, nil)) return } next := iter.Key() @@ -158,7 +159,7 @@ func (ctx *generatorContext) close() { } // iterator returns the corresponding iterator specified by the kind. -func (ctx *generatorContext) iterator(kind string) *holdableIterator { +func (ctx *generatorContext) iterator(kind string) *internal.HoldableIterator { if kind == snapAccount { return ctx.account } diff --git a/triedb/pathdb/verifier.go b/triedb/pathdb/verifier.go index c53590f2fd..4284432c1e 100644 --- a/triedb/pathdb/verifier.go +++ b/triedb/pathdb/verifier.go @@ -52,12 +52,12 @@ func (db *Database) VerifyState(root common.Hash) error { } defer storageIt.Release() - hash, err := internal.GenerateTrieRoot(nil, "", storageIt, accountHash, stackTrieHasher, nil, stat, false) + hash, err := internal.GenerateTrieRoot(nil, "", storageIt, accountHash, stackTrieHasher, nil, stat, false, nil) if err != nil { return common.Hash{}, err } return hash, nil - }, internal.NewGenerateStats(), true) + }, internal.NewGenerateStats(), true, nil) if err != nil { return err From 80d9ba5d97c100565251eeb5f33ad23adbb80c85 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:52:10 +0200 Subject: [PATCH 65/76] internal/era: update to latest ere spec (#34896) Update the EraE (ere) reader and builder to the latest e2store ere spec (https://github.com/eth-clients/e2store-format-specs/pull/16). The reader now derives the component layout from the on-disk e2store type tags via the dynamic block index, rather than assuming fixed slot positions. This makes the optional components (receipts, td, proof) resolvable in any supported subset. --------- Co-authored-by: lightclient --- cmd/era/main.go | 12 +-- cmd/geth/chaincmd.go | 10 +-- cmd/utils/flags.go | 2 +- cmd/utils/history_test.go | 2 +- internal/era/era.go | 15 ++-- internal/era/execdb/builder.go | 38 +++++---- internal/era/execdb/era_test.go | 132 +++++++++++++++++++++++++++++- internal/era/execdb/iterator.go | 26 +++--- internal/era/execdb/reader.go | 139 ++++++++++++++++++++++++-------- 9 files changed, 293 insertions(+), 83 deletions(-) diff --git a/cmd/era/main.go b/cmd/era/main.go index 43279e7001..3abe54a8b4 100644 --- a/cmd/era/main.go +++ b/cmd/era/main.go @@ -183,11 +183,11 @@ func open(ctx *cli.Context, epoch uint64) (era.Era, error) { return openByPath(path) } -// openByPath tries to open a single file as either eraE or era1 based on extension, +// openByPath tries to open a single file as either Ere or Era1 based on extension, // falling back to the other reader if needed. func openByPath(path string) (era.Era, error) { switch strings.ToLower(filepath.Ext(path)) { - case ".erae": + case ".ere": if e, err := execdb.Open(path); err != nil { return nil, err } else { @@ -229,7 +229,7 @@ func verify(ctx *cli.Context) error { // Build the verification list respecting the rule: // era1: must have accumulator, always verify - // erae: verify only if accumulator exists (pre-merge) + // ere: verify only if accumulator exists (pre-merge / transition) // Build list of files to verify. verify := make([]string, 0, len(entries)) @@ -251,15 +251,15 @@ func verify(ctx *cli.Context) error { } verify = append(verify, path) - case ".erae": + case ".ere": e, err := execdb.Open(path) if err != nil { - return fmt.Errorf("error opening erae file %s: %w", name, err) + return fmt.Errorf("error opening ere file %s: %w", name, err) } _, accErr := e.Accumulator() e.Close() if accErr == nil { - verify = append(verify, path) // pre-merge only + verify = append(verify, path) // pre-merge / transition only } default: return fmt.Errorf("unsupported era file: %s", name) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index a27975c4c1..891507d2ed 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -526,15 +526,15 @@ func importHistory(ctx *cli.Context) error { var ( format = ctx.String(utils.EraFormatFlag.Name) - from func(era.ReadAtSeekCloser) (era.Era, error) + from func(f era.ReadAtSeekCloser) (era.Era, error) ) switch format { case "era1", "era": from = onedb.From - case "erae": + case "ere": from = execdb.From default: - return fmt.Errorf("unknown --era.format %q (expected 'era1' or 'erae')", format) + return fmt.Errorf("unknown --era.format %q (expected 'era1' or 'ere')", format) } if err := utils.ImportHistory(chain, dir, network, from); err != nil { return err @@ -580,11 +580,11 @@ func exportHistory(ctx *cli.Context) error { case "era1", "era": newBuilder = func(w io.Writer) era.Builder { return onedb.NewBuilder(w) } filename = func(network string, epoch int, root common.Hash) string { return onedb.Filename(network, epoch, root) } - case "erae": + case "ere": newBuilder = func(w io.Writer) era.Builder { return execdb.NewBuilder(w) } filename = func(network string, epoch int, root common.Hash) string { return execdb.Filename(network, epoch, root) } default: - return fmt.Errorf("unknown archive format %q (use 'era1' or 'erae')", format) + return fmt.Errorf("unknown archive format %q (use 'era1' or 'ere')", format) } if err := utils.ExportHistory(chain, dir, uint64(first), uint64(last), newBuilder, filename); err != nil { utils.Fatalf("Export error: %v\n", err) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index df969bc3cc..9e111707c7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1110,7 +1110,7 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. // Era flags are a group of flags related to the era archive format. EraFormatFlag = &cli.StringFlag{ Name: "era.format", - Usage: "Archive format: 'era1' or 'erae'", + Usage: "Archive format: 'era1' or 'ere'", } ) diff --git a/cmd/utils/history_test.go b/cmd/utils/history_test.go index 6631946129..56375f9ff5 100644 --- a/cmd/utils/history_test.go +++ b/cmd/utils/history_test.go @@ -53,7 +53,7 @@ func TestHistoryImportAndExport(t *testing.T) { from func(f era.ReadAtSeekCloser) (era.Era, error) }{ {"era1", onedb.NewBuilder, onedb.Filename, onedb.From}, - {"erae", execdb.NewBuilder, execdb.Filename, execdb.From}, + {"ere", execdb.NewBuilder, execdb.Filename, execdb.From}, } { t.Run(tt.name, func(t *testing.T) { var ( diff --git a/internal/era/era.go b/internal/era/era.go index a3c8465bc4..0aae75e4bb 100644 --- a/internal/era/era.go +++ b/internal/era/era.go @@ -29,7 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -// Type constants for the e2store entries in the Era1 and EraE formats. +// Type constants for the e2store entries in the Era1 and Ere formats. var ( TypeVersion uint16 = 0x3265 TypeCompressedHeader uint16 = 0x03 @@ -40,10 +40,9 @@ var ( TypeCompressedSlimReceipts uint16 = 0x0a // uses eth/69 encoding TypeProof uint16 = 0x0b TypeBlockIndex uint16 = 0x3266 - TypeComponentIndex uint16 = 0x3267 + TypeDynamicBlockIndex uint16 = 0x3267 MaxSize = 8192 - // headerSize uint64 = 8 ) type ReadAtSeekCloser interface { @@ -93,7 +92,7 @@ type Builder interface { // Finalize writes all collected entries and returns the epoch identifier. // For Era1 (onedb): returns the accumulator root. - // For EraE (execdb): returns the last block hash. + // For Ere (execdb): returns the last block hash. Finalize() (common.Hash, error) // Accumulator returns the accumulator root after Finalize has been called. @@ -115,7 +114,7 @@ type Era interface { } // ReadDir reads all the era files in a directory for a given network. -// Format: --.erae or --.era1 +// Format: --(-)*.ere or --.era1 func ReadDir(dir, network string) ([]string, error) { entries, err := os.ReadDir(dir) @@ -129,14 +128,16 @@ func ReadDir(dir, network string) ([]string, error) { ) for _, entry := range entries { ext := path.Ext(entry.Name()) - if ext != ".erae" && ext != ".era1" { + if ext != ".ere" && ext != ".era1" { continue } if dirType == "" { dirType = ext } parts := strings.Split(entry.Name(), "-") - if len(parts) != 3 || parts[0] != network { + // Ere files may carry an optional profile postfix (e.g. "-noproofs"), + // so the filename has at least 3 dash-separated parts. + if len(parts) < 3 || parts[0] != network { // Invalid era filename, skip. continue } diff --git a/internal/era/execdb/builder.go b/internal/era/execdb/builder.go index 6246b9caae..4c656ab2e0 100644 --- a/internal/era/execdb/builder.go +++ b/internal/era/execdb/builder.go @@ -16,40 +16,44 @@ package execdb -// EraE file format specification. +// Ere file format specification. +// +// See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/ere.md. // // The format can be summarized with the following expression: // -// eraE := Version | CompressedHeader* | CompressedBody* | CompressedSlimReceipts* | TotalDifficulty* | other-entries* | Accumulator? | ComponentIndex +// ere := Version | CompressedHeader+ | CompressedBody+ | CompressedSlimReceipts* | Proof* | TotalDifficulty* | other-entries* | Accumulator? | DynamicBlockIndex // // Each basic element is its own e2store entry: // -// Version = { type: 0x3265, data: nil } -// CompressedHeader = { type: 0x03, data: snappyFramed(rlp(header)) } -// CompressedBody = { type: 0x04, data: snappyFramed(rlp(body)) } -// CompressedSlimReceipts = { type: 0x0a, data: snappyFramed(rlp([tx-type, post-state-or-status, cumulative-gas, logs])) } -// TotalDifficulty = { type: 0x06, data: uint256 (header.total_difficulty) } -// AccumulatorRoot = { type: 0x07, data: hash_tree_root(List(HeaderRecord, 8192)) } -// ComponentIndex = { type: 0x3267, data: component-index } +// Version = { type: [0x65, 0x32], data: nil } +// CompressedHeader = { type: [0x03, 0x00], data: snappyFramed(rlp(header)) } +// CompressedBody = { type: [0x04, 0x00], data: snappyFramed(rlp(body)) } +// CompressedSlimReceipts = { type: [0x0a, 0x00], data: snappyFramed(rlp([tx-type, post-state-or-status, cumulative-gas, logs])) } +// TotalDifficulty = { type: [0x06, 0x00], data: uint256(header.total_difficulty) } +// Proof = { type: [0x0b, 0x00], data: snappyFramed(rlp([proof-type, ssz(proof)])) } +// AccumulatorRoot = { type: [0x07, 0x00], data: hash_tree_root(List(HeaderRecord, 8192)) } +// DynamicBlockIndex = { type: [0x67, 0x32], data: block-index } // // Notes: // - TotalDifficulty is present for pre-merge and merge transition epochs. // For pure post-merge epochs, TotalDifficulty is omitted entirely. // - In merge transition epochs, post-merge blocks store the final total // difficulty (the TD at which the merge occurred). -// - AccumulatorRoot is only written for pre-merge epochs. +// - AccumulatorRoot is only written for pre-merge or transition epochs. // - HeaderRecord is defined in the Portal Network specification. -// - Proofs (type 0x09) are defined in the spec but not yet supported in this implementation. +// - Proof entries are recommended by the spec but not produced by this +// implementation; files written here use the "noproofs" profile postfix. // -// ComponentIndex stores relative offsets to each block's components: +// DynamicBlockIndex stores relative offsets to each block's components: // -// component-index := starting-number | indexes | indexes | ... | component-count | count -// indexes := header-offset | body-offset | receipts-offset | td-offset? +// block-index := starting-number | indexes | indexes | ... | component-count | count +// indexes := header-index | body-index | receipts-index? | difficulty-index? | proof-index? // // All values are little-endian uint64. // // Due to the accumulator size limit of 8192, the maximum number of blocks in an -// EraE file is also 8192. +// Ere file is also 8192. import ( "bytes" @@ -67,7 +71,7 @@ import ( "github.com/golang/snappy" ) -// Builder is used to build an EraE e2store file. It collects block entries and +// Builder is used to build an Ere e2store file. It collects block entries and // writes them to the underlying e2store.Writer. type Builder struct { w *e2store.Writer @@ -326,7 +330,7 @@ func (b *Builder) writeIndex(o *offsets) error { write(uint64(componentCount)) write(uint64(count)) - n, err := b.w.Write(era.TypeComponentIndex, buf.Bytes()) + n, err := b.w.Write(era.TypeDynamicBlockIndex, buf.Bytes()) b.written += uint64(n) return err } diff --git a/internal/era/execdb/era_test.go b/internal/era/execdb/era_test.go index f66931b9ed..add7f5ab9b 100644 --- a/internal/era/execdb/era_test.go +++ b/internal/era/execdb/era_test.go @@ -18,19 +18,24 @@ package execdb import ( "bytes" + "encoding/binary" "fmt" "io" "math/big" "os" + "path/filepath" "slices" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/era" + "github.com/ethereum/go-ethereum/internal/era/e2store" "github.com/ethereum/go-ethereum/rlp" + "github.com/golang/snappy" ) -func TestEraE(t *testing.T) { +func TestEre(t *testing.T) { t.Parallel() tests := []struct { @@ -74,7 +79,7 @@ func TestEraE(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - f, err := os.CreateTemp(t.TempDir(), "erae-test") + f, err := os.CreateTemp(t.TempDir(), "ere-test") if err != nil { t.Fatalf("error creating temp file: %v", err) } @@ -165,6 +170,18 @@ func TestEraE(t *testing.T) { if e.Count() != uint64(totalBlocks) { t.Fatalf("wrong block count: want %d, got %d", totalBlocks, e.Count()) } + // Verify the layout detected from on-disk type tags. Header, + // body, and receipts are always present; TD is only present + // when the epoch contains pre-merge blocks. + if !e.HasComponent(header) || !e.HasComponent(body) || !e.HasComponent(receipts) { + t.Fatalf("missing required component in layout %v", e.m.layout) + } + if got, want := e.HasComponent(td), tt.preMerge > 0; got != want { + t.Fatalf("td component presence mismatch: want %v, got %v", want, got) + } + if e.HasComponent(proof) { + t.Fatalf("proof component should not be present in layout %v", e.m.layout) + } // Verify accumulator in file. if tt.accumulator { @@ -295,7 +312,7 @@ func TestEraE(t *testing.T) { func TestInitialTD(t *testing.T) { t.Parallel() - f, err := os.CreateTemp(t.TempDir(), "erae-initial-td-test") + f, err := os.CreateTemp(t.TempDir(), "ere-initial-td-test") if err != nil { t.Fatalf("error creating temp file: %v", err) } @@ -339,6 +356,115 @@ func TestInitialTD(t *testing.T) { } } +// TestDetectLayoutNoReceipts builds an Ere file with no receipts component and +// checks that block data can still be read, while receipt access fails clearly. +func TestDetectLayoutNoReceipts(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + path := filepath.Join(dir, "synthetic.ere") + f, err := os.Create(path) + if err != nil { + t.Fatalf("create: %v", err) + } + + w := e2store.NewWriter(f) + written := uint64(0) + writeEntry := func(typ uint16, data []byte) { + n, err := w.Write(typ, data) + if err != nil { + t.Fatalf("write type 0x%04x: %v", typ, err) + } + written += uint64(n) + } + + var snappyBuf bytes.Buffer + writeSnappy := func(typ uint16, data []byte) { + snappyBuf.Reset() + sw := snappy.NewBufferedWriter(&snappyBuf) + if _, err := sw.Write(data); err != nil { + t.Fatalf("snappy write: %v", err) + } + if err := sw.Flush(); err != nil { + t.Fatalf("snappy flush: %v", err) + } + writeEntry(typ, snappyBuf.Bytes()) + } + + writeEntry(era.TypeVersion, nil) + + // Block 0 components in order: header, body, td (no receipts). + headerBytes := mustEncode(&types.Header{Number: big.NewInt(0), Difficulty: big.NewInt(1)}) + bodyBytes := mustEncode(&types.Body{}) + tdLE := make([]byte, 32) // uint256(1) little-endian + tdLE[0] = 1 + + headerOff := written + writeSnappy(era.TypeCompressedHeader, headerBytes) + bodyOff := written + writeSnappy(era.TypeCompressedBody, bodyBytes) + tdOff := written + writeEntry(era.TypeTotalDifficulty, tdLE) + + // Build the DynamicBlockIndex with 3 components per block, 1 block, and + // the third slot pointing at the TD entry rather than at receipts. + base := int64(written) + relative := func(absolute uint64) uint64 { return uint64(int64(absolute) - base) } + + var indexBuf bytes.Buffer + writeU64 := func(v uint64) { + if err := binary.Write(&indexBuf, binary.LittleEndian, v); err != nil { + t.Fatalf("index write: %v", err) + } + } + writeU64(0) // starting block number + writeU64(relative(headerOff)) + writeU64(relative(bodyOff)) + writeU64(relative(tdOff)) + writeU64(3) // component count + writeU64(1) // block count + writeEntry(era.TypeDynamicBlockIndex, indexBuf.Bytes()) + + if err := f.Close(); err != nil { + t.Fatalf("close: %v", err) + } + + g, err := os.Open(path) + if err != nil { + t.Fatalf("reopen: %v", err) + } + t.Cleanup(func() { g.Close() }) + e, err := From(g) + if err != nil { + t.Fatalf("From rejected file with no receipts component: %v", err) + } + + blk, err := e.GetBlockByNumber(0) + if err != nil { + t.Fatalf("GetBlockByNumber failed: %v", err) + } + if blk.NumberU64() != 0 { + t.Fatalf("wrong block number: got %d", blk.NumberU64()) + } + if _, err := e.GetRawReceiptsByNumber(0); err == nil { + t.Fatal("expected GetRawReceiptsByNumber to fail") + } + + it, err := e.Iterator() + if err != nil { + t.Fatalf("Iterator failed: %v", err) + } + if !it.Next() { + t.Fatalf("expected iterator to advance, err %v", it.Error()) + } + if _, err := it.Block(); err != nil { + t.Fatalf("iterator Block failed: %v", err) + } + if _, err := it.Receipts(); err == nil { + t.Fatal("expected iterator Receipts to fail") + } +} + func mustEncode(obj any) []byte { b, err := rlp.EncodeToBytes(obj) if err != nil { diff --git a/internal/era/execdb/iterator.go b/internal/era/execdb/iterator.go index 8d17ac00a9..6a972aef11 100644 --- a/internal/era/execdb/iterator.go +++ b/internal/era/execdb/iterator.go @@ -86,7 +86,7 @@ func (it *Iterator) Receipts() (types.Receipts, error) { return nil, err } if it.inner.Receipts == nil { - return nil, errors.New("receipts must be non‑nil") + return nil, errors.New("receipts not found") } var rs []*types.SlimReceipt if err := rlp.Decode(it.inner.Receipts, &rs); err != nil { @@ -180,19 +180,23 @@ func (it *RawIterator) Next() bool { return false } - receiptsOffset, err := it.e.receiptOff(it.next) - if err != nil { - it.setErr(err) - return false - } - it.Receipts, _, err = newSnappyReader(it.e.s, era.TypeCompressedSlimReceipts, receiptsOffset) - if err != nil { - it.setErr(err) - return false + if it.e.HasComponent(receipts) { + receiptsOffset, err := it.e.receiptOff(it.next) + if err != nil { + it.setErr(err) + return false + } + it.Receipts, _, err = newSnappyReader(it.e.s, era.TypeCompressedSlimReceipts, receiptsOffset) + if err != nil { + it.setErr(err) + return false + } + } else { + it.Receipts = nil } // Check if TD component is present in this file (pre-merge or merge-transition epoch). - if int(td) < int(it.e.m.components) { + if it.e.HasComponent(td) { tdOffset, err := it.e.tdOff(it.next) if err != nil { it.setErr(err) diff --git a/internal/era/execdb/reader.go b/internal/era/execdb/reader.go index d0aaad1748..e9831f9655 100644 --- a/internal/era/execdb/reader.go +++ b/internal/era/execdb/reader.go @@ -18,6 +18,7 @@ package execdb import ( "encoding/binary" + "errors" "fmt" "io" "math/big" @@ -39,10 +40,13 @@ type Era struct { m metadata // metadata for the Era file } -// Filename returns a recognizable filename for an EraE file. +// Filename returns a recognizable filename for an Ere file. // The filename uses the last block hash to uniquely identify the epoch's content. +// +// Files produced by this builder do not include Proof entries, so the +// "noproofs" profile postfix is appended per the Ere spec. func Filename(network string, epoch int, lastBlockHash common.Hash) string { - return fmt.Sprintf("%s-%05d-%s.erae", network, epoch, lastBlockHash.Hex()[2:10]) + return fmt.Sprintf("%s-%05d-%s-noproofs.ere", network, epoch, lastBlockHash.Hex()[2:10]) } // Open accesses the era file. @@ -51,8 +55,8 @@ func Open(path string) (*Era, error) { if err != nil { return nil, err } - e := &Era{f: f, s: e2store.NewReader(f)} - if err := e.loadIndex(); err != nil { + e, err := from(f) + if err != nil { f.Close() return nil, err } @@ -71,9 +75,17 @@ func (e *Era) Close() error { // From returns an Era backed by f. func From(f era.ReadAtSeekCloser) (era.Era, error) { + e, err := from(f) + if err != nil { + f.Close() + return nil, err + } + return e, nil +} + +func from(f era.ReadAtSeekCloser) (*Era, error) { e := &Era{f: f, s: e2store.NewReader(f)} if err := e.loadIndex(); err != nil { - f.Close() return nil, err } return e, nil @@ -185,12 +197,19 @@ func (e *Era) GetRawReceiptsByNumber(blockNum uint64) ([]byte, error) { return io.ReadAll(r) } +// HasComponent reports whether the given component is recorded in the file's +// index, as detected from the on-disk e2store type tags. +func (e *Era) HasComponent(c componentType) bool { + _, ok := e.m.layout[c] + return ok +} + // InitialTD returns initial total difficulty before the difficulty of the // first block of the Era is applied. Returns an error if TD is not available // (e.g., post-merge epoch). func (e *Era) InitialTD() (*big.Int, error) { // Check if TD component exists. - if int(td) >= int(e.m.components) { + if !e.HasComponent(td) { return nil, fmt.Errorf("total difficulty not available in this epoch") } @@ -210,8 +229,8 @@ func (e *Era) InitialTD() (*big.Int, error) { return new(big.Int).Sub(firstTD, header.Difficulty), nil } -// Accumulator reads the accumulator entry in the EraE file if it exists. -// Note that one premerge erae files will contain an accumulator entry. +// Accumulator reads the accumulator entry if present. Only pre-merge and +// merge-transition Ere files contain one. func (e *Era) Accumulator() (common.Hash, error) { entry, err := e.s.Find(era.TypeAccumulator) if err != nil { @@ -220,7 +239,8 @@ func (e *Era) Accumulator() (common.Hash, error) { return common.BytesToHash(entry.Value), nil } -// loadIndex loads in the index table containing all offsets and caches it. +// loadIndex loads in the index table trailer (start, count, component-count) +// and then derives the component→slot layout from the on-disk type tags. func (e *Era) loadIndex() error { var err error e.m.length, err = e.f.Seek(0, io.SeekEnd) @@ -241,30 +261,65 @@ func (e *Era) loadIndex() error { if err != nil { return err } - e.m.start = binary.LittleEndian.Uint64(b[:8]) + + layout, err := e.detectLayout() + if err != nil { + return err + } + e.m.layout = layout return nil } -// headerOff, bodyOff, receiptOff, and tdOff return the offsets of the respective components for a given block number. -func (e *Era) headerOff(num uint64) (int64, error) { return e.indexOffset(num, header) } -func (e *Era) bodyOff(num uint64) (int64, error) { return e.indexOffset(num, body) } -func (e *Era) receiptOff(num uint64) (int64, error) { return e.indexOffset(num, receipts) } -func (e *Era) tdOff(num uint64) (int64, error) { return e.indexOffset(num, td) } - -// indexOffset calculates offset to a certain component for a block number within a file. -func (e *Era) indexOffset(n uint64, component componentType) (int64, error) { - if n < e.m.start || n >= e.m.start+e.m.count { - return 0, fmt.Errorf("block %d out of range [%d,%d)", n, e.m.start, e.m.start+e.m.count) +// detectLayout reads the e2store type tag at each component slot of the first +// block and builds a componentType→slot map, so components are looked up by tag +// rather than by a fixed position. +func (e *Era) detectLayout() (map[componentType]int, error) { + if e.m.count == 0 { + return nil, errors.New("Ere file contains no blocks") } - if int(component) >= int(e.m.components) { - return 0, fmt.Errorf("component %d not present", component) + tagToComponent := map[uint16]componentType{ + era.TypeCompressedHeader: header, + era.TypeCompressedBody: body, + era.TypeCompressedSlimReceipts: receipts, + era.TypeTotalDifficulty: td, + era.TypeProof: proof, } + layout := make(map[componentType]int, e.m.components) + for slot := 0; slot < int(e.m.components); slot++ { + off, err := e.slotOffset(0, slot) + if err != nil { + return nil, fmt.Errorf("read slot %d offset: %w", slot, err) + } + typ, _, err := e.s.ReadMetadataAt(off) + if err != nil { + return nil, fmt.Errorf("read slot %d type tag: %w", slot, err) + } + comp, ok := tagToComponent[typ] + if !ok { + return nil, fmt.Errorf("unknown e2store type 0x%04x at index slot %d", typ, slot) + } + if existing, dup := layout[comp]; dup { + return nil, fmt.Errorf("duplicate component %d at slots %d and %d", comp, existing, slot) + } + layout[comp] = slot + } + if _, ok := layout[header]; !ok { + return nil, errors.New("Ere index has no header component") + } + if _, ok := layout[body]; !ok { + return nil, errors.New("Ere index has no body component") + } + return layout, nil +} - payloadlen := 8 + 8*e.m.count*e.m.components + 16 // 8 for start block, 8 per property per block, 16 for the number of properties and the number of blocks +// slotOffset returns the absolute file offset of the entry at the given slot +// of the given block index (0 = first block in file). +func (e *Era) slotOffset(blockIdx uint64, slot int) (int64, error) { + payloadlen := 8 + 8*e.m.count*e.m.components + 16 indstart := e.m.length - int64(payloadlen) - 8 - rec := (n-e.m.start)*e.m.components + uint64(component) + rec := blockIdx*e.m.components + uint64(slot) pos := indstart + 8 + 8 + int64(rec*8) var buf [8]byte @@ -275,18 +330,38 @@ func (e *Era) indexOffset(n uint64, component componentType) (int64, error) { return int64(rel) + indstart, nil } -// metadata contains the information about the era file that is written into the file. -type metadata struct { - start uint64 // start block number - count uint64 // number of blocks in the era - components uint64 // number of properties - length int64 // length of the file in bytes +// headerOff, bodyOff, receiptOff, and tdOff return the offsets of the respective components for a given block number. +func (e *Era) headerOff(num uint64) (int64, error) { return e.indexOffset(num, header) } +func (e *Era) bodyOff(num uint64) (int64, error) { return e.indexOffset(num, body) } +func (e *Era) receiptOff(num uint64) (int64, error) { return e.indexOffset(num, receipts) } +func (e *Era) tdOff(num uint64) (int64, error) { return e.indexOffset(num, td) } + +// indexOffset calculates offset to a certain component for a block number +// within a file. +func (e *Era) indexOffset(n uint64, component componentType) (int64, error) { + if n < e.m.start || n >= e.m.start+e.m.count { + return 0, fmt.Errorf("block %d out of range [%d,%d)", n, e.m.start, e.m.start+e.m.count) + } + slot, ok := e.m.layout[component] + if !ok { + return 0, fmt.Errorf("component %d not present in this Ere file", component) + } + return e.slotOffset(n-e.m.start, slot) } -// componentType represents the integer form of a specific type that can be present in the era file. +// metadata contains the information about the era file that is written into the file. +type metadata struct { + start uint64 // start block number + count uint64 // number of blocks in the era + components uint64 // number of slots per block in the index + layout map[componentType]int // component → slot index, derived from on-disk type tags + length int64 // length of the file in bytes +} + +// componentType identifies a kind of per-block entry (header, body, etc.). type componentType int -// header, body, receipts, td, and proof are the different types of components that can be present in the era file. +// td and proof are independently optional per the Ere spec. const ( header componentType = iota body From 6b451a42455c8afdba88b21ad1d504d21ac18cc2 Mon Sep 17 00:00:00 2001 From: Chase Wright Date: Wed, 3 Jun 2026 05:35:12 -0500 Subject: [PATCH 66/76] internal/ethapi: default block parameter to latest on state methods (#35100) Make the `Block` parameter optional on the six state-reading methods, defaulting to `latest` when omitted: - `eth_getBalance` - `eth_getCode` - `eth_getStorageAt` - `eth_getTransactionCount` - `eth_getProof` - `eth_getStorageValues` This implements the behavior proposed in https://github.com/ethereum/execution-apis/pull/812. --------- Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com> --- internal/ethapi/api.go | 50 ++++++++++++++-------- internal/ethapi/api_test.go | 85 +++++++++++++++++++++++++++++++++++-- internal/web3ext/web3ext.go | 4 +- 3 files changed, 116 insertions(+), 23 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 6452fcf37c..22a59aab58 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -328,11 +328,21 @@ func (api *BlockChainAPI) BlockNumber() hexutil.Uint64 { return hexutil.Uint64(header.Number.Uint64()) } +// blockNrOrHashOrLatest resolves an optional block selector, defaulting to the +// latest block when the parameter was omitted by the caller (nil). +func blockNrOrHashOrLatest(blockNrOrHash *rpc.BlockNumberOrHash) rpc.BlockNumberOrHash { + if blockNrOrHash != nil { + return *blockNrOrHash + } + return rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) +} + // GetBalance returns the amount of wei for the given address in the state of the // given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta -// block numbers are also allowed. -func (api *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) { - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) +// block numbers are also allowed. When the block parameter is omitted, it +// defaults to the latest block. +func (api *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (*hexutil.Big, error) { + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash)) if state == nil || err != nil { return nil, err } @@ -371,7 +381,8 @@ func (n *proofList) Delete(key []byte) error { } // GetProof returns the Merkle-proof for a given account and optionally some storage keys. -func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) { +// When the block parameter is omitted, it defaults to the latest block. +func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash *rpc.BlockNumberOrHash) (*AccountResult, error) { if len(storageKeys) > maxGetProofKeys { return nil, &invalidParamsError{fmt.Sprintf("too many storage keys requested (max %d, got %d)", maxGetProofKeys, len(storageKeys))} } @@ -388,7 +399,7 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, return nil, &invalidParamsError{fmt.Sprintf("%v: %q", err, hexKey)} } } - statedb, header, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + statedb, header, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash)) if statedb == nil || err != nil { return nil, err } @@ -584,8 +595,9 @@ func (api *BlockChainAPI) GetUncleCountByBlockHash(ctx context.Context, blockHas } // GetCode returns the code stored at the given address in the state for the given block number. -func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) +// When the block parameter is omitted, it defaults to the latest block. +func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash)) if state == nil || err != nil { return nil, err } @@ -595,9 +607,10 @@ func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, b // GetStorageAt returns the storage from the state at the given address, key and // block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block -// numbers are also allowed. -func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) +// numbers are also allowed. When the block parameter is omitted, it defaults to +// the latest block. +func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash)) if state == nil || err != nil { return nil, err } @@ -610,8 +623,9 @@ func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Addre } // GetStorageValues returns multiple storage slot values for multiple accounts -// at the given block. -func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[common.Address][]common.Hash, blockNrOrHash rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) { +// at the given block. When the block parameter is omitted, it defaults to the +// latest block. +func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[common.Address][]common.Hash, blockNrOrHash *rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) { // Count total slots requested. var totalSlots int for _, keys := range requests { @@ -624,7 +638,7 @@ func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[com return nil, &invalidParamsError{message: "empty request"} } - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash)) if state == nil || err != nil { return nil, err } @@ -1483,10 +1497,12 @@ func (api *TransactionAPI) GetRawTransactionByBlockHashAndIndex(ctx context.Cont return nil } -// GetTransactionCount returns the number of transactions the given address has sent for the given block number -func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) { +// GetTransactionCount returns the number of transactions the given address has sent for the given block number. +// When the block parameter is omitted, it defaults to the latest block. +func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (*hexutil.Uint64, error) { + bnh := blockNrOrHashOrLatest(blockNrOrHash) // Ask transaction pool for the nonce which includes pending transactions - if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber { + if blockNr, ok := bnh.Number(); ok && blockNr == rpc.PendingBlockNumber { nonce, err := api.b.GetPoolNonce(ctx, address) if err != nil { return nil, err @@ -1494,7 +1510,7 @@ func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address comm return (*hexutil.Uint64)(&nonce), nil } // Resolve block number and use its state to ask for the nonce - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, bnh) if state == nil || err != nil { return nil, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 3b72742e95..80a9036ecc 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -4216,7 +4216,7 @@ func TestGetStorageValues(t *testing.T) { result, err := api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ addr1: {slot0, slot1}, addr2: {slot2}, - }, latest) + }, &latest) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -4236,7 +4236,7 @@ func TestGetStorageValues(t *testing.T) { // Missing slot returns zero. result, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ addr1: {common.HexToHash("0xff")}, - }, latest) + }, &latest) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -4245,7 +4245,7 @@ func TestGetStorageValues(t *testing.T) { } // Empty request returns error. - _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, latest) + _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, &latest) if err == nil { t.Fatal("expected error for empty request") } @@ -4257,8 +4257,85 @@ func TestGetStorageValues(t *testing.T) { } _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ addr1: tooMany, - }, latest) + }, &latest) if err == nil { t.Fatal("expected error for exceeding slot limit") } } + +// TestStateMethodsDefaultToLatest verifies that the state-reading methods +// default the optional block parameter to "latest". +func TestStateMethodsDefaultToLatest(t *testing.T) { + t.Parallel() + var ( + accounts = newAccounts(2) + slot = common.HexToHash("0x01") + val = common.HexToHash("0x42") + code = []byte{0x60, 0x00, 0x60, 0x00} + genesis = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: { + Balance: big.NewInt(2 * params.Ether), + Nonce: 7, + Code: code, + Storage: map[common.Hash]common.Hash{slot: val}, + }, + }, + } + acc = accounts[1].addr + ctx = context.Background() + ) + backend := newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + }) + srv := rpc.NewServer() + if err := srv.RegisterName("eth", NewBlockChainAPI(backend)); err != nil { + t.Fatal(err) + } + if err := srv.RegisterName("eth", NewTransactionAPI(backend, new(AddrLocker))); err != nil { + t.Fatal(err) + } + client := rpc.DialInProc(srv) + defer client.Close() + + // call invokes method twice: once omitting the block param and once passing + // "latest" explicitly. Both must succeed and return identical results. + call := func(name string, dst func() any, explicit []any, omitted []any) { + t.Helper() + gotOmitted := dst() + if err := client.CallContext(ctx, gotOmitted, name, omitted...); err != nil { + t.Fatalf("%s with omitted block: unexpected error: %v", name, err) + } + gotLatest := dst() + if err := client.CallContext(ctx, gotLatest, name, explicit...); err != nil { + t.Fatalf("%s with explicit latest: unexpected error: %v", name, err) + } + o, _ := json.Marshal(gotOmitted) + l, _ := json.Marshal(gotLatest) + if !bytes.Equal(o, l) { + t.Errorf("%s: omitted-block result %s != latest result %s", name, o, l) + } + } + + call("eth_getBalance", + func() any { return new(hexutil.Big) }, + []any{acc, "latest"}, []any{acc}) + call("eth_getCode", + func() any { return new(hexutil.Bytes) }, + []any{acc, "latest"}, []any{acc}) + call("eth_getTransactionCount", + func() any { return new(hexutil.Uint64) }, + []any{acc, "latest"}, []any{acc}) + call("eth_getStorageAt", + func() any { return new(hexutil.Bytes) }, + []any{acc, slot, "latest"}, []any{acc, slot}) + call("eth_getProof", + func() any { return new(AccountResult) }, + []any{acc, []string{slot.Hex()}, "latest"}, []any{acc, []string{slot.Hex()}}) + call("eth_getStorageValues", + func() any { return new(map[common.Address][]hexutil.Bytes) }, + []any{map[common.Address][]common.Hash{acc: {slot}}, "latest"}, + []any{map[common.Address][]common.Hash{acc: {slot}}}) +} diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 6a5f3c9a8a..8622109e21 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -566,13 +566,13 @@ web3._extend({ name: 'getProof', call: 'eth_getProof', params: 3, - inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputBlockNumberFormatter] + inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputDefaultBlockNumberFormatter] }), new web3._extend.Method({ name: 'getStorageValues', call: 'eth_getStorageValues', params: 2, - inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + inputFormatter: [null, web3._extend.formatters.inputDefaultBlockNumberFormatter] }), new web3._extend.Method({ name: 'createAccessList', From eb429a062a404283f826ccf5a7c8ae1f2da6b8e9 Mon Sep 17 00:00:00 2001 From: Sina M <1591639+s1na@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:26:18 +0200 Subject: [PATCH 67/76] core/txpool: drop reorged v0 blob sidecars (#35099) With Osaka being a while ago I believe we can drop this transition and drop the tx instead. --- core/txpool/blobpool/blobpool.go | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 4ab97d35bf..60006130ed 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -166,14 +166,6 @@ func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar { return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) } -// ApplySidecar copies the sidecar's fields into the flat fields. -func (ptx *blobTxForPool) ApplySidecar(sc *types.BlobTxSidecar) { - ptx.Version = sc.Version - ptx.Commitments = sc.Commitments - ptx.Proofs = sc.Proofs - ptx.Blobs = sc.Blobs -} - // TxSize returns the transaction size on the network without // reconstructing the transaction. func (ptx *blobTxForPool) TxSize() uint64 { @@ -1274,22 +1266,13 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error { // TODO: seems like an easy optimization here would be getting the serialized tx // from limbo instead of re-serializing it here. - // Converts reorged-out legacy blob transactions to the new format to prevent - // them from becoming stuck in the pool until eviction. - // - // Performance note: Conversion takes ~140ms (Mac M1 Pro). Since a maximum of - // 9 legacy blob transactions are allowed in a block pre-Osaka, an adversary - // could theoretically halt a Geth node for ~1.2s by reorging per block. However, - // this attack is financially inefficient to execute. + // Post-Osaka, legacy (v0) blob sidecars are no longer accepted into the pool. + // A reorged-out legacy blob transaction can therefore not be re-added, so drop + // it on the floor instead of putting it back. head := p.head.Load() if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.Version == types.BlobSidecarVersion0 { - sc := ptx.Sidecar() - if err := sc.ToV1(); err != nil { - log.Error("Failed to convert the legacy sidecar", "err", err) - return err - } - ptx.ApplySidecar(sc) - log.Info("Legacy blob transaction is reorged", "hash", ptx.Tx.Hash()) + log.Debug("Dropping reorged legacy blob transaction", "hash", txhash) + return errors.New("legacy blob sidecar unsupported post-osaka") } blob, err := rlp.EncodeToBytes(ptx) if err != nil { From f49336459057c19cad2e66ce0ce31c59604bc329 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet <3272758+gballet@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:39:12 +0200 Subject: [PATCH 68/76] build, cmd/geth, signer: remove clef (#35097) `clef` is a great tool, however: * It is no longer maintained * No one else in the team can pronounce it properly We are however receiving some slop PRs for it, so I think it's time - with infinite sadness - to say goodbye. --- README.md | 1 - SECURITY.md | 1 - build/ci.go | 6 +- cmd/clef/README.md | 922 ------------- cmd/clef/consolecmd_test.go | 121 -- cmd/clef/datatypes.md | 224 --- cmd/clef/docs/clef_architecture_pt1.png | Bin 69221 -> 0 bytes cmd/clef/docs/clef_architecture_pt2.png | Bin 81521 -> 0 bytes cmd/clef/docs/clef_architecture_pt3.png | Bin 101351 -> 0 bytes cmd/clef/docs/clef_architecture_pt4.png | Bin 117597 -> 0 bytes cmd/clef/docs/qubes/clef_qubes_http.png | Bin 12237 -> 0 bytes cmd/clef/docs/qubes/clef_qubes_qrexec.png | Bin 17443 -> 0 bytes cmd/clef/docs/qubes/qrexec-example.png | Bin 16166 -> 0 bytes cmd/clef/docs/qubes/qubes-client.py | 23 - cmd/clef/docs/qubes/qubes.Clefsign | 16 - cmd/clef/docs/qubes/qubes_newaccount-1.png | Bin 22348 -> 0 bytes cmd/clef/docs/qubes/qubes_newaccount-2.png | Bin 37250 -> 0 bytes cmd/clef/docs/setup.md | 198 --- cmd/clef/extapi_changelog.md | 104 -- cmd/clef/intapi_changelog.md | 191 --- cmd/clef/main.go | 1222 ----------------- cmd/clef/pythonsigner.py | 315 ----- cmd/clef/requirements.txt | 1 - cmd/clef/rules.md | 234 ---- cmd/clef/run_test.go | 103 -- cmd/clef/sign_flow.png | Bin 20537 -> 0 bytes .../sign_1559_missing_field_exp_fail.json | 16 - ...gn_1559_missing_maxfeepergas_exp_fail.json | 16 - cmd/clef/testdata/sign_1559_tx.json | 17 - .../testdata/sign_bad_checksum_exp_fail.json | 17 - cmd/clef/testdata/sign_normal_exp_ok.json | 17 - cmd/clef/tests/testsigner.js | 89 -- cmd/clef/tutorial.md | 353 ----- signer/core/api.go | 668 --------- signer/core/api_test.go | 320 ----- signer/core/apitypes/types_test.go | 10 - signer/core/auditlog.go | 127 -- signer/core/cliui.go | 281 ---- signer/core/gnosis_safe.go | 117 -- signer/core/signed_data.go | 347 ----- signer/core/signed_data_test.go | 1062 -------------- signer/core/stdioui.go | 120 -- signer/core/testdata/README.md | 5 - signer/core/testdata/arrays-1.json | 60 - signer/core/testdata/custom_arraytype.json | 54 - signer/core/testdata/eip712.json | 76 - .../testdata/expfail_arraytype_overload.json | 67 - .../core/testdata/expfail_datamismatch_1.json | 64 - signer/core/testdata/expfail_extradata.json | 77 -- .../testdata/expfail_malformeddomainkeys.json | 64 - .../testdata/expfail_nonexistant_type.json | 64 - .../testdata/expfail_nonexistant_type2.json | 76 - .../core/testdata/expfail_toolargeuint.json | 38 - .../core/testdata/expfail_toolargeuint2.json | 38 - .../testdata/expfail_unconvertiblefloat.json | 38 - .../testdata/expfail_unconvertiblefloat2.json | 38 - .../2850f6ccf2d7f5f846dfb73119b60e09e712783f | 38 - .../36fb987a774011dc675e1b5246ac5c1d44d84d92 | 60 - .../37ec7b55c7ba014cced204c5f9989d2d0eb9ff6d | 38 - .../582fa92154b784daa1faa293b695fa388fe34bf1 | 1 - .../ab57cb2b2b5ce614efe13a47bc73814580f2cce8 | 54 - .../e4303e23ca34fbbc43164a232b2caa7a3af2bf8d | 64 - .../f658340af009dd4a35abe645a00a7b732bc30921 | 1 - signer/core/uiapi.go | 219 --- signer/core/validation.go | 36 - signer/core/validation_test.go | 45 - signer/rules/rules.go | 240 ---- signer/rules/rules_test.go | 626 --------- 68 files changed, 1 insertion(+), 9439 deletions(-) delete mode 100644 cmd/clef/README.md delete mode 100644 cmd/clef/consolecmd_test.go delete mode 100644 cmd/clef/datatypes.md delete mode 100644 cmd/clef/docs/clef_architecture_pt1.png delete mode 100644 cmd/clef/docs/clef_architecture_pt2.png delete mode 100644 cmd/clef/docs/clef_architecture_pt3.png delete mode 100644 cmd/clef/docs/clef_architecture_pt4.png delete mode 100644 cmd/clef/docs/qubes/clef_qubes_http.png delete mode 100644 cmd/clef/docs/qubes/clef_qubes_qrexec.png delete mode 100644 cmd/clef/docs/qubes/qrexec-example.png delete mode 100644 cmd/clef/docs/qubes/qubes-client.py delete mode 100644 cmd/clef/docs/qubes/qubes.Clefsign delete mode 100644 cmd/clef/docs/qubes/qubes_newaccount-1.png delete mode 100644 cmd/clef/docs/qubes/qubes_newaccount-2.png delete mode 100644 cmd/clef/docs/setup.md delete mode 100644 cmd/clef/extapi_changelog.md delete mode 100644 cmd/clef/intapi_changelog.md delete mode 100644 cmd/clef/main.go delete mode 100644 cmd/clef/pythonsigner.py delete mode 100644 cmd/clef/requirements.txt delete mode 100644 cmd/clef/rules.md delete mode 100644 cmd/clef/run_test.go delete mode 100644 cmd/clef/sign_flow.png delete mode 100644 cmd/clef/testdata/sign_1559_missing_field_exp_fail.json delete mode 100644 cmd/clef/testdata/sign_1559_missing_maxfeepergas_exp_fail.json delete mode 100644 cmd/clef/testdata/sign_1559_tx.json delete mode 100644 cmd/clef/testdata/sign_bad_checksum_exp_fail.json delete mode 100644 cmd/clef/testdata/sign_normal_exp_ok.json delete mode 100644 cmd/clef/tests/testsigner.js delete mode 100644 cmd/clef/tutorial.md delete mode 100644 signer/core/api.go delete mode 100644 signer/core/api_test.go delete mode 100644 signer/core/auditlog.go delete mode 100644 signer/core/cliui.go delete mode 100644 signer/core/gnosis_safe.go delete mode 100644 signer/core/signed_data.go delete mode 100644 signer/core/signed_data_test.go delete mode 100644 signer/core/stdioui.go delete mode 100644 signer/core/testdata/README.md delete mode 100644 signer/core/testdata/arrays-1.json delete mode 100644 signer/core/testdata/custom_arraytype.json delete mode 100644 signer/core/testdata/eip712.json delete mode 100644 signer/core/testdata/expfail_arraytype_overload.json delete mode 100644 signer/core/testdata/expfail_datamismatch_1.json delete mode 100644 signer/core/testdata/expfail_extradata.json delete mode 100644 signer/core/testdata/expfail_malformeddomainkeys.json delete mode 100644 signer/core/testdata/expfail_nonexistant_type.json delete mode 100644 signer/core/testdata/expfail_nonexistant_type2.json delete mode 100644 signer/core/testdata/expfail_toolargeuint.json delete mode 100644 signer/core/testdata/expfail_toolargeuint2.json delete mode 100644 signer/core/testdata/expfail_unconvertiblefloat.json delete mode 100644 signer/core/testdata/expfail_unconvertiblefloat2.json delete mode 100644 signer/core/testdata/fuzzing/2850f6ccf2d7f5f846dfb73119b60e09e712783f delete mode 100644 signer/core/testdata/fuzzing/36fb987a774011dc675e1b5246ac5c1d44d84d92 delete mode 100644 signer/core/testdata/fuzzing/37ec7b55c7ba014cced204c5f9989d2d0eb9ff6d delete mode 100644 signer/core/testdata/fuzzing/582fa92154b784daa1faa293b695fa388fe34bf1 delete mode 100644 signer/core/testdata/fuzzing/ab57cb2b2b5ce614efe13a47bc73814580f2cce8 delete mode 100644 signer/core/testdata/fuzzing/e4303e23ca34fbbc43164a232b2caa7a3af2bf8d delete mode 100644 signer/core/testdata/fuzzing/f658340af009dd4a35abe645a00a7b732bc30921 delete mode 100644 signer/core/uiapi.go delete mode 100644 signer/core/validation.go delete mode 100644 signer/core/validation_test.go delete mode 100644 signer/rules/rules.go delete mode 100644 signer/rules/rules_test.go diff --git a/README.md b/README.md index 639286ba9f..cc90c3128c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ directory. | Command | Description | | :--------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`geth`** | Our main Ethereum CLI client. It is the entry point into the Ethereum network (main-, test- or private net), capable of running as a full node (default), archive node (retaining all historical state) or a light node (retrieving data live). It can be used by other processes as a gateway into the Ethereum network via JSON RPC endpoints exposed on top of HTTP, WebSocket and/or IPC transports. `geth --help` and the [CLI page](https://geth.ethereum.org/docs/fundamentals/command-line-options) for command line options. | -| `clef` | Stand-alone signing tool, which can be used as a backend signer for `geth`. | | `devp2p` | Utilities to interact with nodes on the networking layer, without running a full blockchain. | | `abigen` | Source code generator to convert Ethereum contract definitions into easy-to-use, compile-time type-safe Go packages. It operates on plain [Ethereum contract ABIs](https://docs.soliditylang.org/en/develop/abi-spec.html) with expanded functionality if the contract bytecode is also available. However, it also accepts Solidity source files, making development much more streamlined. Please see our [Native DApps](https://geth.ethereum.org/docs/developers/dapp-developer/native-bindings) page for details. | | `evm` | Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode. Its purpose is to allow isolated, fine-grained debugging of EVM opcodes (e.g. `evm --code 60ff60ff --debug run`). | diff --git a/SECURITY.md b/SECURITY.md index d497248de5..50278b9a02 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,7 +11,6 @@ Audit reports are published in the `docs` folder: https://github.com/ethereum/go | Scope | Date | Report Link | | ------- | ------- | ----------- | | `geth` | 20170425 | [pdf](https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2017-04-25_Geth-audit_Truesec.pdf) | -| `clef` | 20180914 | [pdf](https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2018-09-14_Clef-audit_NCC.pdf) | | `Discv5` | 20191015 | [pdf](https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2019-10-15_Discv5_audit_LeastAuthority.pdf) | | `Discv5` | 20200124 | [pdf](https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2020-01-24_DiscV5_audit_Cure53.pdf) | diff --git a/build/ci.go b/build/ci.go index 173a3280ce..53ade2e1bf 100644 --- a/build/ci.go +++ b/build/ci.go @@ -75,7 +75,7 @@ var ( // Files that end up in the geth-alltools*.zip archive (and the NSIS installer // dev-tools section). Order matches the historical layout produced by ci.go. - allToolsBinaries = []string{"abigen", "evm", "geth", "rlpdump", "clef"} + allToolsBinaries = []string{"abigen", "evm", "geth", "rlpdump"} // Keeper build targets with their configurations keeperTargets = []struct { @@ -135,10 +135,6 @@ var ( BinaryName: "rlpdump", Description: "Developer utility tool that prints RLP structures.", }, - { - BinaryName: "clef", - Description: "Ethereum account management tool.", - }, } // A debian package is created for all executables listed here. diff --git a/cmd/clef/README.md b/cmd/clef/README.md deleted file mode 100644 index a92dcb1d77..0000000000 --- a/cmd/clef/README.md +++ /dev/null @@ -1,922 +0,0 @@ -# Clef - -Clef can be used to sign transactions and data and is meant as a(n eventual) replacement for Geth's account management. This allows DApps to not depend on Geth's account management. When a DApp wants to sign data (or a transaction), it can send the content to Clef, which will then provide the user with context and ask for permission to sign the content. If the user grants the signing request, Clef will send the signature back to the DApp. - -This setup allows a DApp to connect to a remote Ethereum node and send transactions that are locally signed. This can help in situations when a DApp is connected to an untrusted remote Ethereum node, because a local one is not available, not synchronized with the chain, or is a node that has no built-in (or limited) account management. - -Clef can run as a daemon on the same machine, off a usb-stick like [USB armory](https://inversepath.com/usbarmory), or even a separate VM in a [QubesOS](https://www.qubes-os.org/) type setup. - -Check out the - -* [CLI tutorial](tutorial.md) for some concrete examples on how Clef works. -* [Setup docs](docs/setup.md) for information on how to configure Clef on QubesOS or USB Armory. -* [Data types](datatypes.md) for details on the communication messages between Clef and an external UI. - -## Command line flags - -Clef accepts the following command line options: - -``` -COMMANDS: - init Initialize the signer, generate secret storage - attest Attest that a js-file is to be used - setpw Store a credential for a keystore file - delpw Remove a credential for a keystore file - gendoc Generate documentation about json-rpc format - help Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --loglevel value log level to emit to the screen (default: 4) - --keystore value Directory for the keystore (default: "$HOME/.ethereum/keystore") - --configdir value Directory for Clef configuration (default: "$HOME/.clef") - --chainid value Chain id to use for signing (1=mainnet, 17000=Holesky) (default: 1) - --lightkdf Reduce key-derivation RAM & CPU usage at some expense of KDF strength - --nousb Disables monitoring for and managing USB hardware wallets - --pcscdpath value Path to the smartcard daemon (pcscd) socket file (default: "/run/pcscd/pcscd.comm") - --http.addr value HTTP-RPC server listening interface (default: "localhost") - --http.vhosts value Comma separated list of virtual hostnames from which to accept requests (server enforced). Accepts '*' wildcard. (default: "localhost") - --ipcdisable Disable the IPC-RPC server - --ipcpath Filename for IPC socket/pipe within the datadir (explicit paths escape it) - --http Enable the HTTP-RPC server - --http.port value HTTP-RPC server listening port (default: 8550) - --signersecret value A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash - --4bytedb-custom value File used for writing new 4byte-identifiers submitted via API (default: "./4byte-custom.json") - --auditlog value File used to emit audit logs. Set to "" to disable (default: "audit.log") - --rules value Path to the rule file to auto-authorize requests with - --stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when Clef is started by an external process. - --stdio-ui-test Mechanism to test interface between Clef and UI. Requires 'stdio-ui'. - --advanced If enabled, issues warnings instead of rejections for suspicious requests. Default off - --suppress-bootwarn If set, does not show the warning during boot - --help, -h show help - --version, -v print the version -``` - -Example: - -``` -$ clef -keystore /my/keystore -chainid 4 -``` - -## Security model - -The security model of Clef is as follows: - -* One critical component (the Clef binary / daemon) is responsible for handling cryptographic operations: signing, private keys, encryption/decryption of keystore files. -* Clef has a well-defined 'external' API. -* The 'external' API is considered UNTRUSTED. -* Clef also communicates with whatever process that invoked the binary, via stdin/stdout. - * This channel is considered 'trusted'. Over this channel, approvals and passwords are communicated. - -The general flow for signing a transaction using e.g. Geth is as follows: -![image](sign_flow.png) - -In this case, `geth` would be started with `--signer http://localhost:8550` and would relay requests to `eth.sendTransaction`. - -## TODOs - -Some snags and todos - -* [ ] Clef should take a startup param "--no-change", for UIs that do not contain the capability to perform changes to things, only approve/deny. Such a UI should be able to start the signer in a more secure mode by telling it that it only wants approve/deny capabilities. -* [x] It would be nice if Clef could collect new 4byte-id:s/method selectors, and have a secondary database for those (`4byte_custom.json`). Users could then (optionally) submit their collections for inclusion upstream. -* [ ] It should be possible to configure Clef to check if an account is indeed known to it, before passing on to the UI. The reason it currently does not, is that it would make it possible to enumerate accounts if it immediately returned "unknown account" (side channel attack). -* [x] It should be possible to configure Clef to auto-allow listing (certain) accounts, instead of asking every time. -* [x] Done Upon startup, Clef should spit out some info to the caller (particularly important when executed in `stdio-ui`-mode), invoking methods with the following info: - * [x] Version info about the signer - * [x] Address of API (HTTP/IPC) - * [ ] List of known accounts -* [ ] Have a default timeout on signing operations, so that if the user has not answered within e.g. 60 seconds, the request is rejected. -* [ ] `account_signRawTransaction` -* [ ] `account_bulkSignTransactions([] transactions)` should - * only exist if enabled via config/flag - * only allow non-data-sending transactions - * all txs must use the same `from`-account - * let the user confirm, showing - * the total amount - * the number of unique recipients - -* Geth todos - - The signer should pass the `Origin` header as call-info to the UI. As of right now, the way that info about the request is put together is a bit of a hack into the HTTP server. This could probably be greatly improved. - - Relay: Geth should be started in `geth --signer localhost:8550`. - - Currently, the Geth APIs use `common.Address` in the arguments to transaction submission (e.g `to` field). This type is 20 `bytes`, and is incapable of carrying checksum information. The signer uses `common.MixedcaseAddress`, which retains the original input. - - The Geth API should switch to use the same type, and relay `to`-account verbatim to the external API. -* [x] Storage - * [x] An encrypted key-value storage should be implemented. - * See [rules.md](rules.md) for more info about this. -* Another potential thing to introduce is pairing. - * To prevent spurious requests which users just accept, implement a way to "pair" the caller with the signer (external API). - * Thus Geth/cpp would cryptographically handshake and afterwards the caller would be allowed to make signing requests. - * This feature would make the addition of rules less dangerous. - -* Wallets / accounts. Add API methods for wallets. - -## Communication - -### External API - -Clef listens to HTTP requests on `http.addr`:`http.port` (or to IPC on `ipcpath`), with the same JSON-RPC standard as Geth. The messages are expected to be [JSON-RPC 2.0 standard](https://www.jsonrpc.org/specification). - -Some of these calls can require user interaction. Clients must be aware that responses may be delayed significantly or may never be received if a user decides to ignore the confirmation request. - -The External API is **untrusted**: it does not accept credentials, nor does it expect that requests have any authority. - -### Internal UI API - -Clef has one native console-based UI, for operation without any standalone tools. However, there is also an API to communicate with an external UI. To enable that UI, the signer needs to be executed with the `--stdio-ui` option, which allocates `stdin` / `stdout` for the UI API. - -An example (insecure) proof-of-concept has been implemented in `pythonsigner.py`. - -The model is as follows: - -* The user starts the UI app (`pythonsigner.py`). -* The UI app starts `clef` with `--stdio-ui`, and listens to the -process output for confirmation-requests. -* `clef` opens the external HTTP API. -* When the `signer` receives requests, it sends a JSON-RPC request via `stdout`. -* The UI app prompts the user accordingly, and responds to `clef`. -* `clef` signs (or not), and responds to the original request. - -## External API - -See the [external API changelog](extapi_changelog.md) for information about changes to this API. - -### Encoding -- number: positive integers that are hex encoded -- data: hex encoded data -- string: ASCII string - -All hex encoded values must be prefixed with `0x`. - -### account_new - -#### Create new password protected account - -The signer will generate a new private key, encrypt it according to [web3 keystore spec](https://ethereum.org/en/developers/docs/data-structures-and-encoding/web3-secret-storage/) and store it in the keystore directory. -The client is responsible for creating a backup of the keystore. If the keystore is lost there is no method of retrieving lost accounts. - -#### Arguments - -None - -#### Result - - address [string]: account address that is derived from the generated key - -#### Sample call -```json -{ - "id": 0, - "jsonrpc": "2.0", - "method": "account_new", - "params": [] -} -``` -Response -```json -{ - "id": 0, - "jsonrpc": "2.0", - "result": "0xbea9183f8f4f03d427f6bcea17388bdff1cab133" -} -``` - -### account_list - -#### List available accounts - List all accounts that this signer currently manages - -#### Arguments - -None - -#### Result - - array with account records: - - account.address [string]: account address that is derived from the generated key - -#### Sample call -```json -{ - "id": 1, - "jsonrpc": "2.0", - "method": "account_list" -} -``` -Response -```json -{ - "id": 1, - "jsonrpc": "2.0", - "result": [ - "0xafb2f771f58513609765698f65d3f2f0224a956f", - "0xbea9183f8f4f03d427f6bcea17388bdff1cab133" - ] -} -``` - -### account_signTransaction - -#### Sign transactions - Signs a transaction and responds with the signed transaction in RLP-encoded and JSON forms. - -#### Arguments - 1. transaction object: - - `from` [address]: account to send the transaction from - - `to` [address]: receiver account. If omitted or `0x`, will cause contract creation. - - `gas` [number]: maximum amount of gas to burn - - `gasPrice` [number]: gas price - - `value` [number:optional]: amount of Wei to send with the transaction - - `data` [data:optional]: input data - - `nonce` [number]: account nonce - 2. method signature [string:optional] - - The method signature, if present, is to aid decoding the calldata. Should consist of `methodname(paramtype,...)`, e.g. `transfer(uint256,address)`. The signer may use this data to parse the supplied calldata, and show the user. The data, however, is considered totally untrusted, and reliability is not expected. - - -#### Result - - raw [data]: signed transaction in RLP encoded form - - tx [json]: signed transaction in JSON form - -#### Sample call -```json -{ - "id": 2, - "jsonrpc": "2.0", - "method": "account_signTransaction", - "params": [ - { - "from": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", - "gas": "0x55555", - "gasPrice": "0x1234", - "input": "0xabcd", - "nonce": "0x0", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "value": "0x1234" - } - ] -} -``` -Response - -```json -{ - "jsonrpc": "2.0", - "id": 2, - "result": { - "raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "tx": { - "nonce": "0x0", - "gasPrice": "0x1234", - "gas": "0x55555", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "value": "0x1234", - "input": "0xabcd", - "v": "0x26", - "r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e", - "s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e" - } - } -} -``` -#### Sample call with ABI-data - - -```json -{ - "id": 67, - "jsonrpc": "2.0", - "method": "account_signTransaction", - "params": [ - { - "from": "0x694267f14675d7e1b9494fd8d72fefe1755710fa", - "gas": "0x333", - "gasPrice": "0x1", - "nonce": "0x0", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "value": "0x0", - "data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012" - }, - "safeSend(address)" - ] -} -``` -Response - -```json -{ - "jsonrpc": "2.0", - "id": 67, - "result": { - "raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "tx": { - "nonce": "0x0", - "gasPrice": "0x1", - "gas": "0x333", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "value": "0x0", - "input": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", - "v": "0x26", - "r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e", - "s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e" - } - } -} -``` - -Bash example: -```bash -> curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ - -{"jsonrpc":"2.0","id":67,"result":{"raw":"0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","tx":{"nonce":"0x0","gasPrice":"0x1","gas":"0x333","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0","value":"0x0","input":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012","v":"0x26","r":"0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e","s":"0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","hash":"0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"}}} -``` - -### account_signData - -#### Sign data - Signs a chunk of data and returns the calculated signature. - -#### Arguments - - content type [string]: type of signed data - - `text/validator`: hex data with a custom validator defined in a contract - - `application/clique`: [clique](https://github.com/ethereum/EIPs/issues/225) headers - - `text/plain`: simple hex data validated by `account_ecRecover` - - account [address]: account to sign with - - data [object]: data to sign - -#### Result - - calculated signature [data] - -#### Sample call -```json -{ - "id": 3, - "jsonrpc": "2.0", - "method": "account_signData", - "params": [ - "data/plain", - "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", - "0xaabbccdd" - ] -} -``` -Response - -```json -{ - "id": 3, - "jsonrpc": "2.0", - "result": "0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c" -} -``` - -### account_signTypedData - -#### Sign data - Signs a chunk of structured data conformant to [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and returns the calculated signature. - -#### Arguments - - account [address]: account to sign with - - data [object]: data to sign - -#### Result - - calculated signature [data] - -#### Sample call -```json -{ - "id": 68, - "jsonrpc": "2.0", - "method": "account_signTypedData", - "params": [ - "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": 1, - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } - ] -} -``` -Response - -```json -{ - "id": 1, - "jsonrpc": "2.0", - "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" -} -``` - -### account_ecRecover - -#### Recover the signing address - -Derive the address from the account that was used to sign data with content type `text/plain` and the signature. - -#### Arguments - - data [data]: data that was signed - - signature [data]: the signature to verify - -#### Result - - derived account [address] - -#### Sample call -```json -{ - "id": 4, - "jsonrpc": "2.0", - "method": "account_ecRecover", - "params": [ - "0xaabbccdd", - "0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c" - ] -} -``` -Response - -```json -{ - "id": 4, - "jsonrpc": "2.0", - "result": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db" -} -``` - -### account_version - -#### Get external API version - -Get the version of the external API used by Clef. - -#### Arguments - -None - -#### Result - -* external API version [string] - -#### Sample call -```json -{ - "id": 0, - "jsonrpc": "2.0", - "method": "account_version", - "params": [] -} -``` - -Response -```json -{ - "id": 0, - "jsonrpc": "2.0", - "result": "6.0.0" -} -``` - -## UI API - -These methods needs to be implemented by a UI listener. - -By starting the signer with the switch `--stdio-ui-test`, the signer will invoke all known methods, and expect the UI to respond with -denials. This can be used during development to ensure that the API is (at least somewhat) correctly implemented. -See `pythonsigner`, which can be invoked via `python3 pythonsigner.py test` to perform the 'denial-handshake-test'. - -All methods in this API use object-based parameters, so that there can be no mixup of parameters: each piece of data is accessed by key. - -See the [ui API changelog](intapi_changelog.md) for information about changes to this API. - -OBS! A slight deviation from `json` standard is in place: every request and response should be confined to a single line. -Whereas the `json` specification allows for linebreaks, linebreaks __should not__ be used in this communication channel, to make -things simpler for both parties. - -### ApproveTx / `ui_approveTx` - -Invoked when there's a transaction for approval. - - -#### Sample call - -Here's a method invocation: -```bash - -curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ -``` -Results in the following invocation on the UI: -```json - -{ - "jsonrpc": "2.0", - "id": 1, - "method": "ui_approveTx", - "params": [ - { - "transaction": { - "from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa", - "to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "gas": "0x333", - "gasPrice": "0x1", - "value": "0x0", - "nonce": "0x0", - "data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", - "input": null - }, - "call_info": [ - { - "type": "WARNING", - "message": "Invalid checksum on to-address" - }, - { - "type": "Info", - "message": "safeSend(address: 0x0000000000000000000000000000000000000012)" - } - ], - "meta": { - "remote": "127.0.0.1:48486", - "local": "localhost:8550", - "scheme": "HTTP/1.1" - } - } - ] -} - -``` - -The same method invocation, but with invalid data: -```bash - -curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x694267f14675d7e1b9494fd8d72fefe1755710fa","gas":"0x333","gasPrice":"0x1","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x0", "data":"0x4401a6e40000000000000002000000000000000000000000000000000000000000000012"},"safeSend(address)"],"id":67}' http://localhost:8550/ -``` - -```json - -{ - "jsonrpc": "2.0", - "id": 1, - "method": "ui_approveTx", - "params": [ - { - "transaction": { - "from": "0x0x694267f14675d7e1b9494fd8d72fefe1755710fa", - "to": "0x0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "gas": "0x333", - "gasPrice": "0x1", - "value": "0x0", - "nonce": "0x0", - "data": "0x4401a6e40000000000000002000000000000000000000000000000000000000000000012", - "input": null - }, - "call_info": [ - { - "type": "WARNING", - "message": "Invalid checksum on to-address" - }, - { - "type": "WARNING", - "message": "Transaction data did not match ABI-interface: WARNING: Supplied data is stuffed with extra data. \nWant 0000000000000002000000000000000000000000000000000000000000000012\nHave 0000000000000000000000000000000000000000000000000000000000000012\nfor method safeSend(address)" - } - ], - "meta": { - "remote": "127.0.0.1:48492", - "local": "localhost:8550", - "scheme": "HTTP/1.1" - } - } - ] -} - - -``` - -One which has missing `to`, but with no `data`: - - -```json - -{ - "jsonrpc": "2.0", - "id": 3, - "method": "ui_approveTx", - "params": [ - { - "transaction": { - "from": "", - "to": null, - "gas": "0x0", - "gasPrice": "0x0", - "value": "0x0", - "nonce": "0x0", - "data": null, - "input": null - }, - "call_info": [ - { - "type": "CRITICAL", - "message": "Tx will create contract with empty code!" - } - ], - "meta": { - "remote": "signer binary", - "local": "main", - "scheme": "in-proc" - } - } - ] -} -``` - -### ApproveListing / `ui_approveListing` - -Invoked when a request for account listing has been made. - -#### Sample call - -```json - -{ - "jsonrpc": "2.0", - "id": 5, - "method": "ui_approveListing", - "params": [ - { - "accounts": [ - { - "url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-20T14-44-54.089682944Z--123409812340981234098123409812deadbeef42", - "address": "0x123409812340981234098123409812deadbeef42" - }, - { - "url": "keystore:///home/bazonk/.ethereum/keystore/UTC--2017-11-23T21-59-03.199240693Z--cafebabedeadbeef34098123409812deadbeef42", - "address": "0xcafebabedeadbeef34098123409812deadbeef42" - } - ], - "meta": { - "remote": "signer binary", - "local": "main", - "scheme": "in-proc" - } - } - ] -} - -``` - - -### ApproveSignData / `ui_approveSignData` - -#### Sample call - -```json -{ - "jsonrpc": "2.0", - "id": 4, - "method": "ui_approveSignData", - "params": [ - { - "address": "0x123409812340981234098123409812deadbeef42", - "raw_data": "0x01020304", - "messages": [ - { - "name": "message", - "value": "\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004", - "type": "text/plain" - } - ], - "hash": "0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310", - "meta": { - "remote": "signer binary", - "local": "main", - "scheme": "in-proc" - } - } - ] -} -``` - -### ApproveNewAccount / `ui_approveNewAccount` - -Invoked when a request for creating a new account has been made. - -#### Sample call - -```json -{ - "jsonrpc": "2.0", - "id": 4, - "method": "ui_approveNewAccount", - "params": [ - { - "meta": { - "remote": "signer binary", - "local": "main", - "scheme": "in-proc" - } - } - ] -} -``` - -### ShowInfo / `ui_showInfo` - -The UI should show the info (a single message) to the user. Does not expect response. - -#### Sample call - -```json -{ - "jsonrpc": "2.0", - "id": 9, - "method": "ui_showInfo", - "params": [ - "Tests completed" - ] -} - -``` - -### ShowError / `ui_showError` - -The UI should show the error (a single message) to the user. Does not expect response. - -```json - -{ - "jsonrpc": "2.0", - "id": 2, - "method": "ui_showError", - "params": [ - "Something bad happened!" - ] -} - -``` - -### OnApprovedTx / `ui_onApprovedTx` - -`OnApprovedTx` is called when a transaction has been approved and signed. The call contains the return value that will be sent to the external caller. The return value from this method is ignored - the reason for having this callback is to allow the ruleset to keep track of approved transactions. - -When implementing rate-limited rules, this callback should be used. - -TLDR; Use this method to keep track of signed transactions, instead of using the data in `ApproveTx`. - -Example call: -```json - -{ - "jsonrpc": "2.0", - "id": 1, - "method": "ui_onApprovedTx", - "params": [ - { - "raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "tx": { - "nonce": "0x0", - "gasPrice": "0x1", - "gas": "0x333", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "value": "0x0", - "input": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", - "v": "0x26", - "r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e", - "s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", - "hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e" - } - } - ] -} -``` - -### OnSignerStartup / `ui_onSignerStartup` - -This method provides the UI with information about what API version the signer uses (both internal and external) as well as build-info and external API, -in k/v-form. - -Example call: -```json - -{ - "jsonrpc": "2.0", - "id": 1, - "method": "ui_onSignerStartup", - "params": [ - { - "info": { - "extapi_http": "http://localhost:8550", - "extapi_ipc": null, - "extapi_version": "2.0.0", - "intapi_version": "1.2.0" - } - } - ] -} - -``` - -### OnInputRequired / `ui_onInputRequired` - -Invoked when Clef requires user input (e.g. a password). - -Example call: -```json - -{ - "jsonrpc": "2.0", - "id": 1, - "method": "ui_onInputRequired", - "params": [ - { - "title": "Account password", - "prompt": "Please enter the password for account 0x694267f14675d7e1b9494fd8d72fefe1755710fa", - "isPassword": true - } - ] -} -``` - - -### Rules for UI apis - -A UI should conform to the following rules. - -* A UI MUST NOT load any external resources that were not embedded/part of the UI package. - * For example, not load icons, stylesheets from the internet - * Not load files from the filesystem, unless they reside in the same local directory (e.g. config files) -* A Graphical UI MUST show the blocky-identicon for ethereum addresses. -* A UI MUST warn display appropriate warning if the destination-account is formatted with invalid checksum. -* A UI MUST NOT open any ports or services - * The signer opens the public port -* A UI SHOULD verify the permissions on the signer binary, and refuse to execute or warn if permissions allow non-user write. -* A UI SHOULD inform the user about the `SHA256` or `MD5` hash of the binary being executed -* A UI SHOULD NOT maintain a secondary storage of data, e.g. list of accounts - * The signer provides accounts -* A UI SHOULD, to the best extent possible, use static linking / bundling, so that required libraries are bundled -along with the UI. - - -### UI Implementations - -There are a couple of implementation for a UI. We'll try to keep this list up to date. - -| Name | Repo | UI type| No external resources| Blocky support| Verifies permissions | Hash information | No secondary storage | Statically linked| Can modify parameters| -| ---- | ---- | -------| ---- | ---- | ---- |---- | ---- | ---- | ---- | -| QtSigner| https://github.com/holiman/qtsigner/ | Python3/QT-based| :+1:| :+1:| :+1:| :+1:| :+1:| :x: | :+1: (partially)| -| GtkSigner| https://github.com/holiman/gtksigner | Python3/GTK-based| :+1:| :x:| :x:| :+1:| :+1:| :x: | :x: | -| Frame | https://github.com/floating/frame/commits/go-signer | Electron-based| :x:| :x:| :x:| :x:| ?| :x: | :x: | -| Clef UI| https://github.com/ethereum/clef-ui | Golang/QT-based| :+1:| :+1:| :x:| :+1:| :+1:| :x: | :+1: (approve tx only)| diff --git a/cmd/clef/consolecmd_test.go b/cmd/clef/consolecmd_test.go deleted file mode 100644 index a5b324c53f..0000000000 --- a/cmd/clef/consolecmd_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" -) - -// TestImportRaw tests clef --importraw -func TestImportRaw(t *testing.T) { - t.Parallel() - keyPath := filepath.Join(t.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name())) - os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777) - - t.Run("happy-path", func(t *testing.T) { - t.Parallel() - // Run clef importraw - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath) - clef.input("myverylongpassword").input("myverylongpassword") - if out := string(clef.Output()); !strings.Contains(out, - "Key imported:\n Address 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") { - t.Logf("Output\n%v", out) - t.Error("Failure") - } - }) - // tests clef --importraw with mismatched passwords. - t.Run("pw-mismatch", func(t *testing.T) { - t.Parallel() - // Run clef importraw - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath) - clef.input("myverylongpassword1").input("myverylongpassword2").WaitExit() - if have, want := clef.StderrText(), "Passwords do not match\n"; have != want { - t.Errorf("have %q, want %q", have, want) - } - }) - // tests clef --importraw with a too short password. - t.Run("short-pw", func(t *testing.T) { - t.Parallel() - // Run clef importraw - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath) - clef.input("shorty").input("shorty").WaitExit() - if have, want := clef.StderrText(), - "password requirements not met: password too short (<10 characters)\n"; have != want { - t.Errorf("have %q, want %q", have, want) - } - }) -} - -// TestListAccounts tests clef --list-accounts -func TestListAccounts(t *testing.T) { - t.Parallel() - keyPath := filepath.Join(t.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name())) - os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777) - - t.Run("no-accounts", func(t *testing.T) { - t.Parallel() - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-accounts") - if out := string(clef.Output()); !strings.Contains(out, "The keystore is empty.") { - t.Logf("Output\n%v", out) - t.Error("Failure") - } - }) - t.Run("one-account", func(t *testing.T) { - t.Parallel() - // First, we need to import - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath) - clef.input("myverylongpassword").input("myverylongpassword").WaitExit() - // Secondly, do a listing, using the same datadir - clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-accounts") - if out := string(clef.Output()); !strings.Contains(out, "0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6 (keystore:") { - t.Logf("Output\n%v", out) - t.Error("Failure") - } - }) -} - -// TestListWallets tests clef --list-wallets -func TestListWallets(t *testing.T) { - t.Parallel() - keyPath := filepath.Join(t.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name())) - os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777) - - t.Run("no-accounts", func(t *testing.T) { - t.Parallel() - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-wallets") - if out := string(clef.Output()); !strings.Contains(out, "There are no wallets.") { - t.Logf("Output\n%v", out) - t.Error("Failure") - } - }) - t.Run("one-account", func(t *testing.T) { - t.Parallel() - // First, we need to import - clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath) - clef.input("myverylongpassword").input("myverylongpassword").WaitExit() - // Secondly, do a listing, using the same datadir - clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-wallets") - if out := string(clef.Output()); !strings.Contains(out, "Account 0: 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") { - t.Logf("Output\n%v", out) - t.Error("Failure") - } - }) -} diff --git a/cmd/clef/datatypes.md b/cmd/clef/datatypes.md deleted file mode 100644 index 8456edfa35..0000000000 --- a/cmd/clef/datatypes.md +++ /dev/null @@ -1,224 +0,0 @@ -## UI Client interface - -These data types are defined in the channel between clef and the UI -### SignDataRequest - -SignDataRequest contains information about a pending request to sign some data. The data to be signed can be of various types, defined by content-type. Clef has done most of the work in canonicalizing and making sense of the data, and it's up to the UI to present the user with the contents of the `message` - -Example: -```json -{ - "content_type": "text/plain", - "address": "0xDEADbEeF000000000000000000000000DeaDbeEf", - "raw_data": "GUV0aGVyZXVtIFNpZ25lZCBNZXNzYWdlOgoxMWhlbGxvIHdvcmxk", - "messages": [ - { - "name": "message", - "value": "\u0019Ethereum Signed Message:\n11hello world", - "type": "text/plain" - } - ], - "hash": "0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68", - "meta": { - "remote": "localhost:9999", - "local": "localhost:8545", - "scheme": "http", - "User-Agent": "Firefox 3.2", - "Origin": "www.malicious.ru" - } -} -``` -### SignDataResponse - approve - -Response to SignDataRequest - -Example: -```json -{ - "approved": true -} -``` -### SignDataResponse - deny - -Response to SignDataRequest - -Example: -```json -{ - "approved": false -} -``` -### SignTxRequest - -SignTxRequest contains information about a pending request to sign a transaction. Aside from the transaction itself, there is also a `call_info`-struct. That struct contains messages of various types, that the user should be informed of. - -As in any request, it's important to consider that the `meta` info also contains untrusted data. - -The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, they must be identical, otherwise an error is generated. However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket) - -Example: -```json -{ - "transaction": { - "from": "0xDEADbEeF000000000000000000000000DeaDbeEf", - "to": null, - "gas": "0x3e8", - "gasPrice": "0x5", - "value": "0x6", - "nonce": "0x1", - "data": "0x01020304" - }, - "call_info": [ - { - "type": "Warning", - "message": "Something looks odd, show this message as a warning" - }, - { - "type": "Info", - "message": "User should see this as well" - } - ], - "meta": { - "remote": "localhost:9999", - "local": "localhost:8545", - "scheme": "http", - "User-Agent": "Firefox 3.2", - "Origin": "www.malicious.ru" - } -} -``` -### SignTxResponse - approve - -Response to request to sign a transaction. This response needs to contain the `transaction`, because the UI is free to make modifications to the transaction. - -Example: -```json -{ - "transaction": { - "from": "0xDEADbEeF000000000000000000000000DeaDbeEf", - "to": null, - "gas": "0x3e8", - "gasPrice": "0x5", - "value": "0x6", - "nonce": "0x4", - "data": "0x04030201" - }, - "approved": true -} -``` -### SignTxResponse - deny - -Response to SignTxRequest. When denying a request, there's no need to provide the transaction in return - -Example: -```json -{ - "transaction": { - "from": "0x", - "to": null, - "gas": "0x0", - "gasPrice": "0x0", - "value": "0x0", - "nonce": "0x0", - "data": null - }, - "approved": false -} -``` -### OnApproved - SignTransactionResult - -SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)` - -This occurs _after_ successful completion of the entire signing procedure, but right before the signed transaction is passed to the external caller. This method (and data) can be used by the UI to signal to the user that the transaction was signed, but it is primarily useful for ruleset implementations. - -A ruleset that implements a rate limitation needs to know what transactions are sent out to the external interface. By hooking into this methods, the ruleset can maintain track of that count. - -**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. - -The `OnApproved` method cannot be responded to, it's purely informative - -Example: -```json -{ - "raw": "0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed", - "tx": { - "nonce": "0x64", - "gasPrice": "0x1", - "gas": "0x1", - "to": "0x8a8eafb1cf62bfbeb1741769dae1a9dd47996192", - "value": "0x1", - "input": "0x", - "v": "0x26", - "r": "0x716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293", - "s": "0x4e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed", - "hash": "0x662f6d772692dd692f1b5e8baa77a9ff95bbd909362df3fc3d301aafebde5441" - } -} -``` -### UserInputRequest - -Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free) - -Example: -```json -{ - "prompt": "The question to ask the user", - "title": "The title here", - "isPassword": true -} -``` -### UserInputResponse - -Response to UserInputRequest - -Example: -```json -{ - "text": "The textual response from user" -} -``` -### ListRequest - -Sent when a request has been made to list addresses. The UI is provided with the full `account`s, including local directory names. Note: this information is not passed back to the external caller, who only sees the `address`es. - -Example: -```json -{ - "accounts": [ - { - "address": "0xdeadbeef000000000000000000000000deadbeef", - "url": "keystore:///path/to/keyfile/a" - }, - { - "address": "0x1111111122222222222233333333334444444444", - "url": "keystore:///path/to/keyfile/b" - } - ], - "meta": { - "remote": "localhost:9999", - "local": "localhost:8545", - "scheme": "http", - "User-Agent": "Firefox 3.2", - "Origin": "www.malicious.ru" - } -} -``` -### ListResponse - -Response to list request. The response contains a list of all addresses to show to the caller. Note: the UI is free to respond with any address the caller, regardless of whether it exists or not - -Example: -```json -{ - "accounts": [ - { - "address": "0x0000000000000000000000000000000000000000", - "url": ".. ignored .." - }, - { - "address": "0xffffffffffffffffffffffffffffffffffffffff", - "url": "" - } - ] -} -``` diff --git a/cmd/clef/docs/clef_architecture_pt1.png b/cmd/clef/docs/clef_architecture_pt1.png deleted file mode 100644 index e40e532f3051d6b583698b539ae97bf12560f740..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69221 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

*@V)cs~^7_udH`|DL0Y7#oV`hCx? zBw9LbZ%2B4t@0a7E=Ow)V~=9RKm}p{`;E4qDzmE<9enV`uTk(5Um0f}t>WoX#V!W1 z14aJs`}!XBL)2OpK!7QsRJY&{%X=4wj!8iECkkVa>Q88QsZ~eHVs?s$zBGY5jx`MZ z%HD_^QCJbSLKN$3Y7?HAltrJI92IUnJcU;g)>g!VEJ*{6J!RL=qj?!R8@~Xr1BzuwW-(xMIbJHwx73%J2c(TeHvBebP3SpUg}b^Ef` zgr`cm0^IU0b99#Fcv*>fik=eMm%rs!96dF~~IZqFs?gbdB?b2O(VSxuv7 zbY~{MA%{MS9^MIIj`s`@A2{lv7nV-{Z{b!}_Rvj*de=N&y0&1icJH-2^^vUZSUuPc zy}}xEOG*e`f83a8hrK1OhQ?W)KCFV1mh$z3%GYeHt^|68Yvh)GorMiG-{ykX-k$5j zC2GbBo8^geBvCi1FK0FdD)eP2rxl4L7A>R zYHvY}>#Vi_<(KM%TKcRap185goW1_8I9gXs2K$d?R2s4iaV5RI$_hXkSy^07UOEkZ zeRY3T2^@XPB*Td3CKQ=OgO{4SD*?^Hq}i|AYHOWU@F7%W$6HXvw**_`y<3&BmYldR z5z7=9z34jwfCjf;LLh?~pZ726zl+n+q2!WYru^kx%e<+r`7@ZQ<%k*WIb=%mLHsjfc5z^|q-8OlO zskjqh`NP`I(Pl{YXUBa-YLZTJF)(LU0S}_U?G%4SCttY(`dtxx^e*&j`8ZpwlP5Muw|cRA9?@&lbmcJ|4X#``&X=FLd zf{>ydWn<-fIf~JsNRk!Rr5^KOHFAfjm}i=82@mT*JaU>{9VF6j*Pm?bq{oLEx;hNS zy5Amms#u&^1SW1ur6PhrmS2#o?+5GJ+ z(hVNnJw1P7KyGeshW`A4PZv*5P1QeRCkRU_#FqsVRulb+$sjjKM~rYzlone|^m;zi++5hB67!xlA;VLYTddFxTkHJ*I+KggBw%bJ$r>%;GkKeCvJ+pUgxNd`&DVclWiKH)HiEEQTG)mKVGp_^RU&+j0DaA_#U-S@&5OKXX(KG#5ECdHUV zy@#`Zt3`mCVm)7E1-G>4i4P8(q4TNP5Dq`94xJ6K?~EDFMA+Lg!t&pzyN?%@QFU5# zIkzFHYpN-X;Jmn6w)6F89~6Bt{aEBexrV0V(!pZ)roT`(8v}_6_u#?Q9CkScOnKc9 zn@&k0&Y&ya;bMFUnvq&)c4as_J3}7B2(L=SpFN|pfs^e%_h+XLDzn!0Ajb6oD^_Cz zKr?eii$)mrA3ykB8tH@>5z+graZbLy60lIm%NB$9;6hSsskH#L94htysT-kST0Tl? z!Rgl3N9U#UT07S_+{F-wN%q4}X@=3&`BEg>x;jYL=ye&a_2P$zIdjLHY7a#YxY9oc zOYxGH{C3VSTMu z?%5W!k(*fUkl$cIkX7 z^SX#g@3}`KM(^2_lOdrye+JNxW?(Of0C85>{qf>V0Ynzu!t{d)d=?7oJ+X;}smwnQ zo+hzSTY9J(vW0=#Z@$g}1$4s759?Y{c@1D1Aq&W2qoe2Tg){m|2b;U`RayxbqSz%a z>o*o9-%zHzk7Z{&D=QZE4i2YhJFTt$qhDM;QLhr#PY42CB0tuzaAQZs3U31D<|Nzt zLtD@Yri#VQK|Y8L^hu)g@#}jPALS-A>&`-~t%Gz~?GRMmyPa|J3k^Zw$l^nU=^$^( zby6Lkg#>0recQcSjEzkg5f*!5vCKK)ZFSOhP8?T9Tb4;xej>HBcslay8Q}pPaP+ie zbg>HMaR2;PR~uKHS}5eW7{*v&xO1A`RmVuj$xWG)Q)@N|m6c|z)9YRX-^y0-?FI)u ziZX&i?*$*dZhF9q(ammt)u&IYykV~?l8^`g?H!ceX-|Z-;mytd3cZUfA_j|5%7_C% zY)HxDqi_&TUF@+04(Qw?seUP;BvDz!WBetdsAu$8b!dDeN-x6NO#E%<1}>vz-PrYU zf~OXb5Xs^A`qJy>>zp~3(=2tx5nP)JhWyLY4+auPlvmDUjemYf?VTyuo!Y&b%h4*j z?)cT;?C@yVSwuJjCAFVrKBpP$fh-=BDB8&{(N*`55MH2Xf%GxM-m>D9+w2iURQMIE@yG`;r52+q03zxu8QiTQCuFrNfe^4l`Uy@-l72*4 zP-Z8Ki4g%)HBOHNZKBcBfy}q|Q561?CgJt3=mq=g0k&+iI<#v>f$FiA#A~vh{duwG zo)_7ZM^uQiB;mM|^n#&x$T555m^5pj`+hgS#&5L7{3dFnMo!+4P$>|mr=t_OWq>O&&4g3v$uBz3nSBj-gX6#J z6vT!ZtoFt$5QGo$kwPnTa#*{1jIu90nH-|xNRCUWf1~*#7RZ}e;+m_U{npj)=+OW( z)>q%D*ikXHj=bl`L1+)wAr!6|#P-`l=d6cyQYzY$h%s)ti$jP{()`P=fHLlx?zAY61+a|yxJeIGQRgNw|*?Eb>$}6q$pd`mGa~r#qMc8r{6Dj zdp5Xw5FH9BP71+9f%!N#O|I;OA1FkoWC)_DW4{%AVp*bVFCA135@=ye#2W#nIp6#= z03j6Sw>v4@Y+uX)%BnpAto0R|5?<)kmnm4~vkE5qO8+X6*K?iO?&{A}Du1S(05oQQ zTf8#E6xX%n*|JKw*8AptM zd&PjOUFX^wFQvzO=c)Y`6>pZ}n=2`HKKjic(3c}_rIT*o<=b`6`wwR)J@7|20L%(= zuZgWQ_?MP`M?D2ponV(UK{gK1YL$6|CFr}rWXI(G;@Us=3dgS*I$SQY?`#vUpeLOJcj^uI_)Y!0Sw z4yAJ}7`-Kox~bi56QS?>V4MEt)pwq-@zK%90~r?YJ;!huPOg&I;d}2dO~jVvfju7_ zG6^9E86wI+>&+^GShS|Hf~nKhjTY&5a`1Z#C1n3QGW~vFSRw#K&JMJdYVOYJn(z5Z z_`fg??Lu#rQrI#dkIV>nm#APN=S1`&H=OxW&b=e0E)0Xeh^$Mn+NA|_1j8#xa)lDL zzk|fW@xfA7*rBsP69`*Ku^&P%xG&>5vmspdnoT<70ak~+u56ihxP4CGlUsT~{)B{A z%j=$F`e*4y{)X>uQ7sKKhtJMWK4=89c55-xXX5%Dbu)^QX$3{Ob14CE*W~@1T6!B8 z{dHbIPTl*;EfkiY88IEC3DpD{q6>`-9A^m3UBiUSxtMk9Le)*Jh9j$j+~9J%^6zJj zv@tv^)e&hEEfTj&_cJE2@n5gq@5~v&Axx9+nq+UGB#sCNq>q*(p5CDKfvR^NRSJyD zRk`Z4+v)k-lYELUk%eG!!fKTDdmAD@FL_l{lzuu9HEZYUgUg}iIX`9kbFbs-*U1tuzE-0xqrwxT7S9qKkEajK(z+B*0-1s^{=P} z?W40q1)zkAf>%bFuYNGKj%$C3`QRSQ*u8gGcWU4`jO04$8ig0M-idAaS=!E9{@!vt zd<-!HR2|$?#e!a68Poo&##AQR*uFTDQu}MS>Qj8R>&1xN6Lj^O_m32di;3HGXWpyL zP6Li!E`94$_1o1sjUE5%vnw~7fw>Ui#l_f_p-N}HInRcP;!TtlAU7SgpFFDrphrVA z>^DqOISC|EjB!6rqU%q$nRNPwY~v@8OrhXrp0{)me$=w2T(B*OUb}y`zRgaEY$8Ir zScVq5V}$SM=YB;F#&glrC)gNYWIc3fnS#6L02b&ti3C>ScueGN2zI6(qeQ%8>|RhG z5YuXwRG{d_$Kz7o(s$-X!kCt83`noJl&i|V^Ps&@?)ITqOTVg7cw#P>|YY4EBPY0 zUOgaPyvpvTT)ti_p}t(?@A(+e{X}-t!=zpV%q>;z5C2}jndk3V_>&Cdy4a|E(8KGV zsj3PTf;j~bvRpfZL6M59Icq!nw^0L+tgOIRA$HoZ>0O7qk`niwp>;n?eDIUzGxRkw z4`Cw;6VOw3Y~*Pw=*9-?;wu+?dT@F+PIJV4E5hUcJC~D<)_E`0Gtt7j%Yz*?`isnQ zGQOkJucl1_hY== z3s)&R^4EN(>nqfB|GDJWjuTmK`haULkZ+S55Jr4DnJCo*;i8Q7O{e{7&u^CGt9J0S zXjjIIBGyc;7b2Mrm(FIB#*zPvM=jQq>>%GWnH(tlAn*x6BoOgVqn+ylaKhh_#Np^> z4gKU&PqIvMH?itLEF7DmFA*GNmtK?=cTOYBr__pCQPZ)1WJ8u0_u~M27Pl6j!YFj^ zBE(57wid1FqJ7nzoONhF%c_v=X+Pf2Q&y{26pG9z50mn z*&)Q+s_kfbe#(jzei3~FypAWQC zcy8tg1#16*!Kh9E*McPJ5wQG&h-G=*{xNv&_a~ShfBO3;5FfUEhVTRJnaRg;#FkX@ z{-I0I>(g(fT9~ywjk-~tsyth*KMwvQyrlx$!_wW^PC>v9*qws|HU|+pCF&U6EzuN<(e?4xES9`e%-kd zRJ15)^}rO>fCFWuSqP3;8ORDNY8d@P{iqfn;MFQL)z=yGj@I?~(`zHbzuNd#ue}G8`;-H%zH$;a*9XN4_`}*Yuk}H7^QNF#-BYM?@t2jY0|LFdmGq?#> zch$VR?PMju|3_gs37hmLB}&WzFJ;M-`A6N#KypWFJs3sJo*)#@D!`v?WNm_FJ4}2| z_)`mUuy~X%Cv;l8Jgjmhec_MgMi5w=Ae~yX4CUxVF!nYVZsBzYoWP&p*1x(HSi8Gx zS_9Gl8`d$a;Fx7U>xz+=gdqAD2Zc|Fv&%t*W~1o95^keWw9>p2rV1i(SLaG~@YC_G z*Z}7F7rJoFDVr0jx?GI0jgNO%FINw4SSBfN;vkA!dF$5HFh${T>R zrQ>dt(EXQ8U}%6+RdUg@eOo&3C@E_Ki5k52FU57us<~VW(_12rJ7&O*kp)bFi(7(& zg54Fofeo)vY(Tf9JNi|ic1aIS2wN6$)rbLkZ{VC9EP-myE*o0u7Qsjw;iF;m|U-~ISRfY_6dN`wvufO!7-}a%XLrWbi0mQ{&r?W*rLgH_g z+{eHS2hV9m4w=&7^FYDaIyG3E%~`{aM3N2i$UO_dG_64wn#?g#r2GQ$mlY++3uqUY z?3+-~!td#$zOh}9BhksV!>kIx@hFDTm23;9{k)L1Hw`hvZ+w=Tn*I~1-7oxr?^a#| z7ike5-G=`D58I8ldq_VTR+GC<8Ff0cIq9?gE(OH5>?Uu+0k!fgqDx{kN@s+0G6rf& zw2v)(|KHyEY_6s8rm1)n_cI|ZE$Ry3;=;hfC2~1s9=~GA?HLcS2&Cd?C$=`-wMwz> zt>2u(^st?O9(@$+G8%d)7iy?LdO#c;xmhpNM!AL-NfeSvgXu+Op_{17JO2wLelcUn zBit?)9@I&hN6Pr?oE?ozm9`^X;#Yn0N(^HcYS<~Op1^&3h1l{%bd^#yJK~+mpIx%z z8vHGUZ!G|yJFY`t9OwAtW6HB0%!oTixL{b%_89lBfo#qi+t*;F>ZxakClB{`H^(5e zkj$)%xYilDr)4|roX?2h>7wy^8Uy7MPEEV=Z;T$~oS87|8QR4F!e$JKh7Zp5<}`7k zQT;*T$xZtKH|O%yvi}8bsa*i>YQ49>gAI}LsD_SJEn*A4WFx=F(zXHA!-W^xo#ngz zb_o-82GmjchI74pI>Y-1>+cdhxgnY-sgo(fSXV-;lxqWsrf;%7WRS{`nrM~wq8FxA z;V~c0dmGa#C>;d`qO!4a4UK!X0pI7@JB8rTM1(~p=kPPS$T9+Jwv3wNotUHsfRzl$ z$8Rosn42$>B0uh5q}6=pxuq36_v@zWJox>nwL%5NoF8;Y3I zlu5=OJK9KfGi;GGH^{`z8m-fIio%;Ck-kyNpUiL455)kyBVp>5AuivSQSnKf$!Tp! z@KDoj67ZVBROG3n&;ujg97tRuELEc8orceh^6-99JV=(lDqBx|r-W;wu%0n$aotk# z`rq$%X{su{^&*sl#lKGEN5~ez{iZ5aX=OOS-Tx&b1T#zoej@~Dwz>6n&li#njg&l( zbhQ=vs7WcqaI+bPX%tWT!B4leWpx2ZCAO1u50N|sC`U<|9H(BZ(hM>n``&?@A*1|F%fo-Drz@ggZQ6)$uIQ^ z1EGy0atUsBnXA?U6x)Y6dXheWK0Cr@;zJ8z>~RO*ryeMnlONAL15~}UMz`^4NU((}MTf+i?NH z@88yJhW{!Y`6-*)4m(YU40Mf0@9h`8S((0Xx~t$HK}#+Br`jRl6hTe7b}#UwV6}mX8?$D2RvG3m!6 z!tTO*2qNjFU*fl_=F8G4keRMQxeuRKBkn%}f}S$Q`b@Y95WS9CA5t-43c*s~9Za+E zr=52fIb7|ADh2FMF?|91rbg7is}DvC(J9%5zxUS}7n$K0Z1$~XY-Vg9cZW9^aq9%8 z`PJQv2)kzdbjs!#)DV|rY49AarY^KpC)7sBtXU>!$JBOD=(ZMs%mE4%-B!CHvf8`A z8*8B9hvUUYTux5Tg*GqlVw6C&0=e(w@81Ji9o~MZP zooY*v!6!x zmBWhx>y;2K&Vd;Qfs4Ne@jwClOb}%_?~`iuBrMvd!M28nd}nqT{!qn_X)GAQg`)7^ z`}KB=1}$gP$#>c>ZqU4As!kQC3#Q*x1-0rJ#uV?>`>ZmoM4b*)hn>TN>)j7q{?2 zL%h8uZ0+pGSXff~`&FNE|FE{xAY4lQ*ei_IrS?eGoqj?)Iciiy#udmZvIlL>Dxd0D zW~BTPqT7AfT!sPW18n0Jtq;!25VfRi0Lp3Klcm6Z*HY-l`ubOwII^h8Pk$x_ovRqn zHu85cbGR z>s=pxIpXwPepR(iYXmcpqdg=*t_bG$+uJaE+80lkEG%%h6ltp4m3&Qc?&F65Xf=_BktDHcfC+u!d--(w%&SUWEOOaA7Z@8?_QCVgd z74GGuLAl`yCdwF~qcmNx_q^iTWc55Dj9VF2&-Xdz@F((@Vb&t&i|HV~3sBYF>V#Lk z_$1m4CLdaHVOrYa3VnHdB^%uhn=4%r8DdY^oKJ)MA*Y_kn&w|Ia^WK8egLFnaV?LX z=wnJo@QR!nzf#btI%9Y}Vk3u}cnr&cDr6K3G)zQ0=am zGxt(cE4T(IcMmc%@N+mT7yse|1!LKkT)hEhVcFa#j=_VhE$u>==i`OSuiZyW>&S{F5uB+Vky$qx3cY@*YvlOb{g*7%d-hh}dX!j}@ZlDj+jn-PuetAy4$$P!oP zk*RJqHC%ytY(TW7G6WRm&BYN_hut9l%*CB6ybVFY;M92HlB(avAK@W`{1& znA6<9pJSX|#6@D1ysJYP+eJ;9nNJdpVh<*#AQVhoL48xtK8F=ik;}ZD)MzdH3T>vm zkPn$FTi*3@M<+n^G0N<>ZT>1NN-%U2jJ<6AdNcHkab9K`CE{!-R$ez2{Zn12yltyw zKGF##nFr`JC0^Hk57gnD0_&~-?w^+sf(d&FAx-?PgQL1m-*$0*k&Wry_j~RaOko+B zNO~4Cv&IK~^JRa=l8Wh*^4vxUO~(jrxON?@@11ilE+s#k$!lUk`lMeLR^(luZT=-z zu=d8g@G|TC&*R;8-om>R{I3bu~q5?w(#|Un2<+ zm!v&84c$V7;Bis)dcIqcL0N}}`F`e?zt_&?3P!liK^?@#!ix4(hqh^5IL7W_4>Z5D zJ*dgW&&a59lD^Lf;H-6Yo)|;wT2G99cIw)Dj5F`cdW_%Rq66CBzOnyWgvAdzd9ygh z`<5~~^r35S1hMSg`Q)YG0;Ca4yoP@mmU4g%fT7(COHa{tRgZ;Mc77RR+*fWo=tk7y z0zx_Q9{2&ri|+NdEP^F^t;O|g@h>zF(sq5@i~UnQ3<2eXB?tJU7iQ7LKgcbW^J$YZ zvAHzJ*Le!Gpz2WFiBkM=?_xpT7}RO>0WU|T8%e#$p;m6Ij;TG|CqCky9U!1HvR#Aw zjY@kzl)DKtcTGF+9&O>OD{L6_HeE2Uu1z(2#`8ujloW7VJsg#Mf~9Mx)XTLEkkH6OI;IXA?UXo)bQA?=`MV z?v8DXKb3wyFL}0xIi}w65!oza5|?e)HQ_deLo3bIXK$&XBiCZn^af{1|H(T5aWz@E zv$A)GW+-hd&lk*G+wVSDObgd`gO1PeoC&?U0xLzEP~B0szbu{c z;*LI6i6%w5D`7`i;J2#ZpYwZ%FCUijY9H(dp=3Yy(&;kC`{t)nN zmco;$x?N%5?YuDCT|s(!&mHTETnM6c9MAjvl;iM&o`>3?rw}qE@=3V>QU4jyAN2Z} zcsgx~1%OS>NHqU5+kc&Uw|`S36wPdX8@ed>1Rr9cp;Es_3V0J+5cq{gx5&cbFg2z2 z3BZVTVSxMZ+6dB1HLXO<94>abGUbE8I*<{!7wDOt=JAq;0~HTcTSU`m%G64xzl?K= z8Y?viy$b*mz03|y__bJXFwpEVmc{Ud!Hp0+YYxAn+xIpgkWpE1mFr9CKGu79=!MAz z3d?s`BJo58Dr^m+ILpL*obR%6uN_WEm?{z=l_h*jzzpu6Txz-sAJmD+9Bm1)$j;3$; zmq^A<%qJRocJDENc*Cya7FhgQ{_^agV0eF(@Ldib%+2bu;i1&o@j@U`}u5WXX^^n$1Mf@g8aK$vyNd2AS1ZQ3TdXK@K8gBvd7LrfD-8XhG=gFF*gyN4<1jhK++RLJ8$EP7Gy7B zhS6w>L0sEqyAg9rt1}*9=pI~yo0{EQHepZMEb+?i$1gTcoCXy$pX~*M{WEukZ|Lxd z3eIE)LNHY~p3|Sd-fT;?Bud6@#w{LL*rRjCIi9$}C)sJ6=|8j&YR&x}M3;>BwLye8 zl%#DnOZ*Np>s@s(5fNbq%5X?)>3G@yFxY)6cb=NmTf)3UM>VzgFDSmq3?pGB)xxN- zvnSJ$ypD&Q1n!u+X4zi`%gnz7K^LJN0L$C?6%YBS?hGERiR0Wt(~h{!IZ>fFL-x z70L97P%uztfhljN{jDI_9OO`ryf&oz3rLw@0kKIsq1T~a8f3+U+n#eza|9QpQwP@$ z*_VWJCB4WGR(>_2m*8|ep#1qftc>Q}u50zFi2m2y=si(2M(y^USH%KSyq7!`Q)ZT- znhSyDir9-?yOzm;*3>_C?eRH#IDM;1P|_X|UV?!OrUb>hu{YvP>6zY`^%bv$kXOF& zZL~pSHcBVb;qI=t-|{zXeXMN1KE@UoPbwJ`xOu5y0@SPc`nrM^Q40+aJ->!wz#wtD z(Yc~Uz_yR|k8u)xE!hv-^F-`^7*yQse87jGt6Lv5I!M=J!dtks$g58e=HF}9xorLY z!}0U%Tuxs0eR}GT<%7m zMJ8?8F^&03>p>$7>4bRgCIj1;_opqL_v*NABWht1d%WfW*5`Hc>4M-RlR(_0D*NGfqjwCT zlTW8Z$$li&Hs78(kDdYfT|G-FA8%oUH~2r{mMQ*aWKZ|4_irrn7aCz8Hd?Kh?^Zt{ z0NW(^T8s+>A*qG2%T)Bfxxfg8Gmy?RoBlGVtdBDPr2fK53oWpmEQC?Idh8JO(ir2o zi6Pri<|gq7H$^f{q~EBV^mPiQay`=2j`2(Rm&zEadwi|qrK%=0j9qg zmtA(1aHJph@^(eVUZA8f4D*c1FBozMKB|M-#W6cys}+kltrNdklcnxq>zQy#DyPF* z)*4@5Z}N!=W_$+)!X}9lf3>-#<5MsXoGk>zx zQi!+Q1a*v$La48VXf|^;=X&l~E)VR|SSkv>ckEsL@@;o7>)h+xqC8C#M1&@F_yf3{ z)xWq4gc%;LWo92%oGl~q*T@_&a`o4gPmIq~#bCo?R?oORu_eNAnL^-V4fS7et*x|R z%t%`MNL8_*YhBC75$Ada31la#eI%%X*3T{xA191CO%UNf>wI^SJtPegu+PoP4Tn5N zibMoI3Jk}s5^iC1P@dr)U!4BN(zo;rJPHH+dSp(5s8QSI3Rk@M6l+?B#thiOPYKhJ=c&BvQHh~yOS5kfCR$a z@E|~5$Il+!m`rgqxLPRPzYfX}+oN1|2yaF7i zpw%?x2;CcMC>Q8rZ#b5EeZTTSKn+Z&3+$HOt%??lXuxi@-RkNpf6>7zRWyd|cl7x` zX*bzMCF#FCzY&zJ+pDF)NwKigHn8wG!sa^rcPFP}Hm?QfI%=sr#PIBoTMQ2Q+oq{gyZqfDR^A4DH?;|KQlb`m2*aGM8eL!(4^7KU#H& z(Yrz~)zje^n#)7czZ0H%Y@92f>x?;eVQ-=Un|Xsm_b03ub9=NzpeiOTy_VNHL|sF# z{u#3z;1NqUCu(C`_-?HudKKC~$BDA4ersU(;gs%BFg|5o!r5uD$#>n_?PbW_!9klj zVp?`c3C<+Y>p8dFegY~-cL;ykQt1^sSF(6)^-D=sKsOJ_F2xI08?zbNp`rb8w34g) z6J59bFG~6s7nw)}tT4~70S;(v(KqJ1C;A8~D(|bY8^UBMUM>Pkdr}7arfhKDt$OGw z;frx2`y-$*N@cAU$N z{*iXq!T74;NkwPHL8myxt_&d9Ej56{IhlDjKF&x30><6_`Yw{(fS`HyG5Mx!;S2_( z<`|^;(WwL&I_moUJRnx-Q0wauVJL(mYe`hUR%u7NE!u1|xV8s$d?f?dPeKx|StQb& zkvkx2Yr@#$x~8En(h4BM#Qk0U%b;G7uH-irqM_^xJ*%dccPX#s$tSC|0Py?T?O~m8 zhkJ@oJ0K zh_nrp6D%cQrJA%Bzq?KfKM1DnWrk1yVNDYKOXu{g zC~h@t%_THZWys?HZvo!0YxQ>c$%C{Gd4;*Mk0+K?z^k`wQ84Op^&ch(`n%M68jyKk ziHmfE=*~Db>4xu_GH*NvJQ2>;k+Ap#0<74*ms`Ut#eh5N;=eNTBAr)8I0TRUNl;_S zjx1zF$!b7(|A!fe;#|~d$gDhaQ%=sE>_4nQwK&rK1P#FkD(CTZcsSR00nZo&m9l!+ zi&fv{HYMy^WIQJ|BSNvKS03b&agrq}nou_j>P^hsewAV#@|m=X?s7D-T5$Ul<84{r zHmhb_(*vr|Sk&(*I4tf_UGqr~$%xZq0jt9j-31hD0iw1LC(O%ct?leM;^g0}J7krM zgse{%k8a_SWqri>)|x$9J^c=Q30MEq(|0V4k&iKC*n!i}@(bTsFo`XLF=OiK1yWa< z8j&gjPkZ{C{Vqg3k_~$ME>&<1GNdY_R{g8jMW96g-BrEYWAfUD%7eUl0il*iG-nQ5 zIVB-*Ol;Vure(L2`HSMJK?$pSsLffq(53wH4dAm2G0XtERl|aX6ss~ZNrU@CS{lEG zP&_Cw3W@1@iJf_biGJcW&y6}>a~MehLtP`+$&jqj)GDymG|mWuMP+2?&jUW+#D+iw*PL9^HnE75RmrZ-peTr-M6`s zGxZn7Lb78Af=co$v0<2g&yFW|__g9H-CXbPK9$NBP0Y|~QH8K_Rq)N*!!mnK^7tPp z89CjnSd?-*fI1cV@CTBy3NJAa_`E&7amb03ronElc-=M7yH43zFh2(`sbIckxklzc zJ`xb0+OYN7-AivTueAU8^91FX3xytsGQR1V@+GdpCl_bI6O>}M-7haB+!u^8*H+%b z{4!6@{k1W`-5a98r$rPu%5H}*Xa7(qnEQNQ0D{?AFYl+kN7l6sw-YS8IdR%D_E}iH zbG8yjPcwfwyCBFbWcx#A$mY|&sj!2scauqB(>JVZnp)mc3-HrQ{N3q(kj|zai+pZB zBso=7_1aU=4M)=`RR7KR6R>H$bAFKZ$KeT8s8JLDTI`_$5|=r6uLX#(0rawYX+p-Y zfXo^dWG4~rUZ71f~F$#tOR#<$X)_d9CM=mQ3^q*vs*Q3VTa1@51ryg`#cwXZ2Z z8jM+Ps}Y4M4A*q>mb^rSs(pvUQXaRB*9P%FI*C^!ccT15+X#E==3)yIqc~XVm(}qT ziG^9)WY}0)lO+=t`dsS-#@(81Q3=G3bnH}r^urqu%HVpX!Z)v$gTur{K&@%F64?eQ zIVTHbyb!l%3Sp;3<@{XZU2Qx**3HTCrF9_q-OF2zv3nLKk+#88Vee=RSUC@O%4yq2 zs)FpP`E9pb=9IUJz8Ycw|LvW=QMgr9Qj)fs*RZ;x#>*bepR&ALq-XIz=32xQVX6v^ z_I-aSVp{EWMSMAJ_y%J|BD{fZvYrzq zsWE`q5PK~H6rVE;*hr+R^oP;qmA@YSI6U;>Mm1+c!QCJ8BD-pg?qn`D^qR%El~qe+ zN;+P^t2?3ITZLjyS}#x~oepPF$Sv3QFXVVw3If?x9-&-xn zFjG>n3^yXiP4>rn#CueRV;_tEI>n-*B4-2>6B8Z7`z$1{AeX(myo$^8({j_b5ble1 zK&OGFd-Har_B0+M#ski;v~P#_`z8h`fSwL~0|C?2M3E%3b~yb5aMiD*Bd?tH;8p)e zGCGOuA`z!zU`f^(OzRY;A{h-GkL7ZSTPJ+DdaruzTaZm4dF)%m} zCY`DxmdEZTN3JXtsN#M(9XL-qbQ(1!yOVRjw`1n51ppb%fqs~4po#ptw zy!_p3OlsYLmd)fF4zhdR0lX(|02;-KH4JC~%P0*0#tWX#o&G9h^&3YeQt3??Qrgfj zk~^X^ySH6&wOxJZc3fTYLt&2zcqA_F9waZsS~XKKBYOtxcPg>#|K*%UEL)!M)M2ci*J56 zf#Q1=e+`0dRT~Jj4E=JKad+Oh$ozlFRgQ6 zI9~+Ih^(5hS7vM-*s{>UB?K|j`7sKj7h1)LwH023C&gTOVtr83DJp~FnnETL_($^|m|X1;5F?QtY{IF-GP zj*jmA_v@9_K1GCluDNP%gN$(ArWHDN#`;U$SgZLK8RPgKi(Ls%JMc|Hu{8%NjsEue>27@UKiri6w4NH%r$0hry}KZc z1$wfNqL5|}2c+6s*bNa<%!!}KumywAFt8XWi58}3&+Xp%bAAce5Mxk4>ydw3MYnac z$IVXwom{&6r8P6J;Rm8m?JMhSib$|*5P{8HKJtM0X|^K({?Hrdhz6c}ce<3dGV46# z69ZS)$Rjl-3o4e>jC=?jCVy{rfrr@z**1Rkv_B$KBn#ri5Awa#0p+rz$-@0#ylWe( zVX~nKTbJ5lLklSxqWxk%dEwVY?>Z+39l7s|rU1^$@<;9q6K4#8Di(+3c1(23uh7+s5*3zgX@ z&$G{Ulb?elW|NAnGazr1`Mc z3O~Ru2(MUS|4XU1?V53PYny1iX{8=8W#M@(ZoE}3j6Z5ur zy+*RwnF1u)-X$Q1Ae4e5hgTBflgbe&E6Z zw-eC~lGyE9$@kpqn@4-#Jk5#c|Kt59g@Tfmm{q@IugAO(gfr`v;S< zdqMsc6Y5gf>&=ln1;5e;(0V3znF+Qgnh8>`1crbHNz;PqA4bbhjw6(5&E}^$2IN*n z1D9ok*Dpn&s2=X8+im`==KiemIbn-TrqzLPM$BDDs#s{`Cf6S(>ZcmCcDU!xSoT{M z775yiZ@(QDf!m|+&mSiQkUd01@yMUI^y?i_(Kt42k^U%8xDTngG1KS({|BROH@Ejc z9V^?5b7X3C)k2OK3Nyg>$;8!tCsVEad9RKV22f)8+oYON)fio}$kpl? zLSOWlk*j-hb8X4^O;3c?b)k&mXALL--F37`X3ed&GBwL5&H4D_bVVg_dc%d`NFcgl z^5K^2ZBKGKDFoRVdHc(_pWUcnltL=!e^F1A-E&p?s@>J3e>Sjd0)dkKq!A75{WIEo zi5FFev$Lk2#P3V2CJj+()P&B;VnVfF-cC8dCt0-N#z|Lz&PhH89pknkLXxIjE6V+g zRM&O3{4M+2x8D{obO{lmKYnzH84*f<KDC+2g&*XrwDe)p~Y zem4}*qU=#zLzKtS#sKLg@Qwu(C|;y|MET+ih9MAGe#@uyY)$p+^F^2wm+k<&)&4vl z;6gd4!34aJ_e8Rn8P^V2%6d0@%s6)-w)OMVE|Fj5f2Dz7≶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

OgkN>Q6asVi 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

l6RLSeG*7gVi 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 diff --git a/cmd/clef/docs/clef_architecture_pt3.png b/cmd/clef/docs/clef_architecture_pt3.png deleted file mode 100644 index b9d695447308c0fe7c0dc3018618f7350f2a806c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101351 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%0Amw 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≺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 diff --git a/cmd/clef/docs/qubes/clef_qubes_http.png b/cmd/clef/docs/qubes/clef_qubes_http.png deleted file mode 100644 index e95ad8da4af6fdd4f628ec4ea741c0b98147f00b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12237 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

>W^cs-?#mXItenZY6gYiNlBTK1l?7OkpEdc1 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 GkN+2l3G_?= diff --git a/cmd/clef/docs/qubes/qrexec-example.png b/cmd/clef/docs/qubes/qrexec-example.png deleted file mode 100644 index fc5d57725dd30f32415e543244c9c40110a31c8e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16166 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 diff --git a/cmd/clef/docs/qubes/qubes-client.py b/cmd/clef/docs/qubes/qubes-client.py deleted file mode 100644 index 93a74b899b..0000000000 --- a/cmd/clef/docs/qubes/qubes-client.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This implements a dispatcher which listens to localhost:8550, and proxies -requests via qrexec to the service qubes.EthSign on a target domain -""" - -import http.server -import socketserver,subprocess - -PORT=8550 -TARGET_DOMAIN= 'debian-work' - -class Dispatcher(http.server.BaseHTTPRequestHandler): - def do_POST(self): - post_data = self.rfile.read(int(self.headers['Content-Length'])) - p = subprocess.Popen(['/usr/bin/qrexec-client-vm',TARGET_DOMAIN,'qubes.Clefsign'],stdin=subprocess.PIPE, stdout=subprocess.PIPE) - output = p.communicate(post_data)[0] - self.wfile.write(output) - - -with socketserver.TCPServer(("",PORT), Dispatcher) as httpd: - print("Serving at port", PORT) - httpd.serve_forever() - diff --git a/cmd/clef/docs/qubes/qubes.Clefsign b/cmd/clef/docs/qubes/qubes.Clefsign deleted file mode 100644 index 9b5af7b4fe..0000000000 --- a/cmd/clef/docs/qubes/qubes.Clefsign +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -SIGNER_BIN="/home/user/tools/clef/clef" -SIGNER_CMD="/home/user/tools/gtksigner/gtkui.py -s $SIGNER_BIN" - -# Start clef if not already started -if [ ! -S /home/user/.clef/clef.ipc ]; then - $SIGNER_CMD & - sleep 1 -fi - -# Should be started by now -if [ -S /home/user/.clef/clef.ipc ]; then - # Post incoming request to HTTP channel - curl -H "Content-Type: application/json" -X POST -d @- http://localhost:8550 2>/dev/null -fi diff --git a/cmd/clef/docs/qubes/qubes_newaccount-1.png b/cmd/clef/docs/qubes/qubes_newaccount-1.png deleted file mode 100644 index 3bfc8b5b7e91330f1f773d9fef259906eab06c31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22348 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 IQcs@$AA2scU;qFB diff --git a/cmd/clef/docs/setup.md b/cmd/clef/docs/setup.md deleted file mode 100644 index 6cc7a4120d..0000000000 --- a/cmd/clef/docs/setup.md +++ /dev/null @@ -1,198 +0,0 @@ -# Setting up Clef - -This document describes how Clef can be used in a more secure manner than executing it from your everyday laptop, -in order to ensure that the keys remain safe in the event that your computer should get compromised. - -## Qubes OS - - -### Background - -The Qubes operating system is based around virtual machines (qubes), where a set of virtual machines are configured, typically for -different purposes such as: - -- personal - - Your personal email, browsing etc -- work - - Work email etc -- vault - - a VM without network access, where gpg-keys and/or keepass credentials are stored. - -A couple of dedicated virtual machines handle externalities: - -- sys-net provides networking to all other (network-enabled) machines -- sys-firewall handles firewall rules -- sys-usb handles USB devices, and can map usb-devices to certain qubes. - -The goal of this document is to describe how we can set up clef to provide secure transaction -signing from a `vault` vm, to another networked qube which runs Dapps. - -### Setup - -There are two ways that this can be achieved: integrated via Qubes or integrated via networking. - - -#### 1. Qubes Integrated - -Qubes provides a facility for inter-qubes communication via `qrexec`. A qube can request to make a cross-qube RPC request -to another qube. The OS then asks the user if the call is permitted. - -![Example](qubes/qrexec-example.png) - -A policy-file can be created to allow such interaction. On the `target` domain, a service is invoked which can read the -`stdin` from the `client` qube. - -This is how [Split GPG](https://www.qubes-os.org/doc/split-gpg/) is implemented. We can set up Clef the same way: - -##### Server - -![Clef via qrexec](qubes/clef_qubes_qrexec.png) - -On the `target` qubes, we need to define the RPC service. - -[qubes.Clefsign](qubes/qubes.Clefsign): - -```bash -#!/bin/bash - -SIGNER_BIN="/home/user/tools/clef/clef" -SIGNER_CMD="/home/user/tools/gtksigner/gtkui.py -s $SIGNER_BIN" - -# Start clef if not already started -if [ ! -S /home/user/.clef/clef.ipc ]; then - $SIGNER_CMD & - sleep 1 -fi - -# Should be started by now -if [ -S /home/user/.clef/clef.ipc ]; then - # Post incoming request to HTTP channel - curl -H "Content-Type: application/json" -X POST -d @- http://localhost:8550 2>/dev/null -fi - -``` -This RPC service is not complete (see notes about HTTP headers below), but works as a proof-of-concept. -It will forward the data received on `stdin` (forwarded by the OS) to Clef's HTTP channel. - -It would have been possible to send data directly to the `/home/user/.clef/.clef.ipc` -socket via e.g `nc -U /home/user/.clef/clef.ipc`, but the reason for sending the request -data over `HTTP` instead of `IPC` is that we want the ability to forward `HTTP` headers. - -To enable the service: - -``` bash -sudo cp qubes.Clefsign /etc/qubes-rpc/ -sudo chmod +x /etc/qubes-rpc/ qubes.Clefsign -``` - -This setup uses [gtksigner](https://github.com/holiman/gtksigner), which is a very minimal GTK-based UI that works well -with minimal requirements. - -##### Client - - -On the `client` qube, we need to create a listener which will receive the request from the Dapp, and proxy it. - - -[qubes-client.py](qubes/qubes-client.py): - -```python - -""" -This implements a dispatcher which listens to localhost:8550, and proxies -requests via qrexec to the service qubes.EthSign on a target domain -""" - -import http.server -import socketserver,subprocess - -PORT=8550 -TARGET_DOMAIN= 'debian-work' - -class Dispatcher(http.server.BaseHTTPRequestHandler): - def do_POST(self): - post_data = self.rfile.read(int(self.headers['Content-Length'])) - p = subprocess.Popen(['/usr/bin/qrexec-client-vm',TARGET_DOMAIN,'qubes.Clefsign'],stdin=subprocess.PIPE, stdout=subprocess.PIPE) - output = p.communicate(post_data)[0] - self.wfile.write(output) - - -with socketserver.TCPServer(("",PORT), Dispatcher) as httpd: - print("Serving at port", PORT) - httpd.serve_forever() - - -``` - -#### Testing - -To test the flow, if we have set up `debian-work` as the `target`, we can do - -```bash -$ cat newaccnt.json -{ "id": 0, "jsonrpc": "2.0","method": "account_new","params": []} - -$ cat newaccnt.json| qrexec-client-vm debian-work qubes.Clefsign -``` - -A dialog should pop up first to allow the IPC call: - -![one](qubes/qubes_newaccount-1.png) - -Followed by a GTK-dialog to approve the operation: - -![two](qubes/qubes_newaccount-2.png) - -To test the full flow, we use the client wrapper. Start it on the `client` qube: -``` -[user@work qubes]$ python3 qubes-client.py -``` - -Make the request over http (`client` qube): -``` -[user@work clef]$ cat newaccnt.json | curl -X POST -d @- http://localhost:8550 -``` -And it should show the same popups again. - -##### Pros and cons - -The benefits of this setup are: - -- This is the qubes-os intended model for inter-qube communication, -- and thus benefits from qubes-os dialogs and policies for user approval - -However, it comes with a couple of drawbacks: - -- The `qubes-gpg-client` must forward the http request via RPC to the `target` qube. When doing so, the proxy - will either drop important headers, or replace them. - - The `Host` header is most likely `localhost` - - The `Origin` header must be forwarded - - Information about the remote ip must be added as a `X-Forwarded-For`. However, Clef cannot always trust an `XFF` header, - since malicious clients may lie about `XFF` in order to fool the http server into believing it comes from another address. -- Even with a policy in place to allow RPC calls between `caller` and `target`, there will be several popups: - - One qubes-specific where the user specifies the `target` vm - - One clef-specific to approve the transaction - - -#### 2. Network integrated - -The second way to set up Clef on a qubes system is to allow networking, and have Clef listen to a port which is accessible -from other qubes. - -![Clef via http](qubes/clef_qubes_http.png) - - - - -## USBArmory - -The [USB armory](https://inversepath.com/usbarmory) is an open source hardware design with an 800 MHz ARM processor. It is a pocket-size -computer. When inserted into a laptop, it identifies itself as a USB network interface, basically adding another network -to your computer. Over this new network interface, you can SSH into the device. - -Running Clef off a USB armory means that you can use the armory as a very versatile offline computer, which only -ever connects to a local network between your computer and the device itself. - -Needless to say, while this model should be fairly secure against remote attacks, an attacker with physical access -to the USB Armory would trivially be able to extract the contents of the device filesystem. - diff --git a/cmd/clef/extapi_changelog.md b/cmd/clef/extapi_changelog.md deleted file mode 100644 index be84e88c52..0000000000 --- a/cmd/clef/extapi_changelog.md +++ /dev/null @@ -1,104 +0,0 @@ -## Changelog for external API - -The API uses [semantic versioning](https://semver.org/). - -TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the: - -* MAJOR version when you make incompatible API changes, -* MINOR version when you add functionality in a backwards-compatible manner, and -* PATCH version when you make backwards-compatible bug fixes. - -Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. - -### 6.1.0 - -The API-method `account_signGnosisSafeTx` was added. This method takes two parameters, -`[address, safeTx]`. The latter, `safeTx`, can be copy-pasted from the gnosis relay. For example: - -``` -{ - "jsonrpc": "2.0", - "method": "account_signGnosisSafeTx", - "params": ["0xfd1c4226bfD1c436672092F4eCbfC270145b7256", - { - "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", - "to": "0xB372a646f7F05Cc1785018dBDA7EBc734a2A20E2", - "value": "20000000000000000", - "data": null, - "operation": 0, - "gasToken": "0x0000000000000000000000000000000000000000", - "safeTxGas": 27845, - "baseGas": 0, - "gasPrice": "0", - "refundReceiver": "0x0000000000000000000000000000000000000000", - "nonce": 2, - "executionDate": null, - "submissionDate": "2020-09-15T21:54:49.617634Z", - "modified": "2020-09-15T21:54:49.617634Z", - "blockNumber": null, - "transactionHash": null, - "safeTxHash": "0x2edfbd5bc113ff18c0631595db32eb17182872d88d9bf8ee4d8c2dd5db6d95e2", - "executor": null, - "isExecuted": false, - "isSuccessful": null, - "ethGasPrice": null, - "gasUsed": null, - "fee": null, - "origin": null, - "dataDecoded": null, - "confirmationsRequired": null, - "confirmations": [ - { - "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", - "submissionDate": "2020-09-15T21:54:49.663299Z", - "transactionHash": null, - "confirmationType": "CONFIRMATION", - "signature": "0x95a7250bb645f831c86defc847350e7faff815b2fb586282568e96cc859e39315876db20a2eed5f7a0412906ec5ab57652a6f645ad4833f345bda059b9da2b821c", - "signatureType": "EOA" - } - ], - "signatures": null - } - ], - "id": 67 -} -``` - -Not all fields are required, though. This method is really just a UX helper, which massages the -input to conform to the `EIP-712` [specification](https://docs.safe.global/core-api/transaction-service-reference/gnosis) -for the Gnosis Safe, and making the output be directly importable to by a relay service. - - -### 6.0.0 - -* `New` was changed to deliver only an address, not the full `Account` data -* `Export` was moved from External API to the UI Server API - -#### 5.0.0 - -* The external `account_EcRecover`-method was reimplemented. -* The external method `account_sign(address, data)` was replaced with `account_signData(contentType, address, data)`. -The addition of `contentType` makes it possible to use the method for different types of objects, such as: - * signing data with an intended validator (not yet implemented) - * signing clique headers, - * signing plain personal messages, -* The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data. - -#### 4.0.0 - -* The external `account_Ecrecover`-method was removed. -* The external `account_Import`-method was removed. - -#### 3.0.0 - -* The external `account_List`-method was changed to not expose `url`, which contained info about the local filesystem. It now returns only a list of addresses. - -#### 2.0.0 - -* Commit `73abaf04b1372fa4c43201fb1b8019fe6b0a6f8d`, move `from` into `transaction` object in `signTransaction`. This -makes the `accounts_signTransaction` identical to the old `eth_signTransaction`. - - -#### 1.0.0 - -Initial release. diff --git a/cmd/clef/intapi_changelog.md b/cmd/clef/intapi_changelog.md deleted file mode 100644 index 85d04f6d0e..0000000000 --- a/cmd/clef/intapi_changelog.md +++ /dev/null @@ -1,191 +0,0 @@ -## Changelog for internal API (ui-api) - -The API uses [semantic versioning](https://semver.org/). - -TL;DR: Given a version number MAJOR.MINOR.PATCH, increment the: - -* MAJOR version when you make incompatible API changes, -* MINOR version when you add functionality in a backwards-compatible manner, and -* PATCH version when you make backwards-compatible bug fixes. - -Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. - -### 7.0.1 - -Added `clef_New` to the internal API callable from a UI. - -> `New` creates a new password protected Account. The private key is protected with -> the given password. Users are responsible to backup the private key that is stored -> in the keystore location that was specified when this API was created. -> This method is the same as New on the external API, the difference being that -> this implementation does not ask for confirmation, since it's initiated by -> the user - -### 7.0.0 - -- The `message` field was renamed to `messages` in all data signing request methods to better reflect that it's a list, not a value. -- The `storage.Put` and `storage.Get` methods in the rule execution engine were lower-cased to `storage.put` and `storage.get` to be consistent with JavaScript call conventions. - -### 6.0.0 - -Removed `password` from responses to operations which require them. This is for two reasons, - -- Consistency between how rulesets operate and how manual processing works. A rule can `Approve` but require the actual password to be stored in the clef storage. -With this change, the same stored password can be used even if rulesets are not enabled, but storage is. -- It also removes the usability-shortcut that a UI might otherwise want to implement; remembering passwords. Since we now will not require the -password on every `Approve`, there's no need for the UI to cache it locally. - - In a future update, we'll likely add `clef_storePassword` to the internal API, so the user can store it via his UI (currently only CLI works). - -Affected datatypes: -- `SignTxResponse` -- `SignDataResponse` -- `NewAccountResponse` - -If `clef` requires a password, the `OnInputRequired` will be used to collect it. - - -### 5.0.0 - -Changed the namespace format to adhere to the legacy ethereum format: `name_methodName`. Changes: - -* `ApproveTx` -> `ui_approveTx` -* `ApproveSignData` -> `ui_approveSignData` -* `ApproveExport` -> `removed` -* `ApproveImport` -> `removed` -* `ApproveListing` -> `ui_approveListing` -* `ApproveNewAccount` -> `ui_approveNewAccount` -* `ShowError` -> `ui_showError` -* `ShowInfo` -> `ui_showInfo` -* `OnApprovedTx` -> `ui_onApprovedTx` -* `OnSignerStartup` -> `ui_onSignerStartup` -* `OnInputRequired` -> `ui_onInputRequired` - - -### 4.0.0 - -* Bidirectional communication implemented, so the UI can query `clef` via the stdin/stdout RPC channel. Methods implemented are: - - `clef_listWallets` - - `clef_listAccounts` - - `clef_listWallets` - - `clef_deriveAccount` - - `clef_importRawKey` - - `clef_openWallet` - - `clef_chainId` - - `clef_setChainId` - - `clef_export` - - `clef_import` - -* The type `Account` was modified (the json-field `type` was removed), to consist of - -```go -type Account struct { - Address common.Address `json:"address"` // Ethereum account address derived from the key - URL URL `json:"url"` // Optional resource locator within a backend -} -``` - - -### 3.2.0 - -* Make `ShowError`, `OnApprovedTx`, `OnSignerStartup` be json-rpc [notifications](https://www.jsonrpc.org/specification#notification): - -> A Notification is a Request object without an "id" member. A Request object that is a Notification signifies the Client's lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request. -> -> Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params","Internal error" -### 3.1.0 - -* Add `ContentType` `string` to `SignDataRequest` to accommodate the latest [EIP-191](https://eips.ethereum.org/EIPS/eip-191) and [EIP-712](https://eips.ethereum.org/EIPS/eip-712) implementations. - -### 3.0.0 - -* Make use of `OnInputRequired(info UserInputRequest)` for obtaining master password during startup - -### 2.1.0 - -* Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords. - -The following structures are used: - -```go -UserInputRequest struct { - Prompt string `json:"prompt"` - Title string `json:"title"` - IsPassword bool `json:"isPassword"` -} -UserInputResponse struct { - Text string `json:"text"` -} -``` - -### 2.0.0 - -* Modify how `call_info` on a transaction is conveyed. New format: - -``` -{ - "jsonrpc": "2.0", - "id": 2, - "method": "ApproveTx", - "params": [ - { - "transaction": { - "from": "0x82A2A876D39022B3019932D30Cd9c97ad5616813", - "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", - "gas": "0x333", - "gasPrice": "0x123", - "value": "0x10", - "nonce": "0x0", - "data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012", - "input": null - }, - "call_info": [ - { - "type": "WARNING", - "message": "Invalid checksum on to-address" - }, - { - "type": "WARNING", - "message": "Tx contains data, but provided ABI signature could not be matched: Did not match: test (0 matches)" - } - ], - "meta": { - "remote": "127.0.0.1:54286", - "local": "localhost:8550", - "scheme": "HTTP/1.1" - } - } - ] -} -``` - -#### 1.2.0 - -* Add `OnStartup` method, to provide the UI with information about what API version -the signer uses (both internal and external) as well as build-info and external api. - -Example call: -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "OnSignerStartup", - "params": [ - { - "info": { - "extapi_http": "http://localhost:8550", - "extapi_ipc": null, - "extapi_version": "2.0.0", - "intapi_version": "1.2.0" - } - } - ] -} -``` - -#### 1.1.0 - -* Add `OnApproved` method - -#### 1.0.0 - -Initial release. diff --git a/cmd/clef/main.go b/cmd/clef/main.go deleted file mode 100644 index 2b54bb14cb..0000000000 --- a/cmd/clef/main.go +++ /dev/null @@ -1,1222 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "bufio" - "context" - "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "math/big" - "net" - "os" - "os/signal" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/internal/flags" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/signer/core" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/ethereum/go-ethereum/signer/fourbyte" - "github.com/ethereum/go-ethereum/signer/rules" - "github.com/ethereum/go-ethereum/signer/storage" - "github.com/mattn/go-colorable" - "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" -) - -const legalWarning = ` -WARNING! - -Clef is an account management tool. It may, like any software, contain bugs. - -Please take care to -- backup your keystore files, -- verify that the keystore(s) can be opened with your password. - -Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -PURPOSE. See the GNU General Public License for more details. -` - -var ( - logLevelFlag = &cli.IntFlag{ - Name: "loglevel", - Value: 3, - Usage: "log level to emit to the screen", - } - advancedMode = &cli.BoolFlag{ - Name: "advanced", - Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", - } - acceptFlag = &cli.BoolFlag{ - Name: "suppress-bootwarn", - Usage: "If set, does not show the warning during boot", - } - keystoreFlag = &cli.StringFlag{ - Name: "keystore", - Value: filepath.Join(node.DefaultDataDir(), "keystore"), - Usage: "Directory for the keystore", - } - configdirFlag = &cli.StringFlag{ - Name: "configdir", - Value: DefaultConfigDir(), - Usage: "Directory for Clef configuration", - } - chainIdFlag = &cli.Int64Flag{ - Name: "chainid", - Value: params.MainnetChainConfig.ChainID.Int64(), - Usage: "Chain id to use for signing (1=mainnet, 17000=Holesky)", - } - rpcPortFlag = &cli.IntFlag{ - Name: "http.port", - Usage: "HTTP-RPC server listening port", - Value: node.DefaultHTTPPort + 5, - Category: flags.APICategory, - } - signerSecretFlag = &cli.StringFlag{ - Name: "signersecret", - Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash", - } - customDBFlag = &cli.StringFlag{ - Name: "4bytedb-custom", - Usage: "File used for writing new 4byte-identifiers submitted via API", - Value: "./4byte-custom.json", - } - auditLogFlag = &cli.StringFlag{ - Name: "auditlog", - Usage: "File used to emit audit logs. Set to \"\" to disable", - Value: "audit.log", - } - ruleFlag = &cli.StringFlag{ - Name: "rules", - Usage: "Path to the rule file to auto-authorize requests with", - } - stdiouiFlag = &cli.BoolFlag{ - Name: "stdio-ui", - Usage: "Use STDIN/STDOUT as a channel for an external UI. " + - "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + - "interface, and can be used when Clef is started by an external process.", - } - testFlag = &cli.BoolFlag{ - Name: "stdio-ui-test", - Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", - } - initCommand = &cli.Command{ - Action: initializeSecrets, - Name: "init", - Usage: "Initialize the signer, generate secret storage", - ArgsUsage: "", - Flags: []cli.Flag{ - logLevelFlag, - configdirFlag, - }, - Description: ` -The init command generates a master seed which Clef can use to store credentials and data needed for -the rule-engine to work.`, - } - attestCommand = &cli.Command{ - Action: attestFile, - Name: "attest", - Usage: "Attest that a js-file is to be used", - ArgsUsage: "", - Flags: []cli.Flag{ - logLevelFlag, - configdirFlag, - signerSecretFlag, - }, - Description: ` -The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of -incoming requests. - -Whenever you make an edit to the rule file, you need to use attestation to tell -Clef that the file is 'safe' to execute.`, - } - setCredentialCommand = &cli.Command{ - Action: setCredential, - Name: "setpw", - Usage: "Store a credential for a keystore file", - ArgsUsage: "

", - Flags: []cli.Flag{ - logLevelFlag, - configdirFlag, - signerSecretFlag, - }, - Description: ` -The setpw command stores a password for a given address (keyfile). -`} - delCredentialCommand = &cli.Command{ - Action: removeCredential, - Name: "delpw", - Usage: "Remove a credential for a keystore file", - ArgsUsage: "
", - Flags: []cli.Flag{ - logLevelFlag, - configdirFlag, - signerSecretFlag, - }, - Description: ` -The delpw command removes a password for a given address (keyfile). -`} - newAccountCommand = &cli.Command{ - Action: newAccount, - Name: "newaccount", - Usage: "Create a new account", - ArgsUsage: "", - Flags: []cli.Flag{ - logLevelFlag, - keystoreFlag, - utils.LightKDFFlag, - acceptFlag, - }, - Description: ` -The newaccount command creates a new keystore-backed account. It is a convenience-method -which can be used in lieu of an external UI. -`} - gendocCommand = &cli.Command{ - Action: GenDoc, - Name: "gendoc", - Usage: "Generate documentation about json-rpc format", - Description: ` -The gendoc generates example structures of the json-rpc communication types. -`} - listAccountsCommand = &cli.Command{ - Action: listAccounts, - Name: "list-accounts", - Usage: "List accounts in the keystore", - Flags: []cli.Flag{ - logLevelFlag, - keystoreFlag, - utils.LightKDFFlag, - acceptFlag, - }, - Description: ` - Lists the accounts in the keystore. - `} - listWalletsCommand = &cli.Command{ - Action: listWallets, - Name: "list-wallets", - Usage: "List wallets known to Clef", - Flags: []cli.Flag{ - logLevelFlag, - keystoreFlag, - utils.LightKDFFlag, - acceptFlag, - }, - Description: ` - Lists the wallets known to Clef. - `} - importRawCommand = &cli.Command{ - Action: accountImport, - Name: "importraw", - Usage: "Import a hex-encoded private key.", - ArgsUsage: "", - Flags: []cli.Flag{ - logLevelFlag, - keystoreFlag, - utils.LightKDFFlag, - acceptFlag, - }, - Description: ` -Imports an unencrypted private key from and creates a new account. -Prints the address. -The keyfile is assumed to contain an unencrypted private key in hexadecimal format. -The account is saved in encrypted format, you are prompted for a password. -`} -) - -var app = flags.NewApp("Manage Ethereum account operations") - -func init() { - app.Name = "Clef" - app.Flags = []cli.Flag{ - logLevelFlag, - keystoreFlag, - configdirFlag, - chainIdFlag, - utils.LightKDFFlag, - utils.SmartCardDaemonPathFlag, - utils.HTTPListenAddrFlag, - utils.HTTPVirtualHostsFlag, - utils.IPCDisabledFlag, - utils.IPCPathFlag, - utils.HTTPEnabledFlag, - rpcPortFlag, - signerSecretFlag, - customDBFlag, - auditLogFlag, - ruleFlag, - stdiouiFlag, - testFlag, - advancedMode, - acceptFlag, - } - app.Action = signer - app.Commands = []*cli.Command{initCommand, - attestCommand, - setCredentialCommand, - delCredentialCommand, - newAccountCommand, - importRawCommand, - gendocCommand, - listAccountsCommand, - listWalletsCommand, - } -} - -func main() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} - -func initializeSecrets(c *cli.Context) error { - // Get past the legal message - if err := initialize(c); err != nil { - return err - } - // Ensure the master key does not yet exist, we're not willing to overwrite - configDir := c.String(configdirFlag.Name) - if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { - return err - } - location := filepath.Join(configDir, "masterseed.json") - if _, err := os.Stat(location); err == nil { - return fmt.Errorf("master key %v already exists, will not overwrite", location) - } - // Key file does not exist yet, generate a new one and encrypt it - masterSeed := make([]byte, 256) - num, err := io.ReadFull(rand.Reader, masterSeed) - if err != nil { - return err - } - if num != len(masterSeed) { - return errors.New("failed to read enough random") - } - n, p := keystore.StandardScryptN, keystore.StandardScryptP - if c.Bool(utils.LightKDFFlag.Name) { - n, p = keystore.LightScryptN, keystore.LightScryptP - } - text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!" - var password string - for { - password = utils.GetPassPhrase(text, true) - if err := core.ValidatePasswordFormat(password); err != nil { - fmt.Printf("invalid password: %v\n", err) - } else { - fmt.Println() - break - } - } - cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p) - if err != nil { - return fmt.Errorf("failed to encrypt master seed: %v", err) - } - // Double check the master key path to ensure nothing wrote there in between - if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { - return err - } - if _, err := os.Stat(location); err == nil { - return fmt.Errorf("master key %v already exists, will not overwrite", location) - } - // Write the file and print the usual warning message - if err = os.WriteFile(location, cipherSeed, 0400); err != nil { - return err - } - fmt.Printf("A master seed has been generated into %s\n", location) - fmt.Printf(` -This is required to be able to store credentials, such as: -* Passwords for keystores (used by rule engine) -* Storage for JavaScript auto-signing rules -* Hash of JavaScript rule-file - -You should treat 'masterseed.json' with utmost secrecy and make a backup of it! -* The password is necessary but not enough, you need to back up the master seed too! -* The master seed does not contain your accounts, those need to be backed up separately! - -`) - return nil -} - -func attestFile(ctx *cli.Context) error { - if ctx.NArg() < 1 { - utils.Fatalf("This command requires an argument.") - } - if err := initialize(ctx); err != nil { - return err - } - - stretchedKey, err := readMasterKey(ctx, nil) - if err != nil { - utils.Fatalf(err.Error()) - } - configDir := ctx.String(configdirFlag.Name) - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) - confKey := crypto.Keccak256([]byte("config"), stretchedKey) - - // Initialize the encrypted storages - configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) - val := ctx.Args().First() - configStorage.Put("ruleset_sha256", val) - log.Info("Ruleset attestation updated", "sha256", val) - return nil -} - -func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error) { - if err := initialize(c); err != nil { - return nil, nil, err - } - var ( - ui = core.NewCommandlineUI() - pwStorage storage.Storage = &storage.NoStorage{} - ksLoc = c.String(keystoreFlag.Name) - lightKdf = c.Bool(utils.LightKDFFlag.Name) - ) - am := core.StartClefAccountManager(ksLoc, lightKdf, "") - api := core.NewSignerAPI(am, 0, ui, nil, false, pwStorage) - internalApi := core.NewUIServerAPI(api) - return internalApi, ui, nil -} - -func setCredential(ctx *cli.Context) error { - if ctx.NArg() < 1 { - utils.Fatalf("This command requires an address to be passed as an argument") - } - if err := initialize(ctx); err != nil { - return err - } - addr := ctx.Args().First() - if !common.IsHexAddress(addr) { - utils.Fatalf("Invalid address specified: %s", addr) - } - address := common.HexToAddress(addr) - password := utils.GetPassPhrase("Please enter a password to store for this address:", true) - fmt.Println() - - stretchedKey, err := readMasterKey(ctx, nil) - if err != nil { - utils.Fatalf(err.Error()) - } - configDir := ctx.String(configdirFlag.Name) - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) - pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) - - pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) - pwStorage.Put(address.Hex(), password) - - log.Info("Credential store updated", "set", address) - return nil -} - -func removeCredential(ctx *cli.Context) error { - if ctx.NArg() < 1 { - utils.Fatalf("This command requires an address to be passed as an argument") - } - if err := initialize(ctx); err != nil { - return err - } - addr := ctx.Args().First() - if !common.IsHexAddress(addr) { - utils.Fatalf("Invalid address specified: %s", addr) - } - address := common.HexToAddress(addr) - - stretchedKey, err := readMasterKey(ctx, nil) - if err != nil { - utils.Fatalf(err.Error()) - } - configDir := ctx.String(configdirFlag.Name) - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) - pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) - - pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) - pwStorage.Del(address.Hex()) - - log.Info("Credential store updated", "unset", address) - return nil -} - -func initialize(c *cli.Context) error { - // Set up the logger to print everything - logOutput := os.Stdout - if c.Bool(stdiouiFlag.Name) { - logOutput = os.Stderr - // If using the stdioui, we can't do the 'confirm'-flow - if !c.Bool(acceptFlag.Name) { - fmt.Fprint(logOutput, legalWarning) - } - } else if !c.Bool(acceptFlag.Name) { - if !confirm(legalWarning) { - return errors.New("aborted by user") - } - fmt.Println() - } - usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" - output := io.Writer(logOutput) - if usecolor { - output = colorable.NewColorable(logOutput) - } - verbosity := log.FromLegacyLevel(c.Int(logLevelFlag.Name)) - log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor))) - - return nil -} - -func newAccount(c *cli.Context) error { - internalApi, _, err := initInternalApi(c) - if err != nil { - return err - } - addr, err := internalApi.New(context.Background()) - if err == nil { - fmt.Printf("Generated account %v\n", addr.String()) - } - return err -} - -func listAccounts(c *cli.Context) error { - internalApi, _, err := initInternalApi(c) - if err != nil { - return err - } - accs, err := internalApi.ListAccounts(context.Background()) - if err != nil { - return err - } - if len(accs) == 0 { - fmt.Println("\nThe keystore is empty.") - } - fmt.Println() - for _, account := range accs { - fmt.Printf("%v (%v)\n", account.Address, account.URL) - } - return err -} - -func listWallets(c *cli.Context) error { - internalApi, _, err := initInternalApi(c) - if err != nil { - return err - } - wallets := internalApi.ListWallets() - if len(wallets) == 0 { - fmt.Println("\nThere are no wallets.") - } - fmt.Println() - for i, wallet := range wallets { - fmt.Printf("- Wallet %d at %v (%v %v)\n", i, wallet.URL, wallet.Status, wallet.Failure) - for j, acc := range wallet.Accounts { - fmt.Printf(" -Account %d: %v (%v)\n", j, acc.Address, acc.URL) - } - fmt.Println() - } - return nil -} - -// accountImport imports a raw hexadecimal private key via CLI. -func accountImport(c *cli.Context) error { - if c.Args().Len() != 1 { - return errors.New(" must be given as first argument") - } - internalApi, ui, err := initInternalApi(c) - if err != nil { - return err - } - pKey, err := crypto.LoadECDSA(c.Args().First()) - if err != nil { - return err - } - readPw := func(prompt string) (string, error) { - resp, err := ui.OnInputRequired(core.UserInputRequest{ - Title: "Password", - Prompt: prompt, - IsPassword: true, - }) - if err != nil { - return "", err - } - return resp.Text, nil - } - first, err := readPw("Please enter a password for the imported account") - if err != nil { - return err - } - second, err := readPw("Please repeat the password you just entered") - if err != nil { - return err - } - if first != second { - //lint:ignore ST1005 This is a message for the user - return errors.New("Passwords do not match") - } - acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first) - if err != nil { - return err - } - ui.ShowInfo(fmt.Sprintf(`Key imported: - Address %v - Keystore file: %v - -The key is now encrypted; losing the password will result in permanently losing -access to the key and all associated funds! - -Make sure to backup keystore and passwords in a safe location.`, - acc.Address, acc.URL.Path)) - return nil -} - -// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into -// account the set data folders as well as the designated platform we're currently -// running on. -func ipcEndpoint(ipcPath, datadir string) string { - // On windows we can only use plain top-level pipes - if runtime.GOOS == "windows" { - if strings.HasPrefix(ipcPath, `\\.\pipe\`) { - return ipcPath - } - return `\\.\pipe\` + ipcPath - } - // Resolve names into the data directory full paths otherwise - if filepath.Base(ipcPath) == ipcPath { - if datadir == "" { - return filepath.Join(os.TempDir(), ipcPath) - } - return filepath.Join(datadir, ipcPath) - } - return ipcPath -} - -func signer(c *cli.Context) error { - // If we have some unrecognized command, bail out - if c.NArg() > 0 { - return fmt.Errorf("invalid command: %q", c.Args().First()) - } - if err := initialize(c); err != nil { - return err - } - var ( - ui core.UIClientAPI - ) - if c.Bool(stdiouiFlag.Name) { - log.Info("Using stdin/stdout as UI-channel") - ui = core.NewStdIOUI() - } else { - log.Info("Using CLI as UI-channel") - ui = core.NewCommandlineUI() - } - // 4bytedb data - fourByteLocal := c.String(customDBFlag.Name) - db, err := fourbyte.NewWithFile(fourByteLocal) - if err != nil { - utils.Fatalf(err.Error()) - } - embeds, locals := db.Size() - log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal) - - var ( - api core.ExternalAPI - pwStorage storage.Storage = &storage.NoStorage{} - ) - configDir := c.String(configdirFlag.Name) - if stretchedKey, err := readMasterKey(c, ui); err != nil { - log.Warn("Failed to open master, rules disabled", "err", err) - } else { - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) - - // Generate domain specific keys - pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) - jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) - confkey := crypto.Keccak256([]byte("config"), stretchedKey) - - // Initialize the encrypted storages - pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) - jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) - configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) - - // Do we have a rule-file? - if ruleFile := c.String(ruleFlag.Name); ruleFile != "" { - ruleJS, err := os.ReadFile(ruleFile) - if err != nil { - log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) - } else { - shasum := sha256.Sum256(ruleJS) - foundShaSum := hex.EncodeToString(shasum[:]) - storedShasum, _ := configStorage.Get("ruleset_sha256") - if storedShasum != foundShaSum { - log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) - } else { - // Initialize rules - ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) - if err != nil { - utils.Fatalf(err.Error()) - } - ruleEngine.Init(string(ruleJS)) - ui = ruleEngine - log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) - } - } - } - } - var ( - chainId = c.Int64(chainIdFlag.Name) - ksLoc = c.String(keystoreFlag.Name) - lightKdf = c.Bool(utils.LightKDFFlag.Name) - advanced = c.Bool(advancedMode.Name) - scpath = c.String(utils.SmartCardDaemonPathFlag.Name) - ) - log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, - "light-kdf", lightKdf, "advanced", advanced) - am := core.StartClefAccountManager(ksLoc, lightKdf, scpath) - defer am.Close() - apiImpl := core.NewSignerAPI(am, chainId, ui, db, advanced, pwStorage) - - // Establish the bidirectional communication, by creating a new UI backend and registering - // it with the UI. - ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) - api = apiImpl - - // Audit logging - if logfile := c.String(auditLogFlag.Name); logfile != "" { - api, err = core.NewAuditLogger(logfile, api) - if err != nil { - utils.Fatalf(err.Error()) - } - log.Info("Audit logs configured", "file", logfile) - } - // register signer API with server - var ( - extapiURL = "n/a" - ipcapiURL = "n/a" - ) - rpcAPI := []rpc.API{ - { - Namespace: "account", - Service: api, - }, - } - if c.Bool(utils.HTTPEnabledFlag.Name) { - vhosts := utils.SplitAndTrim(c.String(utils.HTTPVirtualHostsFlag.Name)) - cors := utils.SplitAndTrim(c.String(utils.HTTPCORSDomainFlag.Name)) - - srv := rpc.NewServer() - srv.SetBatchLimits(node.DefaultConfig.BatchRequestLimit, node.DefaultConfig.BatchResponseMaxSize) - err := node.RegisterApis(rpcAPI, []string{"account"}, srv) - if err != nil { - utils.Fatalf("Could not register API: %w", err) - } - handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil, false) - - // set port - port := c.Int(rpcPortFlag.Name) - - // start http server - httpEndpoint := net.JoinHostPort(c.String(utils.HTTPListenAddrFlag.Name), fmt.Sprintf("%d", port)) - httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler) - if err != nil { - utils.Fatalf("Could not start RPC api: %v", err) - } - extapiURL = fmt.Sprintf("http://%v/", addr) - log.Info("HTTP endpoint opened", "url", extapiURL) - - defer func() { - // Don't bother imposing a timeout here. - httpServer.Shutdown(context.Background()) - log.Info("HTTP endpoint closed", "url", extapiURL) - }() - } - if !c.Bool(utils.IPCDisabledFlag.Name) { - givenPath := c.String(utils.IPCPathFlag.Name) - ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir) - listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) - if err != nil { - utils.Fatalf("Could not start IPC api: %v", err) - } - log.Info("IPC endpoint opened", "url", ipcapiURL) - defer func() { - listener.Close() - log.Info("IPC endpoint closed", "url", ipcapiURL) - }() - } - if c.Bool(testFlag.Name) { - log.Info("Performing UI test") - go testExternalUI(apiImpl) - } - ui.OnSignerStartup(core.StartupInfo{ - Info: map[string]interface{}{ - "intapi_version": core.InternalAPIVersion, - "extapi_version": core.ExternalAPIVersion, - "extapi_http": extapiURL, - "extapi_ipc": ipcapiURL, - }}) - - abortChan := make(chan os.Signal, 1) - signal.Notify(abortChan, os.Interrupt) - - sig := <-abortChan - log.Info("Exiting...", "signal", sig) - - return nil -} - -// DefaultConfigDir is the default config directory to use for the vaults and other -// persistence requirements. -func DefaultConfigDir() string { - // Try to place the data folder in the user's home dir - home := flags.HomeDir() - if home != "" { - if runtime.GOOS == "darwin" { - return filepath.Join(home, "Library", "Signer") - } else if runtime.GOOS == "windows" { - appdata := os.Getenv("APPDATA") - if appdata != "" { - return filepath.Join(appdata, "Signer") - } - return filepath.Join(home, "AppData", "Roaming", "Signer") - } - return filepath.Join(home, ".clef") - } - // As we cannot guess a stable location, return empty and handle later - return "" -} - -func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { - var ( - file string - configDir = ctx.String(configdirFlag.Name) - ) - if ctx.IsSet(signerSecretFlag.Name) { - file = ctx.String(signerSecretFlag.Name) - } else { - file = filepath.Join(configDir, "masterseed.json") - } - if err := checkFile(file); err != nil { - return nil, err - } - cipherKey, err := os.ReadFile(file) - if err != nil { - return nil, err - } - var password string - // If ui is not nil, get the password from ui. - if ui != nil { - resp, err := ui.OnInputRequired(core.UserInputRequest{ - Title: "Master Password", - Prompt: "Please enter the password to decrypt the master seed", - IsPassword: true}) - if err != nil { - return nil, err - } - password = resp.Text - } else { - password = utils.GetPassPhrase("Decrypt master seed of clef", false) - } - masterSeed, err := decryptSeed(cipherKey, password) - if err != nil { - return nil, errors.New("failed to decrypt the master seed of clef") - } - if len(masterSeed) < 256 { - return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) - } - // Create vault location - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) - err = os.Mkdir(vaultLocation, 0700) - if err != nil && !os.IsExist(err) { - return nil, err - } - return masterSeed, nil -} - -// checkFile is a convenience function to check if a file -// * exists -// * is mode 0400 (unix only) -func checkFile(filename string) error { - info, err := os.Stat(filename) - if err != nil { - return fmt.Errorf("failed stat on %s: %v", filename, err) - } - // Check the unix permission bits - // However, on windows, we cannot use the unix perm-bits, see - // https://github.com/ethereum/go-ethereum/issues/20123 - if runtime.GOOS != "windows" && info.Mode().Perm()&0377 != 0 { - return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) - } - return nil -} - -// confirm displays a text and asks for user confirmation -func confirm(text string) bool { - fmt.Print(text) - fmt.Printf("\nEnter 'ok' to proceed:\n> ") - - text, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - log.Crit("Failed to read user input", "err", err) - } - if text := strings.TrimSpace(text); text == "ok" { - return true - } - return false -} - -func testExternalUI(api *core.SignerAPI) { - ctx := context.WithValue(context.Background(), "remote", "clef binary") - ctx = context.WithValue(ctx, "scheme", "in-proc") - ctx = context.WithValue(ctx, "local", "main") - errs := make([]string, 0) - - a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") - addErr := func(errStr string) { - log.Info("Test error", "err", errStr) - errs = append(errs, errStr) - } - - queryUser := func(q string) string { - resp, err := api.UI.OnInputRequired(core.UserInputRequest{ - Title: "Testing", - Prompt: q, - }) - if err != nil { - addErr(err.Error()) - } - return resp.Text - } - expectResponse := func(testcase, question, expect string) { - if got := queryUser(question); got != expect { - addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) - } - } - expectApprove := func(testcase string, err error) { - if err == nil || err == accounts.ErrUnknownAccount { - return - } - addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) - } - expectDeny := func(testcase string, err error) { - if err == nil || err != core.ErrRequestDenied { - addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) - } - } - var delay = 1 * time.Second - // Test display of info and error - { - api.UI.ShowInfo("If you see this message, enter 'yes' to next question") - time.Sleep(delay) - expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") - api.UI.ShowError("If you see this message, enter 'yes' to the next question") - time.Sleep(delay) - expectResponse("showerror", "Did you see the message? [yes/no]", "yes") - } - { // Sign data test - clique header - api.UI.ShowInfo("Please approve the next request for signing a clique header") - time.Sleep(delay) - cliqueHeader := types.Header{ - ParentHash: common.HexToHash("0000H45H"), - UncleHash: common.HexToHash("0000H45H"), - Coinbase: common.HexToAddress("0000H45H"), - Root: common.HexToHash("0000H00H"), - TxHash: common.HexToHash("0000H45H"), - ReceiptHash: common.HexToHash("0000H45H"), - Difficulty: big.NewInt(1337), - Number: big.NewInt(1337), - GasLimit: 1338, - GasUsed: 1338, - Time: 1338, - Extra: []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), - MixDigest: common.HexToHash("0x0000H45H"), - } - cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) - if err != nil { - utils.Fatalf("Should not error: %v", err) - } - addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") - _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) - expectApprove("signdata - clique header", err) - } - { // Sign data test - typed data - api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data") - time.Sleep(delay) - addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") - data := `{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"test","type":"uint8"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":"1","verifyingContract":"0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","test":"3","wallet":"0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","test":"2"},"contents":"Hello, Bob!"}}` - //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) - var typedData apitypes.TypedData - json.Unmarshal([]byte(data), &typedData) - _, err := api.SignTypedData(ctx, *addr, typedData) - expectApprove("sign 712 typed data", err) - } - { // Sign data test - plain text - api.UI.ShowInfo("Please approve the next request for signing text") - time.Sleep(delay) - addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") - _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) - expectApprove("signdata - text", err) - } - { // Sign data test - plain text reject - api.UI.ShowInfo("Please deny the next request for signing text") - time.Sleep(delay) - addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") - _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) - expectDeny("signdata - text", err) - } - { // Sign transaction - api.UI.ShowInfo("Please reject next transaction") - time.Sleep(delay) - data := hexutil.Bytes([]byte{}) - to := common.NewMixedcaseAddress(a) - tx := apitypes.SendTxArgs{ - Data: &data, - Nonce: 0x1, - Value: hexutil.Big(*big.NewInt(6)), - From: common.NewMixedcaseAddress(a), - To: &to, - GasPrice: (*hexutil.Big)(big.NewInt(5)), - Gas: 1000, - Input: nil, - } - _, err := api.SignTransaction(ctx, tx, nil) - expectDeny("signtransaction [1]", err) - expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") - } - { // Listing - api.UI.ShowInfo("Please reject listing-request") - time.Sleep(delay) - _, err := api.List(ctx) - expectDeny("list", err) - } - { // Import - api.UI.ShowInfo("Please reject new account-request") - time.Sleep(delay) - _, err := api.New(ctx) - expectDeny("newaccount", err) - } - { // Metadata - api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") - time.Sleep(delay) - api.List(context.WithValue(ctx, "Origin", "origin.com")) - expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") - } - - for _, e := range errs { - log.Error(e) - } - result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) - api.UI.ShowInfo(result) -} - -type encryptedSeedStorage struct { - Description string `json:"description"` - Version int `json:"version"` - Params keystore.CryptoJSON `json:"params"` -} - -// encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, -// to encrypt the master seed -func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { - cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) - if err != nil { - return nil, err - } - return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) -} - -// decryptSeed decrypts the master seed -func decryptSeed(keyjson []byte, auth string) ([]byte, error) { - var encSeed encryptedSeedStorage - if err := json.Unmarshal(keyjson, &encSeed); err != nil { - return nil, err - } - if encSeed.Version != 1 { - log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) - } - seed, err := keystore.DecryptDataV3(encSeed.Params, auth) - if err != nil { - return nil, err - } - return seed, err -} - -// GenDoc outputs examples of all structures used in json-rpc communication -func GenDoc(ctx *cli.Context) error { - var ( - a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") - b = common.HexToAddress("0x1111111122222222222233333333334444444444") - meta = core.Metadata{ - Scheme: "http", - Local: "localhost:8545", - Origin: "www.malicious.ru", - Remote: "localhost:9999", - UserAgent: "Firefox 3.2", - } - output []string - add = func(name, desc string, v interface{}) { - if data, err := json.MarshalIndent(v, "", " "); err == nil { - output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) - } else { - log.Error("Error generating output", "err", err) - } - } - ) - - { // Sign plain text request - desc := "SignDataRequest contains information about a pending request to sign some data. " + - "The data to be signed can be of various types, defined by content-type. Clef has done most " + - "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + - "the user with the contents of the `message`" - sighash, msg := accounts.TextAndHash([]byte("hello world")) - messages := []*apitypes.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}} - - add("SignDataRequest", desc, &core.SignDataRequest{ - Address: common.NewMixedcaseAddress(a), - Meta: meta, - ContentType: accounts.MimetypeTextPlain, - Rawdata: []byte(msg), - Messages: messages, - Hash: sighash}) - } - { // Sign plain text response - add("SignDataResponse - approve", "Response to SignDataRequest", - &core.SignDataResponse{Approved: true}) - add("SignDataResponse - deny", "Response to SignDataRequest", - &core.SignDataResponse{}) - } - { // Sign transaction request - desc := "SignTxRequest contains information about a pending request to sign a transaction. " + - "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + - "messages of various types, that the user should be informed of." + - "\n\n" + - "As in any request, it's important to consider that the `meta` info also contains untrusted data." + - "\n\n" + - "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + - "they must be identical, otherwise an error is generated. " + - "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" - - data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) - add("SignTxRequest", desc, &core.SignTxRequest{ - Meta: meta, - Callinfo: []apitypes.ValidationInfo{ - {Typ: "Warning", Message: "Something looks odd, show this message as a warning"}, - {Typ: "Info", Message: "User should see this as well"}, - }, - Transaction: apitypes.SendTxArgs{ - Data: &data, - Nonce: 0x1, - Value: hexutil.Big(*big.NewInt(6)), - From: common.NewMixedcaseAddress(a), - To: nil, - GasPrice: (*hexutil.Big)(big.NewInt(5)), - Gas: 1000, - Input: nil, - }}) - } - { // Sign tx response - data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) - add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ - ", because the UI is free to make modifications to the transaction.", - &core.SignTxResponse{Approved: true, - Transaction: apitypes.SendTxArgs{ - Data: &data, - Nonce: 0x4, - Value: hexutil.Big(*big.NewInt(6)), - From: common.NewMixedcaseAddress(a), - To: nil, - GasPrice: (*hexutil.Big)(big.NewInt(5)), - Gas: 1000, - Input: nil, - }}) - add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ - "provide the transaction in return", - &core.SignTxResponse{}) - } - { // WHen a signed tx is ready to go out - desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + - "\n\n" + - "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + - "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + - "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + - "\n\n" + - "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + - "interface. By hooking into this methods, the ruleset can maintain track of that count." + - "\n\n" + - "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + - " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + - "\n\n" + - "The `OnApproved` method cannot be responded to, it's purely informative" - - rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") - var tx types.Transaction - tx.UnmarshalBinary(rlpdata) - add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) - } - { // User input - add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", - &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) - add("UserInputResponse", "Response to UserInputRequest", - &core.UserInputResponse{Text: "The textual response from user"}) - } - { // List request - add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ - "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ - "who only sees the `address`es. ", - &core.ListRequest{ - Meta: meta, - Accounts: []accounts.Account{ - {Address: a, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, - {Address: b, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, - }) - - add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ - "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", - &core.ListResponse{ - Accounts: []accounts.Account{ - { - Address: common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), - URL: accounts.URL{Path: ".. ignored .."}, - }, - { - Address: common.MaxAddress, - }, - }}) - } - - fmt.Println(`## UI Client interface - -These data types are defined in the channel between clef and the UI`) - for _, elem := range output { - fmt.Println(elem) - } - return nil -} diff --git a/cmd/clef/pythonsigner.py b/cmd/clef/pythonsigner.py deleted file mode 100644 index 5d0eb18dcc..0000000000 --- a/cmd/clef/pythonsigner.py +++ /dev/null @@ -1,315 +0,0 @@ -import sys -import subprocess - -from tinyrpc.transports import ServerTransport -from tinyrpc.protocols.jsonrpc import JSONRPCProtocol -from tinyrpc.dispatch import public, RPCDispatcher -from tinyrpc.server import RPCServer - -""" -This is a POC example of how to write a custom UI for Clef. -The UI starts the clef process with the '--stdio-ui' option -and communicates with clef using standard input / output. - -The standard input/output is a relatively secure way to communicate, -as it does not require opening any ports or IPC files. Needless to say, -it does not protect against memory inspection mechanisms -where an attacker can access process memory. - -To make this work install all the requirements: - - pip install -r requirements.txt -""" - -try: - import urllib.parse as urlparse -except ImportError: - import urllib as urlparse - - -class StdIOTransport(ServerTransport): - """Uses std input/output for RPC""" - - def receive_message(self): - return None, urlparse.unquote(sys.stdin.readline()) - - def send_reply(self, context, reply): - print(reply) - - -class PipeTransport(ServerTransport): - """Uses std a pipe for RPC""" - - def __init__(self, input, output): - self.input = input - self.output = output - - def receive_message(self): - data = self.input.readline() - print(">> {}".format(data)) - return None, urlparse.unquote(data) - - def send_reply(self, context, reply): - reply = str(reply, "utf-8") - print("<< {}".format(reply)) - self.output.write("{}\n".format(reply)) - - -def sanitize(txt, limit=100): - return txt[:limit].encode("unicode_escape").decode("utf-8") - - -def metaString(meta): - """ - "meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""} - """ # noqa: E501 - message = ( - "\tRequest context:\n" - "\t\t{remote} -> {scheme} -> {local}\n" - "\tAdditional HTTP header data, provided by the external caller:\n" - "\t\tUser-Agent: {user_agent}\n" - "\t\tOrigin: {origin}\n" - ) - return message.format( - remote=meta.get("remote", ""), - scheme=meta.get("scheme", ""), - local=meta.get("local", ""), - user_agent=sanitize(meta.get("User-Agent"), 200), - origin=sanitize(meta.get("Origin"), 100), - ) - - -class StdIOHandler: - def __init__(self): - pass - - @public - def approveTx(self, req): - """ - Example request: - - {"jsonrpc":"2.0","id":20,"method":"ui_approveTx","params":[{"transaction":{"from":"0xDEADbEeF000000000000000000000000DeaDbeEf","to":"0xDEADbEeF000000000000000000000000DeaDbeEf","gas":"0x3e8","gasPrice":"0x5","maxFeePerGas":null,"maxPriorityFeePerGas":null,"value":"0x6","nonce":"0x1","data":"0x"},"call_info":null,"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} - - :param transaction: transaction info - :param call_info: info about the call, e.g. if ABI info could not be - :param meta: metadata about the request, e.g. where the call comes from - :return: - """ # noqa: E501 - message = ( - "Sign transaction request:\n" - "\t{meta_string}\n" - "\n" - "\tFrom: {from_}\n" - "\tTo: {to}\n" - "\n" - "\tAuto-rejecting request" - ) - meta = req.get("meta", {}) - transaction = req.get("transaction") - sys.stdout.write( - message.format( - meta_string=metaString(meta), - from_=transaction.get("from", ""), - to=transaction.get("to", ""), - ) - ) - return { - "approved": False, - } - - @public - def approveSignData(self, req): - """ - Example request: - - {"jsonrpc":"2.0","id":8,"method":"ui_approveSignData","params":[{"content_type":"application/x-clique-header","address":"0x0011223344556677889900112233445566778899","raw_data":"+QIRoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIIFOYIFOYIFOoIFOoIFOppFeHRyYSBkYXRhIEV4dHJhIGRhdGEgRXh0cqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIgAAAAAAAAAAA==","messages":[{"name":"Clique header","value":"clique header 1337 [0x44381ab449d77774874aca34634cb53bc21bd22aef2d3d4cf40e51176cb585ec]","type":"clique"}],"call_info":null,"hash":"0xa47ab61438a12a06c81420e308c2b7aae44e9cd837a5df70dd021421c0f58643","meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} - """ # noqa: E501 - message = ( - "Sign data request:\n" - "\t{meta_string}\n" - "\n" - "\tContent-type: {content_type}\n" - "\tAddress: {address}\n" - "\tHash: {hash_}\n" - "\n" - "\tAuto-rejecting request\n" - ) - meta = req.get("meta", {}) - sys.stdout.write( - message.format( - meta_string=metaString(meta), - content_type=req.get("content_type"), - address=req.get("address"), - hash_=req.get("hash"), - ) - ) - - return { - "approved": False, - "password": None, - } - - @public - def approveNewAccount(self, req): - """ - Example request: - - {"jsonrpc":"2.0","id":25,"method":"ui_approveNewAccount","params":[{"meta":{"remote":"clef binary","local":"main","scheme":"in-proc","User-Agent":"","Origin":""}}]} - """ # noqa: E501 - message = ( - "Create new account request:\n" - "\t{meta_string}\n" - "\n" - "\tAuto-rejecting request\n" - ) - meta = req.get("meta", {}) - sys.stdout.write(message.format(meta_string=metaString(meta))) - return { - "approved": False, - } - - @public - def showError(self, req): - """ - Example request: - - {"jsonrpc":"2.0","method":"ui_showError","params":[{"text":"If you see this message, enter 'yes' to the next question"}]} - - :param message: to display - :return:nothing - """ # noqa: E501 - message = ( - "## Error\n{text}\n" - "Press enter to continue\n" - ) - text = req.get("text") - sys.stdout.write(message.format(text=text)) - input() - return - - @public - def showInfo(self, req): - """ - Example request: - - {"jsonrpc":"2.0","method":"ui_showInfo","params":[{"text":"If you see this message, enter 'yes' to next question"}]} - - :param message: to display - :return:nothing - """ # noqa: E501 - message = ( - "## Info\n{text}\n" - "Press enter to continue\n" - ) - text = req.get("text") - sys.stdout.write(message.format(text=text)) - input() - return - - @public - def onSignerStartup(self, req): - """ - Example request: - - {"jsonrpc":"2.0", "method":"ui_onSignerStartup", "params":[{"info":{"extapi_http":"n/a","extapi_ipc":"/home/user/.clef/clef.ipc","extapi_version":"6.1.0","intapi_version":"7.0.1"}}]} - """ # noqa: E501 - message = ( - "\n" - "\t\tExt api url: {extapi_http}\n" - "\t\tInt api ipc: {extapi_ipc}\n" - "\t\tExt api ver: {extapi_version}\n" - "\t\tInt api ver: {intapi_version}\n" - ) - info = req.get("info") - sys.stdout.write( - message.format( - extapi_http=info.get("extapi_http"), - extapi_ipc=info.get("extapi_ipc"), - extapi_version=info.get("extapi_version"), - intapi_version=info.get("intapi_version"), - ) - ) - - @public - def approveListing(self, req): - """ - Example request: - - {"jsonrpc":"2.0","id":23,"method":"ui_approveListing","params":[{"accounts":[{"address":... - """ # noqa: E501 - message = ( - "\n" - "## Account listing request\n" - "\t{meta_string}\n" - "\tDo you want to allow listing the following accounts?\n" - "\t-{addrs}\n" - "\n" - "->Auto-answering No\n" - ) - meta = req.get("meta", {}) - accounts = req.get("accounts", []) - addrs = [x.get("address") for x in accounts] - sys.stdout.write( - message.format( - addrs="\n\t-".join(addrs), - meta_string=metaString(meta) - ) - ) - return {} - - @public - def onInputRequired(self, req): - """ - Example request: - - {"jsonrpc":"2.0","id":1,"method":"ui_onInputRequired","params":[{"title":"Master Password","prompt":"Please enter the password to decrypt the master seed","isPassword":true}]} - - :param message: to display - :return:nothing - """ # noqa: E501 - message = ( - "\n" - "## {title}\n" - "\t{prompt}\n" - "\n" - "> " - ) - sys.stdout.write( - message.format( - title=req.get("title"), - prompt=req.get("prompt") - ) - ) - isPassword = req.get("isPassword") - if not isPassword: - return {"text": input()} - - return "" - - -def main(args): - cmd = ["clef", "--stdio-ui"] - if len(args) > 0 and args[0] == "test": - cmd.extend(["--stdio-ui-test"]) - print("cmd: {}".format(" ".join(cmd))) - - dispatcher = RPCDispatcher() - dispatcher.register_instance(StdIOHandler(), "ui_") - - # line buffered - p = subprocess.Popen( - cmd, - bufsize=1, - universal_newlines=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - ) - - rpc_server = RPCServer( - PipeTransport(p.stdout, p.stdin), JSONRPCProtocol(), dispatcher - ) - rpc_server.serve_forever() - - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/cmd/clef/requirements.txt b/cmd/clef/requirements.txt deleted file mode 100644 index 5381862e30..0000000000 --- a/cmd/clef/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -tinyrpc==1.1.4 diff --git a/cmd/clef/rules.md b/cmd/clef/rules.md deleted file mode 100644 index ef89ec00c8..0000000000 --- a/cmd/clef/rules.md +++ /dev/null @@ -1,234 +0,0 @@ -# Rules - -The `signer` binary contains a ruleset engine, implemented with [OttoVM](https://github.com/robertkrimen/otto) - -It enables use cases like the following: - -* I want to auto-approve transactions with contract `CasinoDapp`, with up to `0.05 ether` in value to maximum `1 ether` per 24h period -* I want to auto-approve transaction to contract `EthAlarmClock` with `data`=`0xdeadbeef`, if `value=0`, `gas < 44k` and `gasPrice < 40Gwei` - -The two main features that are required for this to work well are: - -1. Rule Implementation: how to create, manage, and interpret rules in a flexible but secure manner -2. Credential management and credentials; how to provide auto-unlock without exposing keys unnecessarily. - -The section below deals with both of them - -## Rule Implementation - -A ruleset file is implemented as a `js` file. Under the hood, the ruleset engine is a `SignerUI`, implementing the same methods as the `json-rpc` methods -defined in the UI protocol. Example: - -```js -function asBig(str) { - if (str.slice(0, 2) == "0x") { - return new BigNumber(str.slice(2), 16) - } - return new BigNumber(str) -} - -// Approve transactions to a certain contract if the value is below a certain limit -function ApproveTx(req) { - var limit = new BigNumber("0xb1a2bc2ec50000") - var value = asBig(req.transaction.value); - - if (req.transaction.to.toLowerCase() == "0xae967917c465db8578ca9024c205720b1a3651a9" && value.lt(limit)) { - return "Approve" - } - // If we return "Reject", it will be rejected. - // By not returning anything, it will be passed to the next UI, for manual processing -} - -// Approve listings if request made from IPC -function ApproveListing(req){ - if (req.metadata.scheme == "ipc"){ return "Approve"} -} -``` - -Whenever the external API is called (and the ruleset is enabled), the `signer` calls the UI, which is an instance of a ruleset-engine. The ruleset-engine -invokes the corresponding method. In doing so, there are three possible outcomes: - -1. JS returns "Approve" - * Auto-approve request -2. JS returns "Reject" - * Auto-reject request -3. Error occurs, or something else is returned - * Pass on to `next` ui: the regular UI channel. - -A more advanced example can be found below, "Example 1: ruleset for a rate-limited window", using `storage` to `Put` and `Get` `string`s by key. - -* At the time of writing, storage only exists as an ephemeral unencrypted implementation, to be used during testing. - -### Things to note - -The Otto vm has a few [caveats](https://github.com/robertkrimen/otto): - -* "use strict" will parse, but does nothing. -* The regular expression engine (re2/regexp) is not fully compatible with the ECMA5 specification. -* Otto targets ES5. ES6 features (eg: Typed Arrays) are not supported. - -Additionally, a few more have been added - -* The rule execution cannot load external javascript files. -* The only preloaded library is [`bignumber.js`](https://github.com/MikeMcl/bignumber.js) version `2.0.3`. This one is fairly old, and is not aligned with the documentation at the GitHub repository. -* Each invocation is made in a fresh virtual machine. This means that you cannot store data in global variables between invocations. This is a deliberate choice -- if you want to store data, use the disk-backed `storage`, since rules should not rely on ephemeral data. -* Javascript API parameters are _always_ an object. This is also a design choice, to ensure that parameters are accessed by _key_ and not by order. This is to prevent mistakes due to missing parameters or parameter changes. -* The JS engine has access to `storage` and `console`. - -#### Security considerations - -##### Security of ruleset - -Some security precautions can be made, such as: - -* Never load `ruleset.js` unless the file is `readonly` (`r-??-??-?`). If the user wishes to modify the ruleset, he must make it writeable and then set back to readonly. - * This is to prevent attacks where files are dropped on the users disk. -* Since we're going to have to have some form of secure storage (not defined in this section), we could also store the `sha3` of the `ruleset.js` file in there. - * If the user wishes to modify the ruleset, he'd then have to perform e.g. `signer --attest /path/to/ruleset --credential ` - -##### Security of implementation - -The drawback of this very flexible solution is that the `signer` needs to contain a javascript engine. This is pretty simple to implement since it's already -implemented for `geth`. There are no known security vulnerabilities in it, nor have we had any security problems with it so far. - -The javascript engine would be an added attack surface; but if the validation of `rulesets` is made good (with hash-based attestation), the actual javascript cannot be considered -an attack surface -- if an attacker can control the ruleset, a much simpler attack would be to implement an "always-approve" rule instead of exploiting the js vm. The only benefit -to be gained from attacking the actual `signer` process from the `js` side would be if it could somehow extract cryptographic keys from memory. - -##### Security in usability - -Javascript is flexible, but also easy to get wrong, especially when users assume that `js` can handle large integers natively. Typical errors -include trying to multiply `gasCost` with `gas` without using `bigint`:s. - -It's unclear whether any other DSL could be more secure; since there's always the possibility of erroneously implementing a rule. - - -## Credential management - -The ability to auto-approve transactions means that the signer needs to have the necessary credentials to decrypt keyfiles. These passwords are hereafter called `ksp` (keystore pass). - -### Example implementation - -Upon startup of the signer, the signer is given a switch: `--seed ` -The `seed` contains a blob of bytes, which is the master seed for the `signer`. - -The `signer` uses the `seed` to: - -* Generate the `path` where the settings are stored. - * `./settings/1df094eb-c2b1-4689-90dd-790046d38025/vault.dat` - * `./settings/1df094eb-c2b1-4689-90dd-790046d38025/rules.js` -* Generate the encryption password for `vault.dat`. - -The `vault.dat` would be an encrypted container storing the following information: - -* `ksp` entries -* `sha256` hash of `rules.js` -* Information about pair:ed callers (not yet specified) - -### Security considerations - -This would leave it up to the user to ensure that the `path/to/masterseed` is handled securely. It's difficult to get around this, although one could -imagine leveraging OS-level keychains where supported. The setup is however, in general, similar to how ssh-keys are stored in `.ssh/`. - - -# Implementation status - -This is now implemented (with ephemeral non-encrypted storage for now, so not yet enabled). - -## Example 1: ruleset for a rate-limited window - - -```js -function big(str) { - if (str.slice(0, 2) == "0x") { - return new BigNumber(str.slice(2), 16) - } - return new BigNumber(str) -} - -// Time window: 1 week -var window = 1000* 3600*24*7; - -// Limit: 1 ether -var limit = new BigNumber("1e18"); - -function isLimitOk(transaction) { - var value = big(transaction.value) - // Start of our window function - var windowstart = new Date().getTime() - window; - - var txs = []; - var stored = storage.get('txs'); - - if (stored != "") { - txs = JSON.parse(stored) - } - // First, remove all that has passed out of the time window - var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); - console.log(txs, newtxs.length); - - // Secondly, aggregate the current sum - sum = new BigNumber(0) - - sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); - console.log("ApproveTx > Sum so far", sum); - console.log("ApproveTx > Requested", value.toNumber()); - - // Would we exceed the weekly limit ? - return sum.plus(value).lt(limit) - -} -function ApproveTx(r) { - if (isLimitOk(r.transaction)) { - return "Approve" - } - return "Nope" -} - -/** -* OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter - * 'response_str' contains the return value that will be sent to the external caller. -* The return value from this method is ignore - the reason for having this callback is to allow the -* ruleset to keep track of approved transactions. -* -* When implementing rate-limited rules, this callback should be used. -* If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user -* then accepts the transaction, this method will be called. -* -* TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. -*/ -function OnApprovedTx(resp) { - var value = big(resp.tx.value) - var txs = [] - // Load stored transactions - var stored = storage.get('txs'); - if (stored != "") { - txs = JSON.parse(stored) - } - // Add this to the storage - txs.push({tstamp: new Date().getTime(), value: value}); - storage.put("txs", JSON.stringify(txs)); -} -``` - -## Example 2: allow destination - -```js -function ApproveTx(r) { - if (r.transaction.from.toLowerCase() == "0x0000000000000000000000000000000000001337") { - return "Approve" - } - if (r.transaction.from.toLowerCase() == "0x000000000000000000000000000000000000dead") { - return "Reject" - } - // Otherwise goes to manual processing -} -``` - -## Example 3: Allow listing - -```js -function ApproveListing() { - return "Approve" -} -``` diff --git a/cmd/clef/run_test.go b/cmd/clef/run_test.go deleted file mode 100644 index d404457ba2..0000000000 --- a/cmd/clef/run_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of go-ethereum. -// -// go-ethereum is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// go-ethereum is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with go-ethereum. If not, see . - -package main - -import ( - "fmt" - "os" - "testing" - - "github.com/ethereum/go-ethereum/internal/cmdtest" - "github.com/ethereum/go-ethereum/internal/reexec" -) - -const registeredName = "clef-test" - -type testproc struct { - *cmdtest.TestCmd - - // template variables for expect - Datadir string - Etherbase string -} - -func init() { - reexec.Register(registeredName, func() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - os.Exit(0) - }) -} - -func TestMain(m *testing.M) { - // check if we have been reexec'd - if reexec.Init() { - return - } - os.Exit(m.Run()) -} - -// runClef spawns clef with the given command line args and adds keystore arg. -// This method creates a temporary keystore folder which will be removed after -// the test exits. -func runClef(t *testing.T, args ...string) *testproc { - ddir := t.TempDir() - return runWithKeystore(t, ddir, args...) -} - -// runWithKeystore spawns clef with the given command line args and adds keystore arg. -// This method does _not_ create the keystore folder, but it _does_ add the arg -// to the args. -func runWithKeystore(t *testing.T, keystore string, args ...string) *testproc { - args = append([]string{"--keystore", keystore}, args...) - tt := &testproc{Datadir: keystore} - tt.TestCmd = cmdtest.NewTestCmd(t, tt) - // Boot "clef". This actually runs the test binary but the TestMain - // function will prevent any tests from running. - tt.Run(registeredName, args...) - return tt -} - -func (proc *testproc) input(text string) *testproc { - proc.TestCmd.InputLine(text) - return proc -} - -/* -// waitForEndpoint waits for the rpc endpoint to appear, or -// aborts after 3 seconds. -func (proc *testproc) waitForEndpoint(t *testing.T) *testproc { - t.Helper() - timeout := 3 * time.Second - ipc := filepath.Join(proc.Datadir, "clef.ipc") - - start := time.Now() - for time.Since(start) < timeout { - if _, err := os.Stat(ipc); !errors.Is(err, os.ErrNotExist) { - t.Logf("endpoint %v opened", ipc) - return proc - } - time.Sleep(200 * time.Millisecond) - } - t.Logf("stderr: \n%v", proc.StderrText()) - t.Logf("stdout: \n%v", proc.Output()) - t.Fatal("endpoint", ipc, "did not open within", timeout) - return proc -} -*/ diff --git a/cmd/clef/sign_flow.png b/cmd/clef/sign_flow.png deleted file mode 100644 index e7010ab43f3a6f54d69958055b93b611f7853951..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20537 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. - -// This file is a test-utility for testing clef-functionality -// -// Start clef with -// -// build/bin/clef --4bytedb=./cmd/clef/4byte.json --rpc -// -// Start geth with -// -// build/bin/geth --nodiscover --maxpeers 0 --signer http://localhost:8550 console --preload=cmd/clef/tests/testsigner.js -// -// and in the console simply invoke -// -// > test() -// -// You can reload the file via `reload()` - -function reload(){ - loadScript("./cmd/clef/tests/testsigner.js"); -} - -function init(){ - if (typeof accts == 'undefined' || accts.length == 0){ - accts = eth.accounts - console.log("Got accounts ", accts); - } -} -init() -function testTx(){ - if( accts && accts.length > 0) { - var a = accts[0] - var txdata = eth.signTransaction({from: a, to: a, value: 1, nonce: 1, gas: 1, gasPrice: 1}) - var v = parseInt(txdata.tx.v) - console.log("V value: ", v) - if (v == 37 || v == 38){ - console.log("Mainnet 155-protected chainid was used") - } - if (v == 27 || v == 28){ - throw new Error("Mainnet chainid was used, but without replay protection!") - } - } -} -function testSignText(){ - if( accts && accts.length > 0){ - var a = accts[0] - var r = eth.sign(a, "0x68656c6c6f20776f726c64"); //hello world - console.log("signing response", r) - } -} -function testClique(){ - if( accts && accts.length > 0){ - var a = accts[0] - var r = debug.testSignCliqueBlock(a, 0); // Sign genesis - console.log("signing response", r) - if( a != r){ - throw new Error("Requested signing by "+a+ " but got sealer "+r) - } - } -} - -function test(){ - var tests = [ - testTx, - testSignText, - testClique, - ] - for( i in tests){ - try{ - tests[i]() - }catch(err){ - console.log(err) - } - } - } diff --git a/cmd/clef/tutorial.md b/cmd/clef/tutorial.md deleted file mode 100644 index 3ea662b5d4..0000000000 --- a/cmd/clef/tutorial.md +++ /dev/null @@ -1,353 +0,0 @@ -## Initializing Clef - -First things first, Clef needs to store some data itself. Since that data might be sensitive (passwords, signing rules, accounts), Clef's entire storage is encrypted. To support encrypting data, the first step is to initialize Clef with a random master seed, itself too encrypted with your chosen password: - -```text -$ clef init - -WARNING! - -Clef is an account management tool. It may, like any software, contain bugs. - -Please take care to -- backup your keystore files, -- verify that the keystore(s) can be opened with your password. - -Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -PURPOSE. See the GNU General Public License for more details. - -Enter 'ok' to proceed: -> ok - -The master seed of clef will be locked with a password. -Please specify a password. Do not forget this password! -Password: -Repeat password: - -A master seed has been generated into /home/martin/.clef/masterseed.json - -This is required to be able to store credentials, such as: -* Passwords for keystores (used by rule engine) -* Storage for JavaScript auto-signing rules -* Hash of JavaScript rule-file - -You should treat 'masterseed.json' with utmost secrecy and make a backup of it! -* The password is necessary but not enough, you need to back up the master seed too! -* The master seed does not contain your accounts, those need to be backed up separately! -``` - -*For readability purposes, we'll remove the WARNING printout, user confirmation and the unlocking of the master seed in the rest of this document.* - -## Remote interactions - -Clef is capable of managing both key-file based accounts as well as hardware wallets. To evaluate clef, we're going to point it to our Rinkeby testnet keystore and specify the Rinkeby chain ID for signing (Clef doesn't have a backing chain, so it doesn't know what network it runs on). - -```text -$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 - -INFO [07-01|11:00:46.385] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false -DEBUG[07-01|11:00:46.389] FS scan times list=3.521941ms set=9.017µs diff=4.112µs -DEBUG[07-01|11:00:46.391] Ledger support enabled -DEBUG[07-01|11:00:46.391] Trezor support enabled via HID -DEBUG[07-01|11:00:46.391] Trezor support enabled via WebUSB -INFO [07-01|11:00:46.391] Audit logs configured file=audit.log -DEBUG[07-01|11:00:46.392] IPC registered namespace=account -INFO [07-01|11:00:46.392] IPC endpoint opened url=$HOME/.clef/clef.ipc -------- Signer info ------- -* intapi_version : 7.0.0 -* extapi_version : 6.0.0 -* extapi_http : n/a -* extapi_ipc : $HOME/.clef/clef.ipc -``` - -By default, Clef starts up in CLI (Command Line Interface) mode. Arbitrary remote processes may *request* account interactions (e.g. sign a transaction), which the user will need to individually *confirm*. - -To test this out, we can *request* Clef to list all account via its *External API endpoint*: - -```text -echo '{"id": 1, "jsonrpc": "2.0", "method": "account_list"}' | nc -U ~/.clef/clef.ipc -``` - -This will prompt the user within the Clef CLI to confirm or deny the request: - -```text --------- List Account request-------------- -A request has been made to list all accounts. -You can select which accounts the caller can see - [x] 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 - URL: keystore://$HOME/.ethereum/rinkeby/keystore/UTC--2017-04-14T15-15-00.327614556Z--d9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 - [x] 0x086278A6C067775F71d6B2BB1856Db6E28c30418 - URL: keystore://$HOME/.ethereum/rinkeby/keystore/UTC--2018-02-06T22-53-11.211657239Z--086278a6c067775f71d6b2bb1856db6e28c30418 -------------------------------------------- -Request context: - NA -> NA -> NA - -Additional HTTP header data, provided by the external caller: - User-Agent: - Origin: -Approve? [y/N]: -> -``` - -Depending on whether we approve or deny the request, the original NetCat process will get: - -```text -{"jsonrpc":"2.0","id":1,"result":["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3","0x086278a6c067775f71d6b2bb1856db6e28c30418"]} - -or - -{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Request denied"}} -``` - -Apart from listing accounts, you can also *request* creating a new account; signing transactions and data; and recovering signatures. You can find the available methods in the Clef [External API Spec](https://github.com/ethereum/go-ethereum/tree/master/cmd/clef#external-api-1) and the [External API Changelog](https://github.com/ethereum/go-ethereum/blob/master/cmd/clef/extapi_changelog.md). - -*Note, the number of things you can do from the External API is deliberately small, since we want to limit the power of remote calls by as much as possible! Clef has an [Internal API](https://github.com/ethereum/go-ethereum/tree/master/cmd/clef#ui-api-1) too for the UI (User Interface) which is much richer and can support custom interfaces on top. But that's out of scope here.* - -## Automatic rules - -For most users, manually confirming every transaction is the way to go. However, there are cases when it makes sense to set up some rules which permit Clef to sign a transaction without prompting the user. One such example would be running a signer on Rinkeby or other PoA networks. - -For starters, we can create a rule file that automatically permits anyone to list our available accounts without user confirmation. The rule file is a tiny JavaScript snippet that you can program however you want: - -```js -function ApproveListing() { - return "Approve" -} -``` - -Of course, Clef isn't going to just accept and run arbitrary scripts you give it, that would be dangerous if someone changes your rule file! Instead, you need to explicitly *attest* the rule file, which entails injecting its hash into Clef's secure store. - -```text -$ sha256sum rules.js -645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c rules.js - -$ clef attest 645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c -Decrypt master seed of clef -Password: -INFO [07-01|13:25:03.290] Ruleset attestation updated sha256=645b58e4f945e24d0221714ff29f6aa8e860382ced43490529db1695f5fcc71c -``` - -At this point, we can start Clef with the rule file: - -```text -$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js - -INFO [07-01|13:39:49.726] Rule engine configured file=rules.js -INFO [07-01|13:39:49.726] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false -DEBUG[07-01|13:39:49.726] FS scan times list=35.15µs set=4.251µs diff=2.766µs -DEBUG[07-01|13:39:49.727] Ledger support enabled -DEBUG[07-01|13:39:49.727] Trezor support enabled via HID -DEBUG[07-01|13:39:49.727] Trezor support enabled via WebUSB -INFO [07-01|13:39:49.728] Audit logs configured file=audit.log -DEBUG[07-01|13:39:49.728] IPC registered namespace=account -INFO [07-01|13:39:49.728] IPC endpoint opened url=$HOME/.clef/clef.ipc -------- Signer info ------- -* intapi_version : 7.0.0 -* extapi_version : 6.0.0 -* extapi_http : n/a -* extapi_ipc : $HOME/.clef/clef.ipc -``` - -Any account listing *request* will now be auto-approved by the rule file: - -```text -$ echo '{"id": 1, "jsonrpc": "2.0", "method": "account_list"}' | nc -U ~/.clef/clef.ipc -{"jsonrpc":"2.0","id":1,"result":["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3","0x086278a6c067775f71d6b2bb1856db6e28c30418"]} -``` - -## Under the hood - -While doing the operations above, these files have been created: - -```text -$ ls -laR ~/.clef/ - -$HOME/.clef/: -total 24 -drwxr-x--x 3 user user 4096 Jul 1 13:45 . -drwxr-xr-x 102 user user 12288 Jul 1 13:39 .. -drwx------ 2 user user 4096 Jul 1 13:25 02f90c0603f4f2f60188 --r-------- 1 user user 868 Jun 28 13:55 masterseed.json - -$HOME/.clef/02f90c0603f4f2f60188: -total 12 -drwx------ 2 user user 4096 Jul 1 13:25 . -drwxr-x--x 3 user user 4096 Jul 1 13:45 .. --rw------- 1 user user 159 Jul 1 13:25 config.json - -$ cat ~/.clef/02f90c0603f4f2f60188/config.json -{"ruleset_sha256":{"iv":"SWWEtnl+R+I+wfG7","c":"I3fjmwmamxVcfGax7D0MdUOL29/rBWcs73WBILmYK0o1CrX7wSMc3y37KsmtlZUAjp0oItYq01Ow8VGUOzilG91tDHInB5YHNtm/YkufEbo="}} -``` - -In `$HOME/.clef`, the `masterseed.json` file was created, containing the master seed. This seed was then used to derive a few other things: - -- **Vault location**: in this case `02f90c0603f4f2f60188`. - - If you use a different master seed, a different vault location will be used that does not conflict with each other (e.g. `clef --signersecret /path/to/file`). This allows you to run multiple instances of Clef, each with its own rules (e.g. mainnet + testnet). -- **`config.json`**: the encrypted key/value storage for configuration data, currently only containing the key `ruleset_sha256`, the attested hash of the automatic rules to use. - -## Advanced rules - -In order to make more useful rules - like signing transactions - the signer needs access to the passwords needed to unlock keys from the keystore. You can inject an unlock password via `clef setpw`. - -```text -$ clef setpw 0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 - -Please enter a password to store for this address: -Password: -Repeat password: - -Decrypt master seed of clef -Password: -INFO [07-01|14:05:56.031] Credential store updated key=0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 -``` - -Now let's update the rules to make use of the new credentials: - -```js -function ApproveListing() { - return "Approve" -} - -function ApproveSignData(req) { - if (req.address.toLowerCase() == "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3") { - if (req.messages[0].value.indexOf("bazonk") >= 0) { - return "Approve" - } - return "Reject" - } - // Otherwise goes to manual processing -} -``` - -In this example: - -- Any requests to sign data with the account `0xd9c9...` will be: - - Auto-approved if the message contains `bazonk`, - - Auto-rejected if the message does not contain `bazonk`, -- Any other requests will be passed along for manual confirmation. - -*Note, to make this example work, please use you own accounts. You can create a new account either via Clef or the traditional account CLI tools. If the latter was chosen, make sure both Clef and Geth use the same keystore by specifying `--keystore path/to/your/keystore` when running Clef.* - -Attest the new rule file so that Clef will accept loading it: - -```text -$ sha256sum rules.js -f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 rules.js - -$ clef attest f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 -Decrypt master seed of clef -Password: -INFO [07-01|14:11:28.509] Ruleset attestation updated sha256=f163a1738b649259bb9b369c593fdc4c6b6f86cc87e343c3ba58faee03c2a178 -``` - -Restart Clef with the new rules in place: - -``` -$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js - -INFO [07-01|14:12:41.636] Rule engine configured file=rules.js -INFO [07-01|14:12:41.636] Starting signer chainid=4 keystore=$HOME/.ethereum/rinkeby/keystore light-kdf=false advanced=false -DEBUG[07-01|14:12:41.636] FS scan times list=46.722µs set=4.47µs diff=2.157µs -DEBUG[07-01|14:12:41.637] Ledger support enabled -DEBUG[07-01|14:12:41.637] Trezor support enabled via HID -DEBUG[07-01|14:12:41.638] Trezor support enabled via WebUSB -INFO [07-01|14:12:41.638] Audit logs configured file=audit.log -DEBUG[07-01|14:12:41.638] IPC registered namespace=account -INFO [07-01|14:12:41.638] IPC endpoint opened url=$HOME/.clef/clef.ipc -------- Signer info ------- -* intapi_version : 7.0.0 -* extapi_version : 6.0.0 -* extapi_http : n/a -* extapi_ipc : $HOME/.clef/clef.ipc -``` - -Then test signing, once with `bazonk` and once without: - -``` -$ echo '{"id": 1, "jsonrpc":"2.0", "method":"account_signData", "params":["data/plain", "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x202062617a6f6e6b2062617a2067617a0a"]}' | nc -U ~/.clef/clef.ipc -{"jsonrpc":"2.0","id":1,"result":"0x4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c"} - -$ echo '{"id": 1, "jsonrpc":"2.0", "method":"account_signData", "params":["data/plain", "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x2020626f6e6b2062617a2067617a0a"]}' | nc -U ~/.clef/clef.ipc -{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"Request denied"}} -``` - -Meanwhile, in the Clef output log you can see: -```text -INFO [02-21|14:42:41] Op approved -INFO [02-21|14:42:56] Op rejected -``` - -The signer also stores all traffic over the external API in a log file. The last 4 lines shows the two requests and their responses: - -```text -$ tail -n 4 audit.log -t=2019-07-01T15:52:14+0300 lvl=info msg=SignData api=signer type=request metadata="{\"remote\":\"NA\",\"local\":\"NA\",\"scheme\":\"NA\",\"User-Agent\":\"\",\"Origin\":\"\"}" addr="0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 [chksum INVALID]" data=0x202062617a6f6e6b2062617a2067617a0a content-type=data/plain -t=2019-07-01T15:52:14+0300 lvl=info msg=SignData api=signer type=response data=4f93e3457027f6be99b06b3392d0ebc60615ba448bb7544687ef1248dea4f5317f789002df783979c417d969836b6fda3710f5bffb296b4d51c8aaae6e2ac4831c error=nil -t=2019-07-01T15:52:23+0300 lvl=info msg=SignData api=signer type=request metadata="{\"remote\":\"NA\",\"local\":\"NA\",\"scheme\":\"NA\",\"User-Agent\":\"\",\"Origin\":\"\"}" addr="0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3 [chksum INVALID]" data=0x2020626f6e6b2062617a2067617a0a content-type=data/plain -t=2019-07-01T15:52:23+0300 lvl=info msg=SignData api=signer type=response data= error="Request denied" -``` - -For more details on writing automatic rules, please see the [rules spec](https://github.com/ethereum/go-ethereum/blob/master/cmd/clef/rules.md). - -## Geth integration - -Of course, as awesome as Clef is, it's not feasible to interact with it via JSON RPC by hand. Long term, we're hoping to convince the general Ethereum community to support Clef as a general signer (it's only 3-5 methods), thus allowing your favorite DApp, Metamask, MyCrypto, etc to request signatures directly. - -Until then however, we're trying to pave the way via Geth. Geth v1.9.0 has built in support via `--signer ` for using a local or remote Clef instance as an account backend! - -We can try this by running Clef with our previous rules on Rinkeby (for now it's a good idea to allow auto-listing accounts, since Geth likes to retrieve them once in a while). - -```text -$ clef --keystore ~/.ethereum/rinkeby/keystore --chainid 4 --rules rules.js -``` - -In a different window we can start Geth, list our accounts, even list our wallets to see where the accounts originate from: - -```text -$ geth --rinkeby --signer=~/.clef/clef.ipc console - -> eth.accounts -["0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", "0x086278a6c067775f71d6b2bb1856db6e28c30418"] - -> personal.listWallets -[{ - accounts: [{ - address: "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3", - url: "extapi://$HOME/.clef/clef.ipc" - }, { - address: "0x086278a6c067775f71d6b2bb1856db6e28c30418", - url: "extapi://$HOME/.clef/clef.ipc" - }], - status: "ok [version=6.0.0]", - url: "extapi://$HOME/.clef/clef.ipc" -}] - -> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[0]}) -``` - -Lastly, when we requested a transaction to be sent, Clef prompted us in the original window to approve it: - -```text ---------- Transaction request------------- -to: 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 -from: 0xD9C9Cd5f6779558b6e0eD4e6Acf6b1947E7fA1F3 [chksum ok] -value: 0 wei -gas: 0x5208 (21000) -gasprice: 1000000000 wei -nonce: 0x2366 (9062) - -Request context: - NA -> NA -> NA - -Additional HTTP header data, provided by the external caller: - User-Agent: - Origin: -------------------------------------------- -Approve? [y/N]: -> y -``` - -:boom: - -*Note, if you enable the external signer backend in Geth, all other account management is disabled. This is because long term we want to remove account management from Geth.* diff --git a/signer/core/api.go b/signer/core/api.go deleted file mode 100644 index c5cc226b79..0000000000 --- a/signer/core/api.go +++ /dev/null @@ -1,668 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - "os" - "reflect" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/accounts/scwallet" - "github.com/ethereum/go-ethereum/accounts/usbwallet" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/ethereum/go-ethereum/signer/storage" -) - -const ( - // numberOfAccountsToDerive For hardware wallets, the number of accounts to derive - numberOfAccountsToDerive = 10 - // ExternalAPIVersion -- see extapi_changelog.md - ExternalAPIVersion = "6.1.0" - // InternalAPIVersion -- see intapi_changelog.md - InternalAPIVersion = "7.0.1" -) - -// ExternalAPI defines the external API through which signing requests are made. -type ExternalAPI interface { - // List available accounts - List(ctx context.Context) ([]common.Address, error) - // New request to create a new account - New(ctx context.Context) (common.Address, error) - // SignTransaction request to sign the specified transaction - SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) - // SignData - request to sign the given data (plus prefix) - SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) - // SignTypedData - request to sign the given structured data (plus prefix) - SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error) - // EcRecover - recover public key from given message and signature - EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) - // Version info about the APIs - Version(ctx context.Context) (string, error) - // SignGnosisSafeTx signs/confirms a gnosis-safe multisig transaction - SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) -} - -// UIClientAPI specifies what method a UI needs to implement to be able to be used as a -// UI for the signer -type UIClientAPI interface { - // ApproveTx prompt the user for confirmation to request to sign Transaction - ApproveTx(request *SignTxRequest) (SignTxResponse, error) - // ApproveSignData prompt the user for confirmation to request to sign data - ApproveSignData(request *SignDataRequest) (SignDataResponse, error) - // ApproveListing prompt the user for confirmation to list accounts - // the list of accounts to list can be modified by the UI - ApproveListing(request *ListRequest) (ListResponse, error) - // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller - ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) - // ShowError displays error message to user - ShowError(message string) - // ShowInfo displays info message to user - ShowInfo(message string) - // OnApprovedTx notifies the UI about a transaction having been successfully signed. - // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. - OnApprovedTx(tx ethapi.SignTransactionResult) - // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version - // information - OnSignerStartup(info StartupInfo) - // OnInputRequired is invoked when clef requires user input, for example master password or - // pin-code for unlocking hardware wallets - OnInputRequired(info UserInputRequest) (UserInputResponse, error) - // RegisterUIServer tells the UI to use the given UIServerAPI for ui->clef communication - RegisterUIServer(api *UIServerAPI) -} - -// Validator defines the methods required to validate a transaction against some -// sanity defaults as well as any underlying 4byte method database. -// -// Use fourbyte.Database as an implementation. It is separated out of this package -// to allow pieces of the signer package to be used without having to load the -// 7MB embedded 4byte dump. -type Validator interface { - // ValidateTransaction does a number of checks on the supplied transaction, and - // returns either a list of warnings, or an error (indicating that the transaction - // should be immediately rejected). - ValidateTransaction(selector *string, tx *apitypes.SendTxArgs) (*apitypes.ValidationMessages, error) -} - -// SignerAPI defines the actual implementation of ExternalAPI -type SignerAPI struct { - chainID *big.Int - am *accounts.Manager - UI UIClientAPI - validator Validator - rejectMode bool - credentials storage.Storage -} - -// Metadata about a request -type Metadata struct { - Remote string `json:"remote"` - Local string `json:"local"` - Scheme string `json:"scheme"` - UserAgent string `json:"User-Agent"` - Origin string `json:"Origin"` -} - -func StartClefAccountManager(ksLocation string, lightKDF bool, scpath string) *accounts.Manager { - var ( - backends []accounts.Backend - n, p = keystore.StandardScryptN, keystore.StandardScryptP - ) - if lightKDF { - n, p = keystore.LightScryptN, keystore.LightScryptP - } - // support password based accounts - if len(ksLocation) > 0 { - backends = append(backends, keystore.NewKeyStore(ksLocation, n, p)) - } - // Start a USB hub for Ledger hardware wallets - if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil { - log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err)) - } else { - backends = append(backends, ledgerhub) - log.Debug("Ledger support enabled") - } - // Start a USB hub for Trezor hardware wallets (HID version) - if trezorhub, err := usbwallet.NewTrezorHubWithHID(); err != nil { - log.Warn(fmt.Sprintf("Failed to start HID Trezor hub, disabling: %v", err)) - } else { - backends = append(backends, trezorhub) - log.Debug("Trezor support enabled via HID") - } - // Start a USB hub for Trezor hardware wallets (WebUSB version) - if trezorhub, err := usbwallet.NewTrezorHubWithWebUSB(); err != nil { - log.Warn(fmt.Sprintf("Failed to start WebUSB Trezor hub, disabling: %v", err)) - } else { - backends = append(backends, trezorhub) - log.Debug("Trezor support enabled via WebUSB") - } - - // Start a smart card hub - if len(scpath) > 0 { - // Sanity check that the smartcard path is valid - fi, err := os.Stat(scpath) - if err != nil { - log.Info("Smartcard socket file missing, disabling", "err", err) - } else { - if fi.Mode()&os.ModeType != os.ModeSocket { - log.Error("Invalid smartcard socket file type", "path", scpath, "type", fi.Mode().String()) - } else { - if schub, err := scwallet.NewHub(scpath, scwallet.Scheme, ksLocation); err != nil { - log.Warn(fmt.Sprintf("Failed to start smart card hub, disabling: %v", err)) - } else { - backends = append(backends, schub) - } - } - } - } - return accounts.NewManager(nil, backends...) -} - -// MetadataFromContext extracts Metadata from a given context.Context -func MetadataFromContext(ctx context.Context) Metadata { - info := rpc.PeerInfoFromContext(ctx) - - m := Metadata{"NA", "NA", "NA", "", ""} // batman - - if info.Transport != "" { - if info.Transport == "http" { - m.Scheme = info.HTTP.Version - } else { - m.Scheme = info.Transport - } - } - if info.RemoteAddr != "" { - m.Remote = info.RemoteAddr - } - if info.HTTP.Host != "" { - m.Local = info.HTTP.Host - } - m.Origin = info.HTTP.Origin - m.UserAgent = info.HTTP.UserAgent - return m -} - -// String implements Stringer interface -func (m Metadata) String() string { - s, err := json.Marshal(m) - if err == nil { - return string(s) - } - return err.Error() -} - -// types for the requests/response types between signer and UI -type ( - // SignTxRequest contains info about a Transaction to sign - SignTxRequest struct { - Transaction apitypes.SendTxArgs `json:"transaction"` - Callinfo []apitypes.ValidationInfo `json:"call_info"` - Meta Metadata `json:"meta"` - } - // SignTxResponse result from SignTxRequest - SignTxResponse struct { - //The UI may make changes to the TX - Transaction apitypes.SendTxArgs `json:"transaction"` - Approved bool `json:"approved"` - } - SignDataRequest struct { - ContentType string `json:"content_type"` - Address common.MixedcaseAddress `json:"address"` - Rawdata []byte `json:"raw_data"` - Messages []*apitypes.NameValueType `json:"messages"` - Callinfo []apitypes.ValidationInfo `json:"call_info"` - Hash hexutil.Bytes `json:"hash"` - Meta Metadata `json:"meta"` - } - SignDataResponse struct { - Approved bool `json:"approved"` - } - NewAccountRequest struct { - Meta Metadata `json:"meta"` - } - NewAccountResponse struct { - Approved bool `json:"approved"` - } - ListRequest struct { - Accounts []accounts.Account `json:"accounts"` - Meta Metadata `json:"meta"` - } - ListResponse struct { - Accounts []accounts.Account `json:"accounts"` - } - Message struct { - Text string `json:"text"` - } - StartupInfo struct { - Info map[string]interface{} `json:"info"` - } - UserInputRequest struct { - Title string `json:"title"` - Prompt string `json:"prompt"` - IsPassword bool `json:"isPassword"` - } - UserInputResponse struct { - Text string `json:"text"` - } -) - -var ErrRequestDenied = errors.New("request denied") - -// NewSignerAPI creates a new API that can be used for Account management. -// ksLocation specifies the directory where to store the password protected private -// key that is generated when a new Account is created. -// noUSB disables USB support that is required to support hardware devices such as -// ledger and trezor. -func NewSignerAPI(am *accounts.Manager, chainID int64, ui UIClientAPI, validator Validator, advancedMode bool, credentials storage.Storage) *SignerAPI { - if advancedMode { - log.Info("Clef is in advanced mode: will warn instead of reject") - } - signer := &SignerAPI{big.NewInt(chainID), am, ui, validator, !advancedMode, credentials} - signer.startUSBListener() - return signer -} -func (api *SignerAPI) openTrezor(url accounts.URL) { - resp, err := api.UI.OnInputRequired(UserInputRequest{ - Prompt: "Pin required to open Trezor wallet\n" + - "Look at the device for number positions\n\n" + - "7 | 8 | 9\n" + - "--+---+--\n" + - "4 | 5 | 6\n" + - "--+---+--\n" + - "1 | 2 | 3\n\n", - IsPassword: true, - Title: "Trezor unlock", - }) - if err != nil { - log.Warn("failed getting trezor pin", "err", err) - return - } - // We're using the URL instead of the pointer to the - // Wallet -- perhaps it is not actually present anymore - w, err := api.am.Wallet(url.String()) - if err != nil { - log.Warn("wallet unavailable", "url", url) - return - } - err = w.Open(resp.Text) - if err != nil { - log.Warn("failed to open wallet", "wallet", url, "err", err) - return - } -} - -// startUSBListener starts a listener for USB events, for hardware wallet interaction -func (api *SignerAPI) startUSBListener() { - eventCh := make(chan accounts.WalletEvent, 16) - am := api.am - am.Subscribe(eventCh) - // Open any wallets already attached - for _, wallet := range am.Wallets() { - if err := wallet.Open(""); err != nil { - log.Warn("Failed to open wallet", "url", wallet.URL(), "err", err) - if err == usbwallet.ErrTrezorPINNeeded { - go api.openTrezor(wallet.URL()) - } - } - } - go api.derivationLoop(eventCh) -} - -// derivationLoop listens for wallet events -func (api *SignerAPI) derivationLoop(events chan accounts.WalletEvent) { - // Listen for wallet event till termination - for event := range events { - switch event.Kind { - case accounts.WalletArrived: - if err := event.Wallet.Open(""); err != nil { - log.Warn("New wallet appeared, failed to open", "url", event.Wallet.URL(), "err", err) - if err == usbwallet.ErrTrezorPINNeeded { - go api.openTrezor(event.Wallet.URL()) - } - } - case accounts.WalletOpened: - status, _ := event.Wallet.Status() - log.Info("New wallet appeared", "url", event.Wallet.URL(), "status", status) - var derive = func(limit int, next func() accounts.DerivationPath) { - // Derive first N accounts, hardcoded for now - for i := 0; i < limit; i++ { - path := next() - if acc, err := event.Wallet.Derive(path, true); err != nil { - log.Warn("Account derivation failed", "error", err) - } else { - log.Info("Derived account", "address", acc.Address, "path", path) - } - } - } - log.Info("Deriving default paths") - derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.DefaultBaseDerivationPath)) - if event.Wallet.URL().Scheme == "ledger" { - log.Info("Deriving ledger legacy paths") - derive(numberOfAccountsToDerive, accounts.DefaultIterator(accounts.LegacyLedgerBaseDerivationPath)) - log.Info("Deriving ledger live paths") - // For ledger live, since it's based off the same (DefaultBaseDerivationPath) - // as one we've already used, we need to step it forward one step to avoid - // hitting the same path again - nextFn := accounts.LedgerLiveIterator(accounts.DefaultBaseDerivationPath) - nextFn() - derive(numberOfAccountsToDerive, nextFn) - } - case accounts.WalletDropped: - log.Info("Old wallet dropped", "url", event.Wallet.URL()) - event.Wallet.Close() - } - } -} - -// List returns the set of wallet this signer manages. Each wallet can contain -// multiple accounts. -func (api *SignerAPI) List(ctx context.Context) ([]common.Address, error) { - var accs = make([]accounts.Account, 0) - // accs is initialized as empty list, not nil. We use 'nil' to signal - // rejection, as opposed to an empty list. - for _, wallet := range api.am.Wallets() { - accs = append(accs, wallet.Accounts()...) - } - result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)}) - if err != nil { - return nil, err - } - if result.Accounts == nil { - return nil, ErrRequestDenied - } - addresses := make([]common.Address, 0) - for _, acc := range result.Accounts { - addresses = append(addresses, acc.Address) - } - return addresses, nil -} - -// New creates a new password protected Account. The private key is protected with -// the given password. Users are responsible to backup the private key that is stored -// in the keystore location that was specified when this API was created. -func (api *SignerAPI) New(ctx context.Context) (common.Address, error) { - if be := api.am.Backends(keystore.KeyStoreType); len(be) == 0 { - return common.Address{}, errors.New("password based accounts not supported") - } - if resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)}); err != nil { - return common.Address{}, err - } else if !resp.Approved { - return common.Address{}, ErrRequestDenied - } - return api.newAccount() -} - -// newAccount is the internal method to create a new account. It should be used -// _after_ user-approval has been obtained -func (api *SignerAPI) newAccount() (common.Address, error) { - be := api.am.Backends(keystore.KeyStoreType) - if len(be) == 0 { - return common.Address{}, errors.New("password based accounts not supported") - } - // Three retries to get a valid password - for i := 0; i < 3; i++ { - resp, err := api.UI.OnInputRequired(UserInputRequest{ - "New account password", - fmt.Sprintf("Please enter a password for the new account to be created (attempt %d of 3)", i), - true}) - if err != nil { - log.Warn("error obtaining password", "attempt", i, "error", err) - continue - } - if pwErr := ValidatePasswordFormat(resp.Text); pwErr != nil { - api.UI.ShowError(fmt.Sprintf("Account creation attempt #%d failed due to password requirements: %v", i+1, pwErr)) - } else { - // No error - acc, err := be[0].(*keystore.KeyStore).NewAccount(resp.Text) - log.Info("Your new key was generated", "address", acc.Address) - log.Warn("Please backup your key file!", "path", acc.URL.Path) - log.Warn("Please remember your password!") - return acc.Address, err - } - } - // Otherwise fail, with generic error message - return common.Address{}, errors.New("account creation failed") -} - -// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. -// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow -// UI-modifications to requests -func logDiff(original *SignTxRequest, new *SignTxResponse) bool { - var intPtrModified = func(a, b *hexutil.Big) bool { - aBig := (*big.Int)(a) - bBig := (*big.Int)(b) - if aBig != nil && bBig != nil { - return aBig.Cmp(bBig) != 0 - } - // One or both of them are nil - return a != b - } - - modified := false - if f0, f1 := original.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) { - log.Info("Sender-account changed by UI", "was", f0, "is", f1) - modified = true - } - if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) { - log.Info("Recipient-account changed by UI", "was", t0, "is", t1) - modified = true - } - if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 { - modified = true - log.Info("Gas changed by UI", "was", g0, "is", g1) - } - if a, b := original.Transaction.GasPrice, new.Transaction.GasPrice; intPtrModified(a, b) { - log.Info("GasPrice changed by UI", "was", a, "is", b) - modified = true - } - if a, b := original.Transaction.MaxPriorityFeePerGas, new.Transaction.MaxPriorityFeePerGas; intPtrModified(a, b) { - log.Info("maxPriorityFeePerGas changed by UI", "was", a, "is", b) - modified = true - } - if a, b := original.Transaction.MaxFeePerGas, new.Transaction.MaxFeePerGas; intPtrModified(a, b) { - log.Info("maxFeePerGas changed by UI", "was", a, "is", b) - modified = true - } - if v0, v1 := big.Int(original.Transaction.Value), big.Int(new.Transaction.Value); v0.Cmp(&v1) != 0 { - modified = true - log.Info("Value changed by UI", "was", v0, "is", v1) - } - if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 { - d0s := "" - d1s := "" - if d0 != nil { - d0s = hexutil.Encode(*d0) - } - if d1 != nil { - d1s = hexutil.Encode(*d1) - } - if d1s != d0s { - modified = true - log.Info("Data changed by UI", "was", d0s, "is", d1s) - } - } - if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { - modified = true - log.Info("Nonce changed by UI", "was", n0, "is", n1) - } - return modified -} - -func (api *SignerAPI) lookupPassword(address common.Address) (string, error) { - return api.credentials.Get(address.Hex()) -} - -func (api *SignerAPI) lookupOrQueryPassword(address common.Address, title, prompt string) (string, error) { - // Look up the password and return if available - if pw, err := api.lookupPassword(address); err == nil { - return pw, nil - } - // Password unavailable, request it from the user - pwResp, err := api.UI.OnInputRequired(UserInputRequest{title, prompt, true}) - if err != nil { - log.Warn("error obtaining password", "error", err) - // We'll not forward the error here, in case the error contains info about the response from the UI, - // which could leak the password if it was malformed json or something - return "", errors.New("internal error") - } - return pwResp.Text, nil -} - -// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form -func (api *SignerAPI) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { - var ( - err error - result SignTxResponse - ) - msgs, err := api.validator.ValidateTransaction(methodSelector, &args) - if err != nil { - return nil, err - } - // If we are in 'rejectMode', then reject rather than show the user warnings - if api.rejectMode { - if err := msgs.GetWarnings(); err != nil { - log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.") - return nil, err - } - } - if args.ChainID != nil { - requestedChainId := (*big.Int)(args.ChainID) - if api.chainID.Cmp(requestedChainId) != 0 { - log.Error("Signing request with wrong chain id", "requested", requestedChainId, "configured", api.chainID) - return nil, fmt.Errorf("requested chainid %d does not match the configuration of the signer", - requestedChainId) - } - } - req := SignTxRequest{ - Transaction: args, - Meta: MetadataFromContext(ctx), - Callinfo: msgs.Messages, - } - // Process approval - result, err = api.UI.ApproveTx(&req) - if err != nil { - return nil, err - } - if !result.Approved { - return nil, ErrRequestDenied - } - // Log changes made by the UI to the signing-request - logDiff(&req, &result) - var ( - acc accounts.Account - wallet accounts.Wallet - ) - acc = accounts.Account{Address: result.Transaction.From.Address()} - wallet, err = api.am.Find(acc) - if err != nil { - return nil, err - } - // Convert fields into a real transaction - unsignedTx, err := result.Transaction.ToTransaction() - if err != nil { - return nil, err - } - // Get the password for the transaction - pw, err := api.lookupOrQueryPassword(acc.Address, "Account password", - fmt.Sprintf("Please enter the password for account %s", acc.Address.String())) - if err != nil { - return nil, err - } - // The one to sign is the one that was returned from the UI - signedTx, err := wallet.SignTxWithPassphrase(acc, pw, unsignedTx, api.chainID) - if err != nil { - api.UI.ShowError(err.Error()) - return nil, err - } - - data, err := signedTx.MarshalBinary() - if err != nil { - return nil, err - } - response := ethapi.SignTransactionResult{Raw: data, Tx: signedTx} - - // Finally, send the signed tx to the UI - api.UI.OnApprovedTx(response) - // ...and to the external caller - return &response, nil -} - -func (api *SignerAPI) SignGnosisSafeTx(ctx context.Context, signerAddress common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { - // Do the usual validations, but on the last-stage transaction - args := gnosisTx.ArgsForValidation() - msgs, err := api.validator.ValidateTransaction(methodSelector, args) - if err != nil { - return nil, err - } - // If we are in 'rejectMode', then reject rather than show the user warnings - if api.rejectMode { - if err := msgs.GetWarnings(); err != nil { - log.Info("Signing aborted due to warnings. In order to continue despite warnings, please use the flag '--advanced'.") - return nil, err - } - } - typedData := gnosisTx.ToTypedData() - // might as well error early. - // we are expected to sign. If our calculated hash does not match what they want, - // The gnosis safetx input contains a 'safeTxHash' which is the expected safeTxHash that - sighash, _, err := apitypes.TypedDataAndHash(typedData) - if err != nil { - return nil, err - } - if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) { - // It might be the case that the json is missing chain id. - if gnosisTx.ChainId == nil { - gnosisTx.ChainId = (*math.HexOrDecimal256)(api.chainID) - typedData = gnosisTx.ToTypedData() - sighash, _, _ = apitypes.TypedDataAndHash(typedData) - if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) { - return nil, fmt.Errorf("mismatched safeTxHash; have %#x want %#x", sighash, gnosisTx.InputExpHash[:]) - } - } - } - signature, preimage, err := api.signTypedData(ctx, signerAddress, typedData, msgs) - - if err != nil { - return nil, err - } - checkSummedSender, _ := common.NewMixedcaseAddressFromString(signerAddress.Address().Hex()) - - gnosisTx.Signature = signature - gnosisTx.SafeTxHash = common.BytesToHash(preimage) - gnosisTx.Sender = *checkSummedSender // Must be checksummed to be accepted by relay - - return &gnosisTx, nil -} - -// Version returns the external api version. This method does not require user acceptance. Available methods are -// available via enumeration anyway, and this info does not contain user-specific data -func (api *SignerAPI) Version(ctx context.Context) (string, error) { - return ExternalAPIVersion, nil -} diff --git a/signer/core/api_test.go b/signer/core/api_test.go deleted file mode 100644 index 7c5cefdf8c..0000000000 --- a/signer/core/api_test.go +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core_test - -import ( - "bytes" - "fmt" - "math/big" - "os" - "path/filepath" - "testing" - "time" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/signer/core" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/ethereum/go-ethereum/signer/fourbyte" - "github.com/ethereum/go-ethereum/signer/storage" -) - -// Used for testing -type headlessUi struct { - approveCh chan string // to send approve/deny - inputCh chan string // to send password -} - -func (ui *headlessUi) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { - input := <-ui.inputCh - return core.UserInputResponse{Text: input}, nil -} - -func (ui *headlessUi) OnSignerStartup(info core.StartupInfo) {} -func (ui *headlessUi) RegisterUIServer(api *core.UIServerAPI) {} -func (ui *headlessUi) OnApprovedTx(tx ethapi.SignTransactionResult) {} - -func (ui *headlessUi) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { - switch <-ui.approveCh { - case "Y": - return core.SignTxResponse{request.Transaction, true}, nil - case "M": // modify - // The headless UI always modifies the transaction - old := big.Int(request.Transaction.Value) - newVal := new(big.Int).Add(&old, big.NewInt(1)) - request.Transaction.Value = hexutil.Big(*newVal) - return core.SignTxResponse{request.Transaction, true}, nil - default: - return core.SignTxResponse{request.Transaction, false}, nil - } -} - -func (ui *headlessUi) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { - approved := (<-ui.approveCh == "Y") - return core.SignDataResponse{approved}, nil -} - -func (ui *headlessUi) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { - approval := <-ui.approveCh - //fmt.Printf("approval %s\n", approval) - switch approval { - case "A": - return core.ListResponse{request.Accounts}, nil - case "1": - l := make([]accounts.Account, 1) - l[0] = request.Accounts[1] - return core.ListResponse{l}, nil - default: - return core.ListResponse{nil}, nil - } -} - -func (ui *headlessUi) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { - if <-ui.approveCh == "Y" { - return core.NewAccountResponse{true}, nil - } - return core.NewAccountResponse{false}, nil -} - -func (ui *headlessUi) ShowError(message string) { - // stdout is used by communication - fmt.Fprintln(os.Stderr, message) -} - -func (ui *headlessUi) ShowInfo(message string) { - // stdout is used by communication - fmt.Fprintln(os.Stderr, message) -} - -func tmpDirName(t *testing.T) string { - d := t.TempDir() - d, err := filepath.EvalSymlinks(d) - if err != nil { - t.Fatal(err) - } - return d -} - -func setup(t *testing.T) (*core.SignerAPI, *headlessUi) { - db, err := fourbyte.New() - if err != nil { - t.Fatal(err.Error()) - } - ui := &headlessUi{make(chan string, 20), make(chan string, 20)} - am := core.StartClefAccountManager(tmpDirName(t), true, "") - api := core.NewSignerAPI(am, 1337, ui, db, true, &storage.NoStorage{}) - return api, ui -} -func createAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) { - ui.approveCh <- "Y" - ui.inputCh <- "a_long_password" - _, err := api.New(t.Context()) - if err != nil { - t.Fatal(err) - } - // Some time to allow changes to propagate - time.Sleep(250 * time.Millisecond) -} - -func failCreateAccountWithPassword(ui *headlessUi, api *core.SignerAPI, password string, t *testing.T) { - ui.approveCh <- "Y" - // We will be asked three times to provide a suitable password - ui.inputCh <- password - ui.inputCh <- password - ui.inputCh <- password - - addr, err := api.New(t.Context()) - if err == nil { - t.Fatal("Should have returned an error") - } - if addr != (common.Address{}) { - t.Fatal("Empty address should be returned") - } -} - -func failCreateAccount(ui *headlessUi, api *core.SignerAPI, t *testing.T) { - ui.approveCh <- "N" - addr, err := api.New(t.Context()) - if err != core.ErrRequestDenied { - t.Fatal(err) - } - if addr != (common.Address{}) { - t.Fatal("Empty address should be returned") - } -} - -func list(ui *headlessUi, api *core.SignerAPI, t *testing.T) ([]common.Address, error) { - ui.approveCh <- "A" - return api.List(t.Context()) -} - -func TestNewAcc(t *testing.T) { - t.Parallel() - api, control := setup(t) - verifyNum := func(num int) { - list, err := list(control, api, t) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - if len(list) != num { - t.Errorf("Expected %d accounts, got %d", num, len(list)) - } - } - // Testing create and create-deny - createAccount(control, api, t) - createAccount(control, api, t) - failCreateAccount(control, api, t) - failCreateAccount(control, api, t) - createAccount(control, api, t) - failCreateAccount(control, api, t) - createAccount(control, api, t) - failCreateAccount(control, api, t) - verifyNum(4) - - // Fail to create this, due to bad password - failCreateAccountWithPassword(control, api, "short", t) - failCreateAccountWithPassword(control, api, "longerbutbad\rfoo", t) - verifyNum(4) - - // Testing listing: - // Listing one Account - control.approveCh <- "1" - list, err := api.List(t.Context()) - if err != nil { - t.Fatal(err) - } - if len(list) != 1 { - t.Fatalf("List should only show one Account") - } - // Listing denied - control.approveCh <- "Nope" - list, err = api.List(t.Context()) - if len(list) != 0 { - t.Fatalf("List should be empty") - } - if err != core.ErrRequestDenied { - t.Fatal("Expected deny") - } -} - -func mkTestTx(from common.MixedcaseAddress) apitypes.SendTxArgs { - to := common.NewMixedcaseAddress(common.HexToAddress("0x1337")) - gas := hexutil.Uint64(21000) - gasPrice := (hexutil.Big)(*big.NewInt(2000000000)) - value := (hexutil.Big)(*big.NewInt(1e18)) - nonce := (hexutil.Uint64)(0) - data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a")) - tx := apitypes.SendTxArgs{ - From: from, - To: &to, - Gas: gas, - GasPrice: &gasPrice, - Value: value, - Data: &data, - Nonce: nonce} - return tx -} - -func TestSignTx(t *testing.T) { - t.Parallel() - var ( - list []common.Address - res, res2 *ethapi.SignTransactionResult - err error - ) - - api, control := setup(t) - createAccount(control, api, t) - control.approveCh <- "A" - list, err = api.List(t.Context()) - if err != nil { - t.Fatal(err) - } - if len(list) == 0 { - t.Fatal("Unexpected empty list") - } - a := common.NewMixedcaseAddress(list[0]) - - methodSig := "test(uint)" - tx := mkTestTx(a) - - control.approveCh <- "Y" - control.inputCh <- "wrongpassword" - res, err = api.SignTransaction(t.Context(), tx, &methodSig) - if res != nil { - t.Errorf("Expected nil-response, got %v", res) - } - if err != keystore.ErrDecrypt { - t.Errorf("Expected ErrDecrypt! %v", err) - } - control.approveCh <- "No way" - res, err = api.SignTransaction(t.Context(), tx, &methodSig) - if res != nil { - t.Errorf("Expected nil-response, got %v", res) - } - if err != core.ErrRequestDenied { - t.Errorf("Expected ErrRequestDenied! %v", err) - } - // Sign with correct password - control.approveCh <- "Y" - control.inputCh <- "a_long_password" - res, err = api.SignTransaction(t.Context(), tx, &methodSig) - if err != nil { - t.Fatal(err) - } - parsedTx := &types.Transaction{} - rlp.DecodeBytes(res.Raw, parsedTx) - - // The tx should NOT be modified by the UI - if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 { - t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value()) - } - control.approveCh <- "Y" - control.inputCh <- "a_long_password" - - res2, err = api.SignTransaction(t.Context(), tx, &methodSig) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(res.Raw, res2.Raw) { - t.Error("Expected tx to be unmodified by UI") - } - - // The tx is modified by the UI - control.approveCh <- "M" - control.inputCh <- "a_long_password" - - res2, err = api.SignTransaction(t.Context(), tx, &methodSig) - if err != nil { - t.Fatal(err) - } - parsedTx2 := &types.Transaction{} - rlp.DecodeBytes(res2.Raw, parsedTx2) - - // The tx should be modified by the UI - if parsedTx2.Value().Cmp(tx.Value.ToInt()) == 0 { - t.Errorf("Expected value to be changed, got %v", parsedTx2.Value()) - } - if bytes.Equal(res.Raw, res2.Raw) { - t.Error("Expected tx to be modified by UI") - } -} diff --git a/signer/core/apitypes/types_test.go b/signer/core/apitypes/types_test.go index f010cef207..2d60383b32 100644 --- a/signer/core/apitypes/types_test.go +++ b/signer/core/apitypes/types_test.go @@ -97,16 +97,6 @@ func TestTxArgs(t *testing.T) { t.Errorf("test %d: have %v, want %v", i, have, want) } } - /* - End to end testing: - - $ go run ./cmd/clef --advanced --suppress-bootwarn - - $ go run ./cmd/geth --nodiscover --maxpeers 0 --signer /home/user/.clef/clef.ipc console - - > tx={"from":"0x1b442286e32ddcaa6e2570ce9ed85f4b4fc87425","to":"0x1b442286e32ddcaa6e2570ce9ed85f4b4fc87425","gas":"0x124f8","maxFeePerGas":"0x6fc23ac00","maxPriorityFeePerGas":"0x3b9aca00","value":"0x0","nonce":"0x0","input":"0x","accessList":[],"maxFeePerBlobGas":"0x3b9aca00","blobVersionedHashes":["0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014"]} - > eth.signTransaction(tx) - */ } func TestBlobTxs(t *testing.T) { diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go deleted file mode 100644 index 78785a3b02..0000000000 --- a/signer/core/auditlog.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "context" - "encoding/json" - "log/slog" - "os" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/signer/core/apitypes" -) - -type AuditLogger struct { - log log.Logger - api ExternalAPI -} - -func (l *AuditLogger) List(ctx context.Context) ([]common.Address, error) { - l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String()) - res, e := l.api.List(ctx) - l.log.Info("List", "type", "response", "data", res) - - return res, e -} - -func (l *AuditLogger) New(ctx context.Context) (common.Address, error) { - return l.api.New(ctx) -} - -func (l *AuditLogger) SignTransaction(ctx context.Context, args apitypes.SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) { - sel := "" - if methodSelector != nil { - sel = *methodSelector - } - l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "tx", args.String(), - "methodSelector", sel) - - res, e := l.api.SignTransaction(ctx, args, methodSelector) - if res != nil { - l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e) - } else { - l.log.Info("SignTransaction", "type", "response", "data", res, "error", e) - } - return res, e -} - -func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { - marshalledData, _ := json.Marshal(data) // can ignore error, marshalling what we just unmarshalled - l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "addr", addr.String(), "data", marshalledData, "content-type", contentType) - b, e := l.api.SignData(ctx, contentType, addr, data) - l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e) - return b, e -} - -func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.MixedcaseAddress, gnosisTx GnosisSafeTx, methodSelector *string) (*GnosisSafeTx, error) { - sel := "" - if methodSelector != nil { - sel = *methodSelector - } - data, _ := json.Marshal(gnosisTx) // can ignore error, marshalling what we just unmarshalled - l.log.Info("SignGnosisSafeTx", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "addr", addr.String(), "data", string(data), "selector", sel) - res, e := l.api.SignGnosisSafeTx(ctx, addr, gnosisTx, methodSelector) - if res != nil { - data, _ := json.Marshal(res) // can ignore error, marshalling what we just unmarshalled - l.log.Info("SignGnosisSafeTx", "type", "response", "data", string(data), "error", e) - } else { - l.log.Info("SignGnosisSafeTx", "type", "response", "data", res, "error", e) - } - return res, e -} - -func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error) { - l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "addr", addr.String(), "data", data) - b, e := l.api.SignTypedData(ctx, addr, data) - l.log.Info("SignTypedData", "type", "response", "data", common.Bytes2Hex(b), "error", e) - return b, e -} - -func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { - l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(), - "data", common.Bytes2Hex(data), "sig", common.Bytes2Hex(sig)) - b, e := l.api.EcRecover(ctx, data, sig) - l.log.Info("EcRecover", "type", "response", "address", b.String(), "error", e) - return b, e -} - -func (l *AuditLogger) Version(ctx context.Context) (string, error) { - l.log.Info("Version", "type", "request", "metadata", MetadataFromContext(ctx).String()) - data, err := l.api.Version(ctx) - l.log.Info("Version", "type", "response", "data", data, "error", err) - return data, err -} - -func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) { - f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - - handler := slog.NewTextHandler(f, nil) - l := log.NewLogger(handler).With("api", "signer") - l.Info("Configured", "audit log", path) - return &AuditLogger{l, api}, nil -} diff --git a/signer/core/cliui.go b/signer/core/cliui.go deleted file mode 100644 index e04077865d..0000000000 --- a/signer/core/cliui.go +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "os" - "strings" - "sync" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/console/prompt" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/log" -) - -type CommandlineUI struct { - in *bufio.Reader - mu sync.Mutex - api *UIServerAPI -} - -func NewCommandlineUI() *CommandlineUI { - return &CommandlineUI{in: bufio.NewReader(os.Stdin)} -} - -func (ui *CommandlineUI) RegisterUIServer(api *UIServerAPI) { - ui.api = api -} - -// readString reads a single line from stdin, trimming if from spaces, enforcing -// non-emptyness. -func (ui *CommandlineUI) readString() string { - for { - fmt.Printf("> ") - text, err := ui.in.ReadString('\n') - if err != nil { - log.Crit("Failed to read user input", "err", err) - } - if text = strings.TrimSpace(text); text != "" { - return text - } - } -} - -func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { - fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt) - defer fmt.Println("-----------------------") - if info.IsPassword { - text, err := prompt.Stdin.PromptPassword("> ") - if err != nil { - log.Error("Failed to read password", "error", err) - return UserInputResponse{}, err - } - return UserInputResponse{text}, nil - } - text := ui.readString() - return UserInputResponse{text}, nil -} - -// confirm returns true if user enters 'Yes', otherwise false -func (ui *CommandlineUI) confirm() bool { - fmt.Printf("Approve? [y/N]:\n") - if ui.readString() == "y" { - return true - } - fmt.Println("-----------------------") - return false -} - -// sanitize quotes and truncates 'txt' if longer than 'limit'. If truncated, -// and ellipsis is added after the quoted string -func sanitize(txt string, limit int) string { - if len(txt) > limit { - return fmt.Sprintf("%q...", txt[:limit]) - } - return fmt.Sprintf("%q", txt) -} - -func showMetadata(metadata Metadata) { - fmt.Printf("Request context:\n\t%v -> %v -> %v\n", metadata.Remote, metadata.Scheme, metadata.Local) - fmt.Printf("\nAdditional HTTP header data, provided by the external caller:\n") - fmt.Printf("\tUser-Agent: %v\n\tOrigin: %v\n", sanitize(metadata.UserAgent, 200), sanitize(metadata.Origin, 100)) -} - -// ApproveTx prompt the user for confirmation to request to sign Transaction -func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { - ui.mu.Lock() - defer ui.mu.Unlock() - weival := request.Transaction.Value.ToInt() - fmt.Printf("--------- Transaction request-------------\n") - if to := request.Transaction.To; to != nil { - fmt.Printf("to: %v\n", to.Original()) - if !to.ValidChecksum() { - fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n") - } - } else { - fmt.Printf("to: \n") - } - fmt.Printf("from: %v\n", request.Transaction.From.String()) - fmt.Printf("value: %v wei\n", weival) - fmt.Printf("gas: %v (%v)\n", request.Transaction.Gas, uint64(request.Transaction.Gas)) - if request.Transaction.MaxFeePerGas != nil { - fmt.Printf("maxFeePerGas: %v wei\n", request.Transaction.MaxFeePerGas.ToInt()) - fmt.Printf("maxPriorityFeePerGas: %v wei\n", request.Transaction.MaxPriorityFeePerGas.ToInt()) - } else { - fmt.Printf("gasprice: %v wei\n", request.Transaction.GasPrice.ToInt()) - } - fmt.Printf("nonce: %v (%v)\n", request.Transaction.Nonce, uint64(request.Transaction.Nonce)) - if chainId := request.Transaction.ChainID; chainId != nil { - fmt.Printf("chainid: %v\n", chainId) - } - if list := request.Transaction.AccessList; list != nil { - fmt.Printf("Accesslist:\n") - for i, el := range *list { - fmt.Printf(" %d. %v\n", i, el.Address) - for j, slot := range el.StorageKeys { - fmt.Printf(" %d. %v\n", j, slot) - } - } - } - if len(request.Transaction.BlobHashes) > 0 { - fmt.Printf("Blob hashes:\n") - for _, bh := range request.Transaction.BlobHashes { - fmt.Printf(" %v\n", bh) - } - } - if request.Transaction.Data != nil { - d := *request.Transaction.Data - if len(d) > 0 { - fmt.Printf("data: %v\n", hexutil.Encode(d)) - } - } - if request.Callinfo != nil { - fmt.Printf("\nTransaction validation:\n") - for _, m := range request.Callinfo { - fmt.Printf(" * %s : %s\n", m.Typ, m.Message) - } - fmt.Println() - } - fmt.Printf("\n") - showMetadata(request.Meta) - fmt.Printf("-------------------------------------------\n") - if !ui.confirm() { - return SignTxResponse{request.Transaction, false}, nil - } - return SignTxResponse{request.Transaction, true}, nil -} - -// ApproveSignData prompt the user for confirmation to request to sign data -func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { - ui.mu.Lock() - defer ui.mu.Unlock() - - fmt.Printf("-------- Sign data request--------------\n") - fmt.Printf("Account: %s\n", request.Address.String()) - if len(request.Callinfo) != 0 { - fmt.Printf("\nValidation messages:\n") - for _, m := range request.Callinfo { - fmt.Printf(" * %s : %s\n", m.Typ, m.Message) - } - fmt.Println() - } - fmt.Printf("messages:\n") - for _, nvt := range request.Messages { - fmt.Printf("\u00a0\u00a0%v\n", strings.TrimSpace(nvt.Pprint(1))) - } - fmt.Printf("raw data: \n\t%q\n", request.Rawdata) - fmt.Printf("data hash: %v\n", request.Hash) - fmt.Printf("-------------------------------------------\n") - showMetadata(request.Meta) - if !ui.confirm() { - return SignDataResponse{false}, nil - } - return SignDataResponse{true}, nil -} - -// ApproveListing prompt the user for confirmation to list accounts -// the list of accounts to list can be modified by the UI -func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) { - ui.mu.Lock() - defer ui.mu.Unlock() - - fmt.Printf("-------- List Account request--------------\n") - fmt.Printf("A request has been made to list all accounts. \n") - fmt.Printf("You can select which accounts the caller can see\n") - for _, account := range request.Accounts { - fmt.Printf(" [x] %v\n", account.Address.Hex()) - fmt.Printf(" URL: %v\n", account.URL) - } - fmt.Printf("-------------------------------------------\n") - showMetadata(request.Meta) - if !ui.confirm() { - return ListResponse{nil}, nil - } - return ListResponse{request.Accounts}, nil -} - -// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller -func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { - ui.mu.Lock() - defer ui.mu.Unlock() - - fmt.Printf("-------- New Account request--------------\n\n") - fmt.Printf("A request has been made to create a new account. \n") - fmt.Printf("Approving this operation means that a new account is created,\n") - fmt.Printf("and the address is returned to the external caller\n\n") - showMetadata(request.Meta) - if !ui.confirm() { - return NewAccountResponse{false}, nil - } - return NewAccountResponse{true}, nil -} - -// ShowError displays error message to user -func (ui *CommandlineUI) ShowError(message string) { - fmt.Printf("## Error \n%s\n", message) - fmt.Printf("-------------------------------------------\n") -} - -// ShowInfo displays info message to user -func (ui *CommandlineUI) ShowInfo(message string) { - fmt.Printf("## Info \n%s\n", message) -} - -func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) { - fmt.Printf("Transaction signed:\n ") - if jsn, err := json.MarshalIndent(tx.Tx, " ", " "); err != nil { - fmt.Printf("WARN: marshalling error %v\n", err) - } else { - fmt.Println(string(jsn)) - } -} - -func (ui *CommandlineUI) showAccounts() { - accounts, err := ui.api.ListAccounts(context.Background()) - if err != nil { - log.Error("Error listing accounts", "err", err) - return - } - if len(accounts) == 0 { - fmt.Print("No accounts found\n") - return - } - var msg string - var out = new(strings.Builder) - if limit := 20; len(accounts) > limit { - msg = fmt.Sprintf("\nFirst %d accounts listed (%d more available).\n", limit, len(accounts)-limit) - accounts = accounts[:limit] - } - fmt.Fprint(out, "\n------- Available accounts -------\n") - for i, account := range accounts { - fmt.Fprintf(out, "%d. %s at %s\n", i, account.Address, account.URL) - } - fmt.Print(out.String(), msg) -} - -func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) { - fmt.Print("\n------- Signer info -------\n") - for k, v := range info.Info { - fmt.Printf("* %v : %v\n", k, v) - } - go ui.showAccounts() -} diff --git a/signer/core/gnosis_safe.go b/signer/core/gnosis_safe.go deleted file mode 100644 index 01724e5383..0000000000 --- a/signer/core/gnosis_safe.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2020 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/signer/core/apitypes" -) - -// GnosisSafeTx is a type to parse the safe-tx returned by the relayer, -// it also conforms to the API required by the Gnosis Safe tx relay service. -// See 'SafeMultisigTransaction' on https://safe-transaction.mainnet.gnosis.io/ -type GnosisSafeTx struct { - // These fields are only used on output - Signature hexutil.Bytes `json:"signature"` - SafeTxHash common.Hash `json:"contractTransactionHash"` - Sender common.MixedcaseAddress `json:"sender"` - // These fields are used both on input and output - Safe common.MixedcaseAddress `json:"safe"` - To common.MixedcaseAddress `json:"to"` - Value math.Decimal256 `json:"value"` - GasPrice math.Decimal256 `json:"gasPrice"` - Data *hexutil.Bytes `json:"data"` - Operation uint8 `json:"operation"` - GasToken common.Address `json:"gasToken"` - RefundReceiver common.Address `json:"refundReceiver"` - BaseGas big.Int `json:"baseGas"` - SafeTxGas big.Int `json:"safeTxGas"` - Nonce big.Int `json:"nonce"` - InputExpHash common.Hash `json:"safeTxHash"` - ChainId *math.HexOrDecimal256 `json:"chainId,omitempty"` -} - -// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing -func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData { - var data hexutil.Bytes - if tx.Data != nil { - data = *tx.Data - } - var domainType = []apitypes.Type{{Name: "verifyingContract", Type: "address"}} - if tx.ChainId != nil { - domainType = append([]apitypes.Type{{Name: "chainId", Type: "uint256"}}, domainType[0]) - } - - gnosisTypedData := apitypes.TypedData{ - Types: apitypes.Types{ - "EIP712Domain": domainType, - "SafeTx": []apitypes.Type{ - {Name: "to", Type: "address"}, - {Name: "value", Type: "uint256"}, - {Name: "data", Type: "bytes"}, - {Name: "operation", Type: "uint8"}, - {Name: "safeTxGas", Type: "uint256"}, - {Name: "baseGas", Type: "uint256"}, - {Name: "gasPrice", Type: "uint256"}, - {Name: "gasToken", Type: "address"}, - {Name: "refundReceiver", Type: "address"}, - {Name: "nonce", Type: "uint256"}, - }, - }, - Domain: apitypes.TypedDataDomain{ - VerifyingContract: tx.Safe.Address().Hex(), - ChainId: tx.ChainId, - }, - PrimaryType: "SafeTx", - Message: apitypes.TypedDataMessage{ - "to": tx.To.Address().Hex(), - "value": tx.Value.String(), - "data": data, - "operation": fmt.Sprintf("%d", tx.Operation), - "safeTxGas": fmt.Sprintf("%#d", &tx.SafeTxGas), - "baseGas": fmt.Sprintf("%#d", &tx.BaseGas), - "gasPrice": tx.GasPrice.String(), - "gasToken": tx.GasToken.Hex(), - "refundReceiver": tx.RefundReceiver.Hex(), - "nonce": fmt.Sprintf("%d", tx.Nonce.Uint64()), - }, - } - return gnosisTypedData -} - -// ArgsForValidation returns a SendTxArgs struct, which can be used for the -// common validations, e.g. look up 4byte destinations -func (tx *GnosisSafeTx) ArgsForValidation() *apitypes.SendTxArgs { - gp := hexutil.Big(tx.GasPrice) - args := &apitypes.SendTxArgs{ - From: tx.Safe, - To: &tx.To, - Gas: hexutil.Uint64(tx.SafeTxGas.Uint64()), - GasPrice: &gp, - Value: hexutil.Big(tx.Value), - Nonce: hexutil.Uint64(tx.Nonce.Uint64()), - Data: tx.Data, - Input: nil, - ChainID: (*hexutil.Big)(tx.ChainId), - } - return args -} diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go deleted file mode 100644 index d8b6ef0674..0000000000 --- a/signer/core/signed_data.go +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "mime" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/consensus/clique" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/signer/core/apitypes" -) - -// sign receives a request and produces a signature -// -// Note, the produced signature conforms to the secp256k1 curve R, S and V values, -// where the V value will be 27 or 28 for legacy reasons, if legacyV==true. -func (api *SignerAPI) sign(req *SignDataRequest, legacyV bool) (hexutil.Bytes, error) { - // We make the request prior to looking up if we actually have the account, to prevent - // account-enumeration via the API - res, err := api.UI.ApproveSignData(req) - if err != nil { - return nil, err - } - if !res.Approved { - return nil, ErrRequestDenied - } - // Look up the wallet containing the requested signer - account := accounts.Account{Address: req.Address.Address()} - wallet, err := api.am.Find(account) - if err != nil { - return nil, err - } - pw, err := api.lookupOrQueryPassword(account.Address, - "Password for signing", - fmt.Sprintf("Please enter password for signing data with account %s", account.Address.Hex())) - if err != nil { - return nil, err - } - // Sign the data with the wallet - signature, err := wallet.SignDataWithPassphrase(account, pw, req.ContentType, req.Rawdata) - if err != nil { - return nil, err - } - if legacyV { - signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - } - return signature, nil -} - -// SignData signs the hash of the provided data, but does so differently -// depending on the content-type specified. -// -// Different types of validation occur. -func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) { - var req, transformV, err = api.determineSignatureFormat(ctx, contentType, addr, data) - if err != nil { - return nil, err - } - signature, err := api.sign(req, transformV) - if err != nil { - api.UI.ShowError(err.Error()) - return nil, err - } - return signature, nil -} - -// determineSignatureFormat determines which signature method should be used based upon the mime type -// In the cases where it matters ensure that the charset is handled. The charset -// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType -// charset, ok := params["charset"] -// As it is now, we accept any charset and just treat it as 'raw'. -// This method returns the mimetype for signing along with the request -func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, bool, error) { - var ( - req *SignDataRequest - useEthereumV = true // Default to use V = 27 or 28, the legacy Ethereum format - ) - mediaType, _, err := mime.ParseMediaType(contentType) - if err != nil { - return nil, useEthereumV, err - } - - switch mediaType { - case apitypes.IntendedValidator.Mime: - // Data with an intended validator - validatorData, err := UnmarshalValidatorData(data) - if err != nil { - return nil, useEthereumV, err - } - sighash, msg := SignTextValidator(validatorData) - messages := []*apitypes.NameValueType{ - { - Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", - Typ: "description", - Value: "", - }, - { - Name: "Intended validator address", - Typ: "address", - Value: validatorData.Address.String(), - }, - { - Name: "Application-specific data", - Typ: "hexdata", - Value: validatorData.Message, - }, - { - Name: "Full message for signing", - Typ: "hexdata", - Value: fmt.Sprintf("%#x", msg), - }, - } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} - case apitypes.ApplicationClique.Mime: - // Clique is the Ethereum PoA standard - cliqueData, err := fromHex(data) - if err != nil { - return nil, useEthereumV, err - } - header := &types.Header{} - if err := rlp.DecodeBytes(cliqueData, header); err != nil { - return nil, useEthereumV, err - } - // Add space in the extradata to put the signature - newExtra := make([]byte, len(header.Extra)+65) - copy(newExtra, header.Extra) - header.Extra = newExtra - - // Get back the rlp data, encoded by us - sighash, cliqueRlp, err := cliqueHeaderHashAndRlp(header) - if err != nil { - return nil, useEthereumV, err - } - messages := []*apitypes.NameValueType{ - { - Name: "Clique header", - Typ: "clique", - Value: fmt.Sprintf("clique header %d [%#x]", header.Number, header.Hash()), - }, - } - // Clique uses V on the form 0 or 1 - useEthereumV = false - req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash} - case apitypes.DataTyped.Mime: - // EIP-712 conformant typed data - var err error - req, err = typedDataRequest(data) - if err != nil { - return nil, useEthereumV, err - } - default: // also case TextPlain.Mime: - // Calculates an Ethereum ECDSA signature for: - // hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}") - // We expect input to be a hex-encoded string - textData, err := fromHex(data) - if err != nil { - return nil, useEthereumV, err - } - sighash, msg := accounts.TextAndHash(textData) - messages := []*apitypes.NameValueType{ - { - Name: "message", - Typ: accounts.MimetypeTextPlain, - Value: msg, - }, - } - req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} - } - req.Address = addr - req.Meta = MetadataFromContext(ctx) - return req, useEthereumV, nil -} - -// SignTextValidator signs the given message which can be further recovered -// with the given validator. -// hash = keccak256("\x19\x00"${address}${data}). -func SignTextValidator(validatorData apitypes.ValidatorData) (hexutil.Bytes, string) { - msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message)) - return crypto.Keccak256([]byte(msg)), msg -} - -// cliqueHeaderHashAndRlp returns the hash which is used as input for the proof-of-authority -// signing. It is the hash of the entire header apart from the 65 byte signature -// contained at the end of the extra data. -// -// The method requires the extra data to be at least 65 bytes -- the original implementation -// in clique.go panics if this is the case, thus it's been reimplemented here to avoid the panic -// and simply return an error instead -func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error) { - if len(header.Extra) < 65 { - err = fmt.Errorf("clique header extradata too short, %d < 65", len(header.Extra)) - return - } - rlp = clique.CliqueRLP(header) - hash = clique.SealHash(header).Bytes() - return hash, rlp, err -} - -// SignTypedData signs EIP-712 conformant typed data -// hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}") -// It returns -// - the signature, -// - and/or any error -func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData apitypes.TypedData) (hexutil.Bytes, error) { - signature, _, err := api.signTypedData(ctx, addr, typedData, nil) - return signature, err -} - -// signTypedData is identical to the capitalized version, except that it also returns the hash (preimage) -// - the signature preimage (hash) -func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress, - typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) { - req, err := typedDataRequest(typedData) - if err != nil { - return nil, nil, err - } - req.Address = addr - req.Meta = MetadataFromContext(ctx) - if validationMessages != nil { - req.Callinfo = validationMessages.Messages - } - signature, err := api.sign(req, true) - if err != nil { - api.UI.ShowError(err.Error()) - return nil, nil, err - } - return signature, req.Hash, nil -} - -// fromHex tries to interpret the data as type string, and convert from -// hexadecimal to []byte -func fromHex(data any) ([]byte, error) { - if stringData, ok := data.(string); ok { - binary, err := hexutil.Decode(stringData) - return binary, err - } - return nil, fmt.Errorf("wrong type %T", data) -} - -// typedDataRequest tries to convert the data into a SignDataRequest. -func typedDataRequest(data any) (*SignDataRequest, error) { - var typedData apitypes.TypedData - if td, ok := data.(apitypes.TypedData); ok { - typedData = td - } else { // Hex-encoded data - jsonData, err := fromHex(data) - if err != nil { - return nil, err - } - if err = json.Unmarshal(jsonData, &typedData); err != nil { - return nil, err - } - } - messages, err := typedData.Format() - if err != nil { - return nil, err - } - sighash, rawData, err := apitypes.TypedDataAndHash(typedData) - if err != nil { - return nil, err - } - return &SignDataRequest{ - ContentType: apitypes.DataTyped.Mime, - Rawdata: []byte(rawData), - Messages: messages, - Hash: sighash}, nil -} - -// EcRecover recovers the address associated with the given sig. -// Only compatible with `text/plain` -func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { - // Returns the address for the Account that was used to create the signature. - // - // Note, this function is compatible with eth_sign. As such it recovers - // the address of: - // hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}") - // addr = ecrecover(hash, signature) - // - // Note, the signature must conform to the secp256k1 curve R, S and V values, where - // the V value must be 27 or 28 for legacy reasons. - // - // https://geth.ethereum.org/docs/tools/clef/apis#account-ecrecover - if len(sig) != 65 { - return common.Address{}, errors.New("signature must be 65 bytes long") - } - if sig[64] != 27 && sig[64] != 28 { - return common.Address{}, errors.New("invalid Ethereum signature (V is not 27 or 28)") - } - sig = bytes.Clone(sig) // Avoid mutating the input - sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 - hash := accounts.TextHash(data) - rpk, err := crypto.SigToPub(hash, sig) - if err != nil { - return common.Address{}, err - } - return crypto.PubkeyToAddress(*rpk), nil -} - -// UnmarshalValidatorData converts the bytes input to typed data -func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) { - raw, ok := data.(map[string]interface{}) - if !ok { - return apitypes.ValidatorData{}, errors.New("validator input is not a map[string]interface{}") - } - addrBytes, err := fromHex(raw["address"]) - if err != nil { - return apitypes.ValidatorData{}, fmt.Errorf("validator address error: %w", err) - } - if len(addrBytes) == 0 { - return apitypes.ValidatorData{}, errors.New("validator address is undefined") - } - messageBytes, err := fromHex(raw["message"]) - if err != nil { - return apitypes.ValidatorData{}, fmt.Errorf("message error: %w", err) - } - if len(messageBytes) == 0 { - return apitypes.ValidatorData{}, errors.New("message is undefined") - } - return apitypes.ValidatorData{ - Address: common.BytesToAddress(addrBytes), - Message: messageBytes, - }, nil -} diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go deleted file mode 100644 index 8455aaf9c5..0000000000 --- a/signer/core/signed_data_test.go +++ /dev/null @@ -1,1062 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core_test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "math/big" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/signer/core" - "github.com/ethereum/go-ethereum/signer/core/apitypes" -) - -var typesStandard = apitypes.Types{ - "EIP712Domain": { - { - Name: "name", - Type: "string", - }, - { - Name: "version", - Type: "string", - }, - { - Name: "chainId", - Type: "uint256", - }, - { - Name: "verifyingContract", - Type: "address", - }, - }, - "Person": { - { - Name: "name", - Type: "string", - }, - { - Name: "wallet", - Type: "address", - }, - }, - "Mail": { - { - Name: "from", - Type: "Person", - }, - { - Name: "to", - Type: "Person", - }, - { - Name: "contents", - Type: "string", - }, - }, -} - -var jsonTypedData = ` - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "test", - "type": "uint8" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "test": 3, - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } -` - -const primaryType = "Mail" - -var domainStandard = apitypes.TypedDataDomain{ - Name: "Ether Mail", - Version: "1", - ChainId: math.NewHexOrDecimal256(1), - VerifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", - Salt: "", -} - -var messageStandard = map[string]interface{}{ - "from": map[string]interface{}{ - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", - }, - "to": map[string]interface{}{ - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", - }, - "contents": "Hello, Bob!", -} - -var typedData = apitypes.TypedData{ - Types: typesStandard, - PrimaryType: primaryType, - Domain: domainStandard, - Message: messageStandard, -} - -func TestSignData(t *testing.T) { - t.Parallel() - api, control := setup(t) - //Create two accounts - createAccount(control, api, t) - createAccount(control, api, t) - control.approveCh <- "1" - list, err := api.List(context.Background()) - if err != nil { - t.Fatal(err) - } - a := common.NewMixedcaseAddress(list[0]) - - control.approveCh <- "Y" - control.inputCh <- "wrongpassword" - signature, err := api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) - if signature != nil { - t.Errorf("Expected nil-data, got %x", signature) - } - if err != keystore.ErrDecrypt { - t.Errorf("Expected ErrDecrypt! '%v'", err) - } - control.approveCh <- "No way" - signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) - if signature != nil { - t.Errorf("Expected nil-data, got %x", signature) - } - if err != core.ErrRequestDenied { - t.Errorf("Expected ErrRequestDenied! '%v'", err) - } - // text/plain - control.approveCh <- "Y" - control.inputCh <- "a_long_password" - signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) - if err != nil { - t.Fatal(err) - } - if signature == nil || len(signature) != 65 { - t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) - } - // data/typed via SignTypeData - control.approveCh <- "Y" - control.inputCh <- "a_long_password" - var want []byte - if signature, err = api.SignTypedData(context.Background(), a, typedData); err != nil { - t.Fatal(err) - } else if signature == nil || len(signature) != 65 { - t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) - } else { - want = signature - } - - // data/typed via SignData / mimetype typed data - control.approveCh <- "Y" - control.inputCh <- "a_long_password" - if typedDataJson, err := json.Marshal(typedData); err != nil { - t.Fatal(err) - } else if signature, err = api.SignData(context.Background(), apitypes.DataTyped.Mime, a, hexutil.Encode(typedDataJson)); err != nil { - t.Fatal(err) - } else if signature == nil || len(signature) != 65 { - t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) - } else if have := signature; !bytes.Equal(have, want) { - t.Fatalf("want %x, have %x", want, have) - } -} - -func TestDomainChainId(t *testing.T) { - t.Parallel() - withoutChainID := apitypes.TypedData{ - Types: apitypes.Types{ - "EIP712Domain": []apitypes.Type{ - {Name: "name", Type: "string"}, - }, - }, - Domain: apitypes.TypedDataDomain{ - Name: "test", - }, - } - - if _, ok := withoutChainID.Domain.Map()["chainId"]; ok { - t.Errorf("Expected the chainId key to not be present in the domain map") - } - // should encode successfully - if _, err := withoutChainID.HashStruct("EIP712Domain", withoutChainID.Domain.Map()); err != nil { - t.Errorf("Expected the typedData to encode the domain successfully, got %v", err) - } - withChainID := apitypes.TypedData{ - Types: apitypes.Types{ - "EIP712Domain": []apitypes.Type{ - {Name: "name", Type: "string"}, - {Name: "chainId", Type: "uint256"}, - }, - }, - Domain: apitypes.TypedDataDomain{ - Name: "test", - ChainId: math.NewHexOrDecimal256(1), - }, - } - - if _, ok := withChainID.Domain.Map()["chainId"]; !ok { - t.Errorf("Expected the chainId key be present in the domain map") - } - // should encode successfully - if _, err := withChainID.HashStruct("EIP712Domain", withChainID.Domain.Map()); err != nil { - t.Errorf("Expected the typedData to encode the domain successfully, got %v", err) - } -} - -func TestHashStruct(t *testing.T) { - t.Parallel() - hash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - t.Fatal(err) - } - mainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) - if mainHash != "0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" { - t.Errorf("Expected different hashStruct result (got %s)", mainHash) - } - - hash, err = typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - t.Error(err) - } - domainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) - if domainHash != "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f" { - t.Errorf("Expected different domain hashStruct result (got %s)", domainHash) - } -} - -func TestEncodeType(t *testing.T) { - t.Parallel() - domainTypeEncoding := string(typedData.EncodeType("EIP712Domain")) - if domainTypeEncoding != "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" { - t.Errorf("Expected different encodeType result (got %s)", domainTypeEncoding) - } - - mailTypeEncoding := string(typedData.EncodeType(typedData.PrimaryType)) - if mailTypeEncoding != "Mail(Person from,Person to,string contents)Person(string name,address wallet)" { - t.Errorf("Expected different encodeType result (got %s)", mailTypeEncoding) - } -} - -func TestTypeHash(t *testing.T) { - t.Parallel() - mailTypeHash := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.TypeHash(typedData.PrimaryType))) - if mailTypeHash != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" { - t.Errorf("Expected different typeHash result (got %s)", mailTypeHash) - } -} - -func TestEncodeData(t *testing.T) { - t.Parallel() - hash, err := typedData.EncodeData(typedData.PrimaryType, typedData.Message, 0) - if err != nil { - t.Fatal(err) - } - dataEncoding := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) - if dataEncoding != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" { - t.Errorf("Expected different encodeData result (got %s)", dataEncoding) - } -} - -func TestFormatter(t *testing.T) { - t.Parallel() - var d apitypes.TypedData - err := json.Unmarshal([]byte(jsonTypedData), &d) - if err != nil { - t.Fatalf("unmarshalling failed '%v'", err) - } - formatted, _ := d.Format() - for _, item := range formatted { - t.Logf("'%v'\n", item.Pprint(0)) - } - - j, _ := json.Marshal(formatted) - t.Logf("'%v'\n", string(j)) -} - -func sign(typedData apitypes.TypedData) ([]byte, []byte, error) { - domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - return nil, nil, err - } - typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - return nil, nil, err - } - rawData := fmt.Appendf(nil, "\x19\x01%s%s", string(domainSeparator), string(typedDataHash)) - sighash := crypto.Keccak256(rawData) - return typedDataHash, sighash, nil -} - -func TestJsonFiles(t *testing.T) { - t.Parallel() - testfiles, err := os.ReadDir("testdata/") - if err != nil { - t.Fatalf("failed reading files: %v", err) - } - for i, fInfo := range testfiles { - if !strings.HasSuffix(fInfo.Name(), "json") { - continue - } - expectedFailure := strings.HasPrefix(fInfo.Name(), "expfail") - data, err := os.ReadFile(filepath.Join("testdata", fInfo.Name())) - if err != nil { - t.Errorf("Failed to read file %v: %v", fInfo.Name(), err) - continue - } - var typedData apitypes.TypedData - err = json.Unmarshal(data, &typedData) - if err != nil { - t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err) - continue - } - _, _, err = sign(typedData) - t.Logf("Error %v\n", err) - if err != nil && !expectedFailure { - t.Errorf("Test %d failed, file %v: %v", i, fInfo.Name(), err) - } - if expectedFailure && err == nil { - t.Errorf("Test %d succeeded (expected failure), file %v: %v", i, fInfo.Name(), err) - } - } -} - -// TestFuzzerFiles tests some files that have been found by fuzzing to cause -// crashes or hangs. -func TestFuzzerFiles(t *testing.T) { - t.Parallel() - corpusdir := filepath.Join("testdata", "fuzzing") - testfiles, err := os.ReadDir(corpusdir) - if err != nil { - t.Fatalf("failed reading files: %v", err) - } - verbose := false - for i, fInfo := range testfiles { - data, err := os.ReadFile(filepath.Join(corpusdir, fInfo.Name())) - if err != nil { - t.Errorf("Failed to read file %v: %v", fInfo.Name(), err) - continue - } - var typedData apitypes.TypedData - err = json.Unmarshal(data, &typedData) - if err != nil { - t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err) - continue - } - _, err = typedData.EncodeData("EIP712Domain", typedData.Domain.Map(), 1) - if verbose && err != nil { - t.Logf("%d, EncodeData[1] err: %v\n", i, err) - } - _, err = typedData.EncodeData(typedData.PrimaryType, typedData.Message, 1) - if verbose && err != nil { - t.Logf("%d, EncodeData[2] err: %v\n", i, err) - } - typedData.Format() - } -} - -var gnosisTypedData = ` -{ - "types": { - "EIP712Domain": [ - { "type": "address", "name": "verifyingContract" } - ], - "SafeTx": [ - { "type": "address", "name": "to" }, - { "type": "uint256", "name": "value" }, - { "type": "bytes", "name": "data" }, - { "type": "uint8", "name": "operation" }, - { "type": "uint256", "name": "safeTxGas" }, - { "type": "uint256", "name": "baseGas" }, - { "type": "uint256", "name": "gasPrice" }, - { "type": "address", "name": "gasToken" }, - { "type": "address", "name": "refundReceiver" }, - { "type": "uint256", "name": "nonce" } - ] - }, - "domain": { - "verifyingContract": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3" - }, - "primaryType": "SafeTx", - "message": { - "to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", - "value": "20000000000000000", - "data": "0x", - "operation": 0, - "safeTxGas": 27845, - "baseGas": 0, - "gasPrice": "0", - "gasToken": "0x0000000000000000000000000000000000000000", - "refundReceiver": "0x0000000000000000000000000000000000000000", - "nonce": 3 - } -}` - -var gnosisTx = ` -{ - "safe": "0x25a6c4BBd32B2424A9c99aEB0584Ad12045382B3", - "to": "0x9eE457023bB3De16D51A003a247BaEaD7fce313D", - "value": "20000000000000000", - "data": null, - "operation": 0, - "gasToken": "0x0000000000000000000000000000000000000000", - "safeTxGas": 27845, - "baseGas": 0, - "gasPrice": "0", - "refundReceiver": "0x0000000000000000000000000000000000000000", - "nonce": 3, - "executionDate": null, - "submissionDate": "2020-09-15T21:59:23.815748Z", - "modified": "2020-09-15T21:59:23.815748Z", - "blockNumber": null, - "transactionHash": null, - "safeTxHash": "0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f", - "executor": null, - "isExecuted": false, - "isSuccessful": null, - "ethGasPrice": null, - "gasUsed": null, - "fee": null, - "origin": null, - "dataDecoded": null, - "confirmationsRequired": null, - "confirmations": [ - { - "owner": "0xAd2e180019FCa9e55CADe76E4487F126Fd08DA34", - "submissionDate": "2020-09-15T21:59:28.281243Z", - "transactionHash": null, - "confirmationType": "CONFIRMATION", - "signature": "0x5e562065a0cb15d766dac0cd49eb6d196a41183af302c4ecad45f1a81958d7797753f04424a9b0aa1cb0448e4ec8e189540fbcdda7530ef9b9d95dfc2d36cb521b", - "signatureType": "EOA" - } - ], - "signatures": null - } -` - -// TestGnosisTypedData tests the scenario where a user submits a full EIP-712 -// struct without using the gnosis-specific endpoint -func TestGnosisTypedData(t *testing.T) { - t.Parallel() - var td apitypes.TypedData - err := json.Unmarshal([]byte(gnosisTypedData), &td) - if err != nil { - t.Fatalf("unmarshalling failed '%v'", err) - } - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -// TestGnosisCustomData tests the scenario where a user submits only the gnosis-safe -// specific data, and we fill the TypedData struct on our side -func TestGnosisCustomData(t *testing.T) { - t.Parallel() - var tx core.GnosisSafeTx - err := json.Unmarshal([]byte(gnosisTx), &tx) - if err != nil { - t.Fatal(err) - } - var td = tx.ToTypedData() - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x28bae2bd58d894a1d9b69e5e9fde3570c4b98a6fc5499aefb54fb830137e831f") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -var gnosisTypedDataWithChainId = ` -{ - "types": { - "EIP712Domain": [ - { "type": "uint256", "name": "chainId" }, - { "type": "address", "name": "verifyingContract" } - ], - "SafeTx": [ - { "type": "address", "name": "to" }, - { "type": "uint256", "name": "value" }, - { "type": "bytes", "name": "data" }, - { "type": "uint8", "name": "operation" }, - { "type": "uint256", "name": "safeTxGas" }, - { "type": "uint256", "name": "baseGas" }, - { "type": "uint256", "name": "gasPrice" }, - { "type": "address", "name": "gasToken" }, - { "type": "address", "name": "refundReceiver" }, - { "type": "uint256", "name": "nonce" } - ] - }, - "domain": { - "verifyingContract": "0x111dAE35D176A9607053e0c46e91F36AFbC1dc57", - "chainId": "4" - }, - "primaryType": "SafeTx", - "message": { - "to": "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa", - "value": "0", - "data": "0xa9059cbb00000000000000000000000099d580d3a7fe7bd183b2464517b2cd7ce5a8f15a0000000000000000000000000000000000000000000000000de0b6b3a7640000", - "operation": 0, - "safeTxGas": 0, - "baseGas": 0, - "gasPrice": "0", - "gasToken": "0x0000000000000000000000000000000000000000", - "refundReceiver": "0x0000000000000000000000000000000000000000", - "nonce": 15 - } -}` - -var gnosisTxWithChainId = ` -{ - "safe": "0x111dAE35D176A9607053e0c46e91F36AFbC1dc57", - "to": "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa", - "value": "0", - "data": "0xa9059cbb00000000000000000000000099d580d3a7fe7bd183b2464517b2cd7ce5a8f15a0000000000000000000000000000000000000000000000000de0b6b3a7640000", - "operation": 0, - "gasToken": "0x0000000000000000000000000000000000000000", - "safeTxGas": 0, - "baseGas": 0, - "gasPrice": "0", - "refundReceiver": "0x0000000000000000000000000000000000000000", - "nonce": 15, - "executionDate": "2022-01-10T20:00:12Z", - "submissionDate": "2022-01-10T19:59:59.689989Z", - "modified": "2022-01-10T20:00:31.903635Z", - "blockNumber": 9968802, - "transactionHash": "0xc9fef30499ee8984974ab9dddd9d15c2a97c1a4393935dceed5efc3af9fc41a4", - "safeTxHash": "0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29", - "executor": "0xbc2BB26a6d821e69A38016f3858561a1D80d4182", - "isExecuted": true, - "isSuccessful": true, - "ethGasPrice": "2500000009", - "gasUsed": 82902, - "fee": "207255000746118", - "chainId": "4", - "origin": null, - "dataDecoded": { - "method": "transfer", - "parameters": [ - { - "name": "to", - "type": "address", - "value": "0x99D580d3a7FE7BD183b2464517B2cD7ce5A8F15A" - }, - { - "name": "value", - "type": "uint256", - "value": "1000000000000000000" - } - ] - }, - "confirmationsRequired": 1, - "confirmations": [ - { - "owner": "0xbc2BB26a6d821e69A38016f3858561a1D80d4182", - "submissionDate": "2022-01-10T19:59:59.722500Z", - "transactionHash": null, - "signature": "0x5ca34641bcdee06e7b99143bfe34778195ca41022bd35837b96c204c7786be9d6dfa6dba43b53cd92da45ac728899e1561b232d28f38ba82df45f164caba38be1b", - "signatureType": "EOA" - } - ], - "signatures": "0x5ca34641bcdee06e7b99143bfe34778195ca41022bd35837b96c204c7786be9d6dfa6dba43b53cd92da45ac728899e1561b232d28f38ba82df45f164caba38be1b" -} -` - -func TestGnosisTypedDataWithChainId(t *testing.T) { - t.Parallel() - var td apitypes.TypedData - err := json.Unmarshal([]byte(gnosisTypedDataWithChainId), &td) - if err != nil { - t.Fatalf("unmarshalling failed '%v'", err) - } - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -// TestGnosisCustomDataWithChainId tests the scenario where a user submits only the gnosis-safe -// specific data, and we fill the TypedData struct on our side -func TestGnosisCustomDataWithChainId(t *testing.T) { - t.Parallel() - var tx core.GnosisSafeTx - err := json.Unmarshal([]byte(gnosisTxWithChainId), &tx) - if err != nil { - t.Fatal(err) - } - var td = tx.ToTypedData() - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x6619dab5401503f2735256e12b898e69eb701d6a7e0d07abf1be4bb8aebfba29") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -var complexTypedData = ` -{ - "types": { - "EIP712Domain": [ - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "name", - "type": "string" - }, - { - "name": "verifyingContract", - "type": "address" - }, - { - "name": "version", - "type": "string" - } - ], - "Action": [ - { - "name": "action", - "type": "string" - }, - { - "name": "params", - "type": "string" - } - ], - "Cell": [ - { - "name": "capacity", - "type": "string" - }, - { - "name": "lock", - "type": "string" - }, - { - "name": "type", - "type": "string" - }, - { - "name": "data", - "type": "string" - }, - { - "name": "extraData", - "type": "string" - } - ], - "Transaction": [ - { - "name": "DAS_MESSAGE", - "type": "string" - }, - { - "name": "inputsCapacity", - "type": "string" - }, - { - "name": "outputsCapacity", - "type": "string" - }, - { - "name": "fee", - "type": "string" - }, - { - "name": "action", - "type": "Action" - }, - { - "name": "inputs", - "type": "Cell[]" - }, - { - "name": "outputs", - "type": "Cell[]" - }, - { - "name": "digest", - "type": "bytes32" - } - ] - }, - "primaryType": "Transaction", - "domain": { - "chainId": "56", - "name": "da.systems", - "verifyingContract": "0x0000000000000000000000000000000020210722", - "version": "1" - }, - "message": { - "DAS_MESSAGE": "SELL mobcion.bit FOR 100000 CKB", - "inputsCapacity": "1216.9999 CKB", - "outputsCapacity": "1216.9998 CKB", - "fee": "0.0001 CKB", - "digest": "0x53a6c0f19ec281604607f5d6817e442082ad1882bef0df64d84d3810dae561eb", - "action": { - "action": "start_account_sale", - "params": "0x00" - }, - "inputs": [ - { - "capacity": "218 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-cell-type,0x01,0x", - "data": "{ account: mobcion.bit, expired_at: 1670913958 }", - "extraData": "{ status: 0, records_hash: 0x55478d76900611eb079b22088081124ed6c8bae21a05dd1a0d197efcc7c114ce }" - } - ], - "outputs": [ - { - "capacity": "218 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-cell-type,0x01,0x", - "data": "{ account: mobcion.bit, expired_at: 1670913958 }", - "extraData": "{ status: 1, records_hash: 0x55478d76900611eb079b22088081124ed6c8bae21a05dd1a0d197efcc7c114ce }" - }, - { - "capacity": "201 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-sale-cell-type,0x01,0x", - "data": "0x1209460ef3cb5f1c68ed2c43a3e020eec2d9de6e...", - "extraData": "" - } - ] - } -} -` - -func TestComplexTypedData(t *testing.T) { - t.Parallel() - var td apitypes.TypedData - err := json.Unmarshal([]byte(complexTypedData), &td) - if err != nil { - t.Fatalf("unmarshalling failed '%v'", err) - } - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x42b1aca82bb6900ff75e90a136de550a58f1a220a071704088eabd5e6ce20446") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -func TestGnosisSafe(t *testing.T) { - t.Parallel() - // json missing chain id - js := "{\n \"safe\": \"0x899FcB1437DE65DC6315f5a69C017dd3F2837557\",\n \"to\": \"0x899FcB1437DE65DC6315f5a69C017dd3F2837557\",\n \"value\": \"0\",\n \"data\": \"0x0d582f13000000000000000000000000d3ed2b8756b942c98c851722f3bd507a17b4745f0000000000000000000000000000000000000000000000000000000000000005\",\n \"operation\": 0,\n \"gasToken\": \"0x0000000000000000000000000000000000000000\",\n \"safeTxGas\": 0,\n \"baseGas\": 0,\n \"gasPrice\": \"0\",\n \"refundReceiver\": \"0x0000000000000000000000000000000000000000\",\n \"nonce\": 0,\n \"executionDate\": null,\n \"submissionDate\": \"2022-02-23T14:09:00.018475Z\",\n \"modified\": \"2022-12-01T15:52:21.214357Z\",\n \"blockNumber\": null,\n \"transactionHash\": null,\n \"safeTxHash\": \"0x6f0f5cffee69087c9d2471e477a63cab2ae171cf433e754315d558d8836274f4\",\n \"executor\": null,\n \"isExecuted\": false,\n \"isSuccessful\": null,\n \"ethGasPrice\": null,\n \"maxFeePerGas\": null,\n \"maxPriorityFeePerGas\": null,\n \"gasUsed\": null,\n \"fee\": null,\n \"origin\": \"https://gnosis-safe.io\",\n \"dataDecoded\": {\n \"method\": \"addOwnerWithThreshold\",\n \"parameters\": [\n {\n \"name\": \"owner\",\n \"type\": \"address\",\n \"value\": \"0xD3Ed2b8756b942c98c851722F3bd507a17B4745F\"\n },\n {\n \"name\": \"_threshold\",\n \"type\": \"uint256\",\n \"value\": \"5\"\n }\n ]\n },\n \"confirmationsRequired\": 4,\n \"confirmations\": [\n {\n \"owner\": \"0x30B714E065B879F5c042A75Bb40a220A0BE27966\",\n \"submissionDate\": \"2022-03-01T14:56:22Z\",\n \"transactionHash\": \"0x6d0a9c83ac7578ef3be1f2afce089fb83b619583dfa779b82f4422fd64ff3ee9\",\n \"signature\": \"0x00000000000000000000000030b714e065b879f5c042a75bb40a220a0be27966000000000000000000000000000000000000000000000000000000000000000001\",\n \"signatureType\": \"APPROVED_HASH\"\n },\n {\n \"owner\": \"0x8300dFEa25Da0eb744fC0D98c23283F86AB8c10C\",\n \"submissionDate\": \"2022-12-01T15:52:21.214357Z\",\n \"transactionHash\": null,\n \"signature\": \"0xbce73de4cc6ee208e933a93c794dcb8ba1810f9848d1eec416b7be4dae9854c07dbf1720e60bbd310d2159197a380c941cfdb55b3ce58f9dd69efd395d7bef881b\",\n \"signatureType\": \"EOA\"\n }\n ],\n \"trusted\": true,\n \"signatures\": null\n}\n" - var gnosisTx core.GnosisSafeTx - if err := json.Unmarshal([]byte(js), &gnosisTx); err != nil { - t.Fatal(err) - } - sighash, _, err := apitypes.TypedDataAndHash(gnosisTx.ToTypedData()) - if err != nil { - t.Fatal(err) - } - if bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) { - t.Fatal("expected inequality") - } - gnosisTx.ChainId = (*math.HexOrDecimal256)(big.NewInt(1)) - sighash, _, _ = apitypes.TypedDataAndHash(gnosisTx.ToTypedData()) - if !bytes.Equal(sighash, gnosisTx.InputExpHash.Bytes()) { - t.Fatal("expected equality") - } -} - -var complexTypedDataLCRefType = ` -{ - "types": { - "EIP712Domain": [ - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "name", - "type": "string" - }, - { - "name": "verifyingContract", - "type": "address" - }, - { - "name": "version", - "type": "string" - } - ], - "Action": [ - { - "name": "action", - "type": "string" - }, - { - "name": "params", - "type": "string" - } - ], - "cCell": [ - { - "name": "capacity", - "type": "string" - }, - { - "name": "lock", - "type": "string" - }, - { - "name": "type", - "type": "string" - }, - { - "name": "data", - "type": "string" - }, - { - "name": "extraData", - "type": "string" - } - ], - "Transaction": [ - { - "name": "DAS_MESSAGE", - "type": "string" - }, - { - "name": "inputsCapacity", - "type": "string" - }, - { - "name": "outputsCapacity", - "type": "string" - }, - { - "name": "fee", - "type": "string" - }, - { - "name": "action", - "type": "Action" - }, - { - "name": "inputs", - "type": "cCell[]" - }, - { - "name": "outputs", - "type": "cCell[]" - }, - { - "name": "digest", - "type": "bytes32" - } - ] - }, - "primaryType": "Transaction", - "domain": { - "chainId": "56", - "name": "da.systems", - "verifyingContract": "0x0000000000000000000000000000000020210722", - "version": "1" - }, - "message": { - "DAS_MESSAGE": "SELL mobcion.bit FOR 100000 CKB", - "inputsCapacity": "1216.9999 CKB", - "outputsCapacity": "1216.9998 CKB", - "fee": "0.0001 CKB", - "digest": "0x53a6c0f19ec281604607f5d6817e442082ad1882bef0df64d84d3810dae561eb", - "action": { - "action": "start_account_sale", - "params": "0x00" - }, - "inputs": [ - { - "capacity": "218 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-cell-type,0x01,0x", - "data": "{ account: mobcion.bit, expired_at: 1670913958 }", - "extraData": "{ status: 0, records_hash: 0x55478d76900611eb079b22088081124ed6c8bae21a05dd1a0d197efcc7c114ce }" - } - ], - "outputs": [ - { - "capacity": "218 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-cell-type,0x01,0x", - "data": "{ account: mobcion.bit, expired_at: 1670913958 }", - "extraData": "{ status: 1, records_hash: 0x55478d76900611eb079b22088081124ed6c8bae21a05dd1a0d197efcc7c114ce }" - }, - { - "capacity": "201 CKB", - "lock": "das-lock,0x01,0x051c152f77f8efa9c7c6d181cc97ee67c165c506...", - "type": "account-sale-cell-type,0x01,0x", - "data": "0x1209460ef3cb5f1c68ed2c43a3e020eec2d9de6e...", - "extraData": "" - } - ] - } -} -` - -func TestComplexTypedDataWithLowercaseReftype(t *testing.T) { - t.Parallel() - var td apitypes.TypedData - err := json.Unmarshal([]byte(complexTypedDataLCRefType), &td) - if err != nil { - t.Fatalf("unmarshalling failed '%v'", err) - } - _, sighash, err := sign(td) - if err != nil { - t.Fatal(err) - } - expSigHash := common.FromHex("0x49191f910874f0148597204d9076af128d4694a7c4b714f1ccff330b87207bff") - if !bytes.Equal(expSigHash, sighash) { - t.Fatalf("Error, got %x, wanted %x", sighash, expSigHash) - } -} - -var recursiveBytesTypesStandard = apitypes.Types{ - "EIP712Domain": { - { - Name: "name", - Type: "string", - }, - { - Name: "version", - Type: "string", - }, - { - Name: "chainId", - Type: "uint256", - }, - { - Name: "verifyingContract", - Type: "address", - }, - }, - "Val": { - { - Name: "field", - Type: "bytes[][]", - }, - }, -} - -var recursiveBytesMessageStandard = map[string]interface{}{ - "field": [][][]byte{{{1}, {2}}, {{3}, {4}}}, -} - -var recursiveBytesTypedData = apitypes.TypedData{ - Types: recursiveBytesTypesStandard, - PrimaryType: "Val", - Domain: domainStandard, - Message: recursiveBytesMessageStandard, -} - -func TestEncodeDataRecursiveBytes(t *testing.T) { - typedData := recursiveBytesTypedData - _, err := typedData.EncodeData(typedData.PrimaryType, typedData.Message, 0) - if err != nil { - t.Fatalf("got err %v", err) - } -} diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go deleted file mode 100644 index a0ce684417..0000000000 --- a/signer/core/stdioui.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "context" - - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/rpc" -) - -type StdIOUI struct { - client *rpc.Client -} - -func NewStdIOUI() *StdIOUI { - client, err := rpc.DialContext(context.Background(), "stdio://") - if err != nil { - log.Crit("Could not create stdio client", "err", err) - } - ui := &StdIOUI{client: client} - return ui -} - -func (ui *StdIOUI) RegisterUIServer(api *UIServerAPI) { - ui.client.RegisterName("clef", api) -} - -// dispatch sends a request over the stdio -func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error { - err := ui.client.Call(&reply, serviceMethod, args) - if err != nil { - log.Info("Error", "exc", err.Error()) - } - return err -} - -// notify sends a request over the stdio, and does not listen for a response -func (ui *StdIOUI) notify(serviceMethod string, args interface{}) error { - ctx := context.Background() - err := ui.client.Notify(ctx, serviceMethod, args) - if err != nil { - log.Info("Error", "exc", err.Error()) - } - return err -} - -func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { - var result SignTxResponse - err := ui.dispatch("ui_approveTx", request, &result) - return result, err -} - -func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { - var result SignDataResponse - err := ui.dispatch("ui_approveSignData", request, &result) - return result, err -} - -func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) { - var result ListResponse - err := ui.dispatch("ui_approveListing", request, &result) - return result, err -} - -func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { - var result NewAccountResponse - err := ui.dispatch("ui_approveNewAccount", request, &result) - return result, err -} - -func (ui *StdIOUI) ShowError(message string) { - err := ui.notify("ui_showError", &Message{message}) - if err != nil { - log.Info("Error calling 'ui_showError'", "exc", err.Error(), "msg", message) - } -} - -func (ui *StdIOUI) ShowInfo(message string) { - err := ui.notify("ui_showInfo", Message{message}) - if err != nil { - log.Info("Error calling 'ui_showInfo'", "exc", err.Error(), "msg", message) - } -} -func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) { - err := ui.notify("ui_onApprovedTx", tx) - if err != nil { - log.Info("Error calling 'ui_onApprovedTx'", "exc", err.Error(), "tx", tx) - } -} - -func (ui *StdIOUI) OnSignerStartup(info StartupInfo) { - err := ui.notify("ui_onSignerStartup", info) - if err != nil { - log.Info("Error calling 'ui_onSignerStartup'", "exc", err.Error(), "info", info) - } -} -func (ui *StdIOUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { - var result UserInputResponse - err := ui.dispatch("ui_onInputRequired", info, &result) - if err != nil { - log.Info("Error calling 'ui_onInputRequired'", "exc", err.Error(), "info", info) - } - return result, err -} diff --git a/signer/core/testdata/README.md b/signer/core/testdata/README.md deleted file mode 100644 index 85aa70c04c..0000000000 --- a/signer/core/testdata/README.md +++ /dev/null @@ -1,5 +0,0 @@ -### EIP-712 tests - -These tests are json files which are converted into [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data. -All files are expected to be proper json, and tests will fail if they are not. -Files that begin with `expfail' are expected to not pass the hashstruct construction. diff --git a/signer/core/testdata/arrays-1.json b/signer/core/testdata/arrays-1.json deleted file mode 100644 index fea82b42c6..0000000000 --- a/signer/core/testdata/arrays-1.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Foo": [ - { - "name": "addys", - "type": "address[]" - }, - { - "name": "stringies", - "type": "string[]" - }, - { - "name": "inties", - "type": "uint[]" - } - ] - }, - "primaryType": "Foo", - "domain": { - "name": "Lorem", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "addys": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "stringies": [ - "lorem", - "ipsum", - "dolores" - ], - "inties": [ - "0x0000000000000000000000000000000000000001", - "3", - 4.0 - ] - } -} diff --git a/signer/core/testdata/custom_arraytype.json b/signer/core/testdata/custom_arraytype.json deleted file mode 100644 index 078de88c22..0000000000 --- a/signer/core/testdata/custom_arraytype.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person[]" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { "name": "Cow"}, - "to": [{ "name": "Moose"},{ "name": "Goose"}], - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/eip712.json b/signer/core/testdata/eip712.json deleted file mode 100644 index 7b1cb8ae2d..0000000000 --- a/signer/core/testdata/eip712.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "test", - "type": "uint8" - }, - { - "name": "test2", - "type": "uint8" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "test": "3", - "test2": 5.0, - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "test": "0", - "test2": 5, - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_arraytype_overload.json b/signer/core/testdata/expfail_arraytype_overload.json deleted file mode 100644 index 786487f100..0000000000 --- a/signer/core/testdata/expfail_arraytype_overload.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Person[]": [ - { - "name": "baz", - "type": "string" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person[]" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": {"baz": "foo"}, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_datamismatch_1.json b/signer/core/testdata/expfail_datamismatch_1.json deleted file mode 100644 index d19d470d1e..0000000000 --- a/signer/core/testdata/expfail_datamismatch_1.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "Person" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_extradata.json b/signer/core/testdata/expfail_extradata.json deleted file mode 100644 index 10f91c23af..0000000000 --- a/signer/core/testdata/expfail_extradata.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "test", - "type": "uint8" - }, - { - "name": "test2", - "type": "uint8" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "blahonga": "zonk bonk", - "from": { - "name": "Cow", - "test": "3", - "test2": 5.0, - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "test": "0", - "test2": 5, - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_malformeddomainkeys.json b/signer/core/testdata/expfail_malformeddomainkeys.json deleted file mode 100644 index 354b3cc859..0000000000 --- a/signer/core/testdata/expfail_malformeddomainkeys.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "vFAILFAILerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_nonexistant_type.json b/signer/core/testdata/expfail_nonexistant_type.json deleted file mode 100644 index d06bc20b9f..0000000000 --- a/signer/core/testdata/expfail_nonexistant_type.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "Blahonga" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_nonexistant_type2.json b/signer/core/testdata/expfail_nonexistant_type2.json deleted file mode 100644 index fd704209bc..0000000000 --- a/signer/core/testdata/expfail_nonexistant_type2.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256 ... and now for something completely different" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "test", - "type": "uint8" - }, - { - "name": "test2", - "type": "uint8" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "test": "3", - "test2": 5.0, - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "test": "0", - "test2": 5, - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } -} diff --git a/signer/core/testdata/expfail_toolargeuint.json b/signer/core/testdata/expfail_toolargeuint.json deleted file mode 100644 index 9854b65b17..0000000000 --- a/signer/core/testdata/expfail_toolargeuint.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test":"257" - } -} diff --git a/signer/core/testdata/expfail_toolargeuint2.json b/signer/core/testdata/expfail_toolargeuint2.json deleted file mode 100644 index c63ed41f9c..0000000000 --- a/signer/core/testdata/expfail_toolargeuint2.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test":257 - } -} diff --git a/signer/core/testdata/expfail_unconvertiblefloat.json b/signer/core/testdata/expfail_unconvertiblefloat.json deleted file mode 100644 index 8229a333ca..0000000000 --- a/signer/core/testdata/expfail_unconvertiblefloat.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test":"255.3" - } -} diff --git a/signer/core/testdata/expfail_unconvertiblefloat2.json b/signer/core/testdata/expfail_unconvertiblefloat2.json deleted file mode 100644 index 59e6d38d24..0000000000 --- a/signer/core/testdata/expfail_unconvertiblefloat2.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test": 255.3 - } -} diff --git a/signer/core/testdata/fuzzing/2850f6ccf2d7f5f846dfb73119b60e09e712783f b/signer/core/testdata/fuzzing/2850f6ccf2d7f5f846dfb73119b60e09e712783f deleted file mode 100644 index 8229a333ca..0000000000 --- a/signer/core/testdata/fuzzing/2850f6ccf2d7f5f846dfb73119b60e09e712783f +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test":"255.3" - } -} diff --git a/signer/core/testdata/fuzzing/36fb987a774011dc675e1b5246ac5c1d44d84d92 b/signer/core/testdata/fuzzing/36fb987a774011dc675e1b5246ac5c1d44d84d92 deleted file mode 100644 index fea82b42c6..0000000000 --- a/signer/core/testdata/fuzzing/36fb987a774011dc675e1b5246ac5c1d44d84d92 +++ /dev/null @@ -1,60 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Foo": [ - { - "name": "addys", - "type": "address[]" - }, - { - "name": "stringies", - "type": "string[]" - }, - { - "name": "inties", - "type": "uint[]" - } - ] - }, - "primaryType": "Foo", - "domain": { - "name": "Lorem", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "addys": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "stringies": [ - "lorem", - "ipsum", - "dolores" - ], - "inties": [ - "0x0000000000000000000000000000000000000001", - "3", - 4.0 - ] - } -} diff --git a/signer/core/testdata/fuzzing/37ec7b55c7ba014cced204c5f9989d2d0eb9ff6d b/signer/core/testdata/fuzzing/37ec7b55c7ba014cced204c5f9989d2d0eb9ff6d deleted file mode 100644 index c63ed41f9c..0000000000 --- a/signer/core/testdata/fuzzing/37ec7b55c7ba014cced204c5f9989d2d0eb9ff6d +++ /dev/null @@ -1,38 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Mail": [ - { - "name": "test", - "type": "uint8" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": "1", - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "test":257 - } -} diff --git a/signer/core/testdata/fuzzing/582fa92154b784daa1faa293b695fa388fe34bf1 b/signer/core/testdata/fuzzing/582fa92154b784daa1faa293b695fa388fe34bf1 deleted file mode 100644 index 9bc43938d5..0000000000 --- a/signer/core/testdata/fuzzing/582fa92154b784daa1faa293b695fa388fe34bf1 +++ /dev/null @@ -1 +0,0 @@ -{"domain":{"version":"0","chainId":""}} \ No newline at end of file diff --git a/signer/core/testdata/fuzzing/ab57cb2b2b5ce614efe13a47bc73814580f2cce8 b/signer/core/testdata/fuzzing/ab57cb2b2b5ce614efe13a47bc73814580f2cce8 deleted file mode 100644 index fe27de916c..0000000000 --- a/signer/core/testdata/fuzzing/ab57cb2b2b5ce614efe13a47bc73814580f2cce8 +++ /dev/null @@ -1,54 +0,0 @@ -{ "types": { "":[ { - "name": "name", - "type":"string" }, - { - "name":"version", - "type": "string" }, { - "name": "chaiI", - "type":"uint256 . ad nowretig omeedifere" }, { - "ae": "eifinC", - "ty":"dess" - } - ], - "Person":[ - { - "name":"name", - "type": "string" - }, { - "name":"tes", "type":"it8" - }, - { "name":"t", "tye":"uit8" - }, - { - "a":"ale", - "type": "ress" - } - ], - "Mail": [ - { - "name":"from", "type":"Person" }, - { - "name": "to", "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, "primaryType": "Mail", - "domain": { -"name":"theMail", "version": "1", - "chainId": "1", - "verifyingntract": "0xCcccCCCcCCCCCCCcCCcCCCcCcccccC" - }, - "message": { "from": { - "name": "Cow", - "test": "3", - "est2":5.0, - "llt": "0xcD2a3938E13D947E0bE734DfDD86" }, "to": { "name": "Bob", - "ts":"", - "tet2": 5, - "allet": "0bBBBBbbBBbbbbBbbBbbbbBBBbB" - }, - "contents": "Hello, Bob!" } -} \ No newline at end of file diff --git a/signer/core/testdata/fuzzing/e4303e23ca34fbbc43164a232b2caa7a3af2bf8d b/signer/core/testdata/fuzzing/e4303e23ca34fbbc43164a232b2caa7a3af2bf8d deleted file mode 100644 index c5e14b39ed..0000000000 --- a/signer/core/testdata/fuzzing/e4303e23ca34fbbc43164a232b2caa7a3af2bf8d +++ /dev/null @@ -1,64 +0,0 @@ -{ - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "int" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Mail" - }, - { - "name": "s", - "type": "Person" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "l", - "version": "1", - "chainId": "", - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "": "" - } -} diff --git a/signer/core/testdata/fuzzing/f658340af009dd4a35abe645a00a7b732bc30921 b/signer/core/testdata/fuzzing/f658340af009dd4a35abe645a00a7b732bc30921 deleted file mode 100644 index c4841cb07b..0000000000 --- a/signer/core/testdata/fuzzing/f658340af009dd4a35abe645a00a7b732bc30921 +++ /dev/null @@ -1 +0,0 @@ -{"types":{"0":[{}]}} \ No newline at end of file diff --git a/signer/core/uiapi.go b/signer/core/uiapi.go deleted file mode 100644 index 09ee4b492f..0000000000 --- a/signer/core/uiapi.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2019 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - "os" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" -) - -// UIServerAPI implements methods Clef provides for a UI to query, in the bidirectional communication -// channel. -// This API is considered secure, since a request can only -// ever arrive from the UI -- and the UI is capable of approving any action, thus we can consider these -// requests pre-approved. -// NB: It's very important that these methods are not ever exposed on the external service -// registry. -type UIServerAPI struct { - extApi *SignerAPI - am *accounts.Manager -} - -// NewUIServerAPI creates a new UIServerAPI -func NewUIServerAPI(extapi *SignerAPI) *UIServerAPI { - return &UIServerAPI{extapi, extapi.am} -} - -// ListAccounts lists available accounts. As opposed to the external API definition, this method delivers -// the full Account object and not only Address. -// Example call -// {"jsonrpc":"2.0","method":"clef_listAccounts","params":[], "id":4} -func (api *UIServerAPI) ListAccounts(ctx context.Context) ([]accounts.Account, error) { - var accs []accounts.Account - for _, wallet := range api.am.Wallets() { - accs = append(accs, wallet.Accounts()...) - } - return accs, nil -} - -// rawWallet is a JSON representation of an accounts.Wallet interface, with its -// data contents extracted into plain fields. -type rawWallet struct { - URL string `json:"url"` - Status string `json:"status"` - Failure string `json:"failure,omitempty"` - Accounts []accounts.Account `json:"accounts,omitempty"` -} - -// ListWallets will return a list of wallets that clef manages -// Example call -// {"jsonrpc":"2.0","method":"clef_listWallets","params":[], "id":5} -func (api *UIServerAPI) ListWallets() []rawWallet { - allWallets := api.am.Wallets() - wallets := make([]rawWallet, 0, len(allWallets)) // return [] instead of nil if empty - for _, wallet := range allWallets { - status, failure := wallet.Status() - - raw := rawWallet{ - URL: wallet.URL().String(), - Status: status, - Accounts: wallet.Accounts(), - } - if failure != nil { - raw.Failure = failure.Error() - } - wallets = append(wallets, raw) - } - return wallets -} - -// DeriveAccount requests a HD wallet to derive a new account, optionally pinning -// it for later reuse. -// Example call -// {"jsonrpc":"2.0","method":"clef_deriveAccount","params":["ledger://","m/44'/60'/0'", false], "id":6} -func (api *UIServerAPI) DeriveAccount(url string, path string, pin *bool) (accounts.Account, error) { - wallet, err := api.am.Wallet(url) - if err != nil { - return accounts.Account{}, err - } - derivPath, err := accounts.ParseDerivationPath(path) - if err != nil { - return accounts.Account{}, err - } - if pin == nil { - pin = new(bool) - } - return wallet.Derive(derivPath, *pin) -} - -// fetchKeystore retrieves the encrypted keystore from the account manager. -func fetchKeystore(am *accounts.Manager) *keystore.KeyStore { - ks := am.Backends(keystore.KeyStoreType) - if len(ks) == 0 { - return nil - } - return ks[0].(*keystore.KeyStore) -} - -// ImportRawKey stores the given hex encoded ECDSA key into the key directory, -// encrypting it with the passphrase. -// Example call (should fail on password too short) -// {"jsonrpc":"2.0","method":"clef_importRawKey","params":["1111111111111111111111111111111111111111111111111111111111111111","test"], "id":6} -func (api *UIServerAPI) ImportRawKey(privkey string, password string) (accounts.Account, error) { - key, err := crypto.HexToECDSA(privkey) - if err != nil { - return accounts.Account{}, err - } - if err := ValidatePasswordFormat(password); err != nil { - return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err) - } - ks := fetchKeystore(api.am) - if ks == nil { - return accounts.Account{}, errors.New("password based accounts not supported") - } - // No error - return ks.ImportECDSA(key, password) -} - -// OpenWallet initiates a hardware wallet opening procedure, establishing a USB -// connection and attempting to authenticate via the provided passphrase. Note, -// the method may return an extra challenge requiring a second open (e.g. the -// Trezor PIN matrix challenge). -// Example -// {"jsonrpc":"2.0","method":"clef_openWallet","params":["ledger://",""], "id":6} -func (api *UIServerAPI) OpenWallet(url string, passphrase *string) error { - wallet, err := api.am.Wallet(url) - if err != nil { - return err - } - pass := "" - if passphrase != nil { - pass = *passphrase - } - return wallet.Open(pass) -} - -// ChainId returns the chainid in use for Eip-155 replay protection -// Example call -// {"jsonrpc":"2.0","method":"clef_chainId","params":[], "id":8} -func (api *UIServerAPI) ChainId() math.HexOrDecimal64 { - return (math.HexOrDecimal64)(api.extApi.chainID.Uint64()) -} - -// SetChainId sets the chain id to use when signing transactions. -// Example call to set Ropsten: -// {"jsonrpc":"2.0","method":"clef_setChainId","params":["3"], "id":8} -func (api *UIServerAPI) SetChainId(id math.HexOrDecimal64) math.HexOrDecimal64 { - api.extApi.chainID = new(big.Int).SetUint64(uint64(id)) - return api.ChainId() -} - -// Export returns encrypted private key associated with the given address in web3 keystore format. -// Example -// {"jsonrpc":"2.0","method":"clef_export","params":["0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"], "id":4} -func (api *UIServerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { - // Look up the wallet containing the requested signer - wallet, err := api.am.Find(accounts.Account{Address: addr}) - if err != nil { - return nil, err - } - if wallet.URL().Scheme != keystore.KeyStoreScheme { - return nil, errors.New("account is not a keystore-account") - } - return os.ReadFile(wallet.URL().Path) -} - -// Import tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be -// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful -// decryption it will encrypt the key with the given newPassphrase and store it in the keystore. -// Example (the address in question has privkey `11...11`): -// {"jsonrpc":"2.0","method":"clef_import","params":[{"address":"19e7e376e7c213b7e7e7e46cc70a5dd086daff2a","crypto":{"cipher":"aes-128-ctr","ciphertext":"33e4cd3756091d037862bb7295e9552424a391a6e003272180a455ca2a9fb332","cipherparams":{"iv":"b54b263e8f89c42bb219b6279fba5cce"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"e4ca94644fd30569c1b1afbbc851729953c92637b7fe4bb9840bbb31ffbc64a5"},"mac":"f4092a445c2b21c0ef34f17c9cd0d873702b2869ec5df4439a0c2505823217e7"},"id":"216c7eac-e8c1-49af-a215-fa0036f29141","version":3},"test","yaddayadda"], "id":4} -func (api *UIServerAPI) Import(ctx context.Context, keyJSON json.RawMessage, oldPassphrase, newPassphrase string) (accounts.Account, error) { - be := api.am.Backends(keystore.KeyStoreType) - - if len(be) == 0 { - return accounts.Account{}, errors.New("password based accounts not supported") - } - if err := ValidatePasswordFormat(newPassphrase); err != nil { - return accounts.Account{}, fmt.Errorf("password requirements not met: %v", err) - } - return be[0].(*keystore.KeyStore).Import(keyJSON, oldPassphrase, newPassphrase) -} - -// New creates a new password protected Account. The private key is protected with -// the given password. Users are responsible to backup the private key that is stored -// in the keystore location that was specified when this API was created. -// This method is the same as New on the external API, the difference being that -// this implementation does not ask for confirmation, since it's initiated by -// the user -func (api *UIServerAPI) New(ctx context.Context) (common.Address, error) { - return api.extApi.newAccount() -} - -// Other methods to be added, not yet implemented are: -// - Ruleset interaction: add rules, attest rulefiles -// - Store metadata about accounts, e.g. naming of accounts diff --git a/signer/core/validation.go b/signer/core/validation.go deleted file mode 100644 index 7639dbf649..0000000000 --- a/signer/core/validation.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "errors" - "regexp" -) - -var printable7BitAscii = regexp.MustCompile("^[A-Za-z0-9!\"#$%&'()*+,\\-./:;<=>?@[\\]^_`{|}~ ]+$") - -// ValidatePasswordFormat returns an error if the password is too short, or consists of characters -// outside the range of the printable 7bit ascii set -func ValidatePasswordFormat(password string) error { - if len(password) < 10 { - return errors.New("password too short (<10 characters)") - } - if !printable7BitAscii.MatchString(password) { - return errors.New("password contains invalid characters - only 7bit printable ascii allowed") - } - return nil -} diff --git a/signer/core/validation_test.go b/signer/core/validation_test.go deleted file mode 100644 index 7f733b0bb1..0000000000 --- a/signer/core/validation_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import "testing" - -func TestPasswordValidation(t *testing.T) { - t.Parallel() - testcases := []struct { - pw string - shouldFail bool - }{ - {"test", true}, - {"testtest\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98", true}, - {"placeOfInterest⌘", true}, - {"password\nwith\nlinebreak", true}, - {"password\twith\vtabs", true}, - // Ok passwords - {"password WhichIsOk", false}, - {"passwordOk!@#$%^&*()", false}, - {"12301203123012301230123012", false}, - } - for _, test := range testcases { - err := ValidatePasswordFormat(test.pw) - if err == nil && test.shouldFail { - t.Errorf("password '%v' should fail validation", test.pw) - } else if err != nil && !test.shouldFail { - t.Errorf("password '%v' shound not fail validation, but did: %v", test.pw, err) - } - } -} diff --git a/signer/rules/rules.go b/signer/rules/rules.go deleted file mode 100644 index c9921e57a9..0000000000 --- a/signer/rules/rules.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package rules - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strings" - - "github.com/dop251/goja" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/internal/jsre/deps" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/signer/core" - "github.com/ethereum/go-ethereum/signer/storage" -) - -// consoleOutput is an override for the console.log and console.error methods to -// stream the output into the configured output stream instead of stdout. -func consoleOutput(call goja.FunctionCall) goja.Value { - output := []string{"JS:> "} - for _, argument := range call.Arguments { - output = append(output, fmt.Sprintf("%v", argument)) - } - fmt.Fprintln(os.Stderr, strings.Join(output, " ")) - return goja.Undefined() -} - -// rulesetUI provides an implementation of UIClientAPI that evaluates a javascript -// file for each defined UI-method -type rulesetUI struct { - next core.UIClientAPI // The next handler, for manual processing - storage storage.Storage - jsRules string // The rules to use -} - -func NewRuleEvaluator(next core.UIClientAPI, jsbackend storage.Storage) (*rulesetUI, error) { - c := &rulesetUI{ - next: next, - storage: jsbackend, - jsRules: "", - } - - return c, nil -} -func (r *rulesetUI) RegisterUIServer(api *core.UIServerAPI) { - r.next.RegisterUIServer(api) - // TODO, make it possible to query from js -} - -func (r *rulesetUI) Init(javascriptRules string) error { - r.jsRules = javascriptRules - return nil -} -func (r *rulesetUI) execute(jsfunc string, jsarg interface{}) (goja.Value, error) { - // Instantiate a fresh vm engine every time - vm := goja.New() - - // Set the native callbacks - consoleObj := vm.NewObject() - consoleObj.Set("log", consoleOutput) - consoleObj.Set("error", consoleOutput) - vm.Set("console", consoleObj) - - storageObj := vm.NewObject() - storageObj.Set("put", func(call goja.FunctionCall) goja.Value { - key, val := call.Argument(0).String(), call.Argument(1).String() - if val == "" { - r.storage.Del(key) - } else { - r.storage.Put(key, val) - } - return goja.Null() - }) - storageObj.Set("get", func(call goja.FunctionCall) goja.Value { - goval, _ := r.storage.Get(call.Argument(0).String()) - jsval := vm.ToValue(goval) - return jsval - }) - vm.Set("storage", storageObj) - - // Load bootstrap libraries - script, err := goja.Compile("bignumber.js", deps.BigNumberJS, true) - if err != nil { - log.Warn("Failed loading libraries", "err", err) - return goja.Undefined(), err - } - vm.RunProgram(script) - - // Run the actual rule implementation - _, err = vm.RunString(r.jsRules) - if err != nil { - log.Warn("Execution failed", "err", err) - return goja.Undefined(), err - } - - // And the actual call - // All calls are objects with the parameters being keys in that object. - // To provide additional insulation between js and go, we serialize it into JSON on the Go-side, - // and deserialize it on the JS side. - - jsonbytes, err := json.Marshal(jsarg) - if err != nil { - log.Warn("failed marshalling data", "data", jsarg) - return goja.Undefined(), err - } - // Now, we call foobar(JSON.parse()). - var call string - if len(jsonbytes) > 0 { - call = fmt.Sprintf("%v(JSON.parse(%v))", jsfunc, string(jsonbytes)) - } else { - call = fmt.Sprintf("%v()", jsfunc) - } - return vm.RunString(call) -} - -func (r *rulesetUI) checkApproval(jsfunc string, jsarg []byte, err error) (bool, error) { - if err != nil { - return false, err - } - v, err := r.execute(jsfunc, string(jsarg)) - if err != nil { - log.Info("error occurred during execution", "error", err) - return false, err - } - result := v.ToString().String() - if result == "Approve" { - log.Info("Op approved") - return true, nil - } else if result == "Reject" { - log.Info("Op rejected") - return false, nil - } - return false, errors.New("unknown response") -} - -func (r *rulesetUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { - jsonreq, err := json.Marshal(request) - approved, err := r.checkApproval("ApproveTx", jsonreq, err) - if err != nil { - log.Info("Rule-based approval error, going to manual", "error", err) - return r.next.ApproveTx(request) - } - - if approved { - return core.SignTxResponse{ - Transaction: request.Transaction, - Approved: true}, - nil - } - return core.SignTxResponse{Approved: false}, err -} - -func (r *rulesetUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { - jsonreq, err := json.Marshal(request) - approved, err := r.checkApproval("ApproveSignData", jsonreq, err) - if err != nil { - log.Info("Rule-based approval error, going to manual", "error", err) - return r.next.ApproveSignData(request) - } - if approved { - return core.SignDataResponse{Approved: true}, nil - } - return core.SignDataResponse{Approved: false}, err -} - -// OnInputRequired not handled by rules -func (r *rulesetUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { - return r.next.OnInputRequired(info) -} - -func (r *rulesetUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { - jsonreq, err := json.Marshal(request) - approved, err := r.checkApproval("ApproveListing", jsonreq, err) - if err != nil { - log.Info("Rule-based approval error, going to manual", "error", err) - return r.next.ApproveListing(request) - } - if approved { - return core.ListResponse{Accounts: request.Accounts}, nil - } - return core.ListResponse{}, err -} - -func (r *rulesetUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { - // This cannot be handled by rules, requires setting a password - // dispatch to next - return r.next.ApproveNewAccount(request) -} - -func (r *rulesetUI) ShowError(message string) { - log.Error(message) - r.next.ShowError(message) -} - -func (r *rulesetUI) ShowInfo(message string) { - log.Info(message) - r.next.ShowInfo(message) -} - -func (r *rulesetUI) OnSignerStartup(info core.StartupInfo) { - jsonInfo, err := json.Marshal(info) - if err != nil { - log.Warn("failed marshalling data", "data", info) - return - } - r.next.OnSignerStartup(info) - _, err = r.execute("OnSignerStartup", string(jsonInfo)) - if err != nil { - log.Info("error occurred during execution", "error", err) - } -} - -func (r *rulesetUI) OnApprovedTx(tx ethapi.SignTransactionResult) { - jsonTx, err := json.Marshal(tx) - if err != nil { - log.Warn("failed marshalling transaction", "tx", tx) - return - } - _, err = r.execute("OnApprovedTx", string(jsonTx)) - if err != nil { - log.Info("error occurred during execution", "error", err) - } -} diff --git a/signer/rules/rules_test.go b/signer/rules/rules_test.go deleted file mode 100644 index d27de22b29..0000000000 --- a/signer/rules/rules_test.go +++ /dev/null @@ -1,626 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package rules - -import ( - "fmt" - "math/big" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/internal/ethapi" - "github.com/ethereum/go-ethereum/signer/core" - "github.com/ethereum/go-ethereum/signer/core/apitypes" - "github.com/ethereum/go-ethereum/signer/storage" -) - -const JS = ` -/** -This is an example implementation of a Javascript rule file. - -When the signer receives a request over the external API, the corresponding method is evaluated. -Three things can happen: - -1. The method returns "Approve". This means the operation is permitted. -2. The method returns "Reject". This means the operation is rejected. -3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means -that the operation will continue to manual processing, via the regular UI method chosen by the user. - -[*] Note: Future version of the ruleset may use more complex json-based return values, making it possible to not -only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all -accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject"). - -**/ - -function ApproveListing(request){ - console.log("In js approve listing"); - console.log(request.accounts[3].Address) - console.log(request.meta.Remote) - return "Approve" -} - -function ApproveTx(request){ - console.log("test"); - console.log("from"); - return "Reject"; -} - -function test(thing){ - console.log(thing.String()) -} - -` - -func mixAddr(a string) (*common.MixedcaseAddress, error) { - return common.NewMixedcaseAddressFromString(a) -} - -type alwaysDenyUI struct{} - -func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { - return core.UserInputResponse{}, nil -} -func (alwaysDenyUI) RegisterUIServer(api *core.UIServerAPI) { -} - -func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { -} - -func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { - return core.SignTxResponse{Transaction: request.Transaction, Approved: false}, nil -} - -func (alwaysDenyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { - return core.SignDataResponse{Approved: false}, nil -} - -func (alwaysDenyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { - return core.ListResponse{Accounts: nil}, nil -} - -func (alwaysDenyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { - return core.NewAccountResponse{Approved: false}, nil -} - -func (alwaysDenyUI) ShowError(message string) { - panic("implement me") -} - -func (alwaysDenyUI) ShowInfo(message string) { - panic("implement me") -} - -func (alwaysDenyUI) OnApprovedTx(tx ethapi.SignTransactionResult) { - panic("implement me") -} - -func initRuleEngine(js string) (*rulesetUI, error) { - r, err := NewRuleEvaluator(&alwaysDenyUI{}, storage.NewEphemeralStorage()) - if err != nil { - return nil, fmt.Errorf("failed to create js engine: %v", err) - } - if err = r.Init(js); err != nil { - return nil, fmt.Errorf("failed to load bootstrap js: %v", err) - } - return r, nil -} - -func TestListRequest(t *testing.T) { - t.Parallel() - accs := make([]accounts.Account, 5) - - for i := range accs { - addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i) - acc := accounts.Account{ - Address: common.BytesToAddress(common.Hex2Bytes(addr)), - URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)}, - } - accs[i] = acc - } - - js := `function ApproveListing(){ return "Approve" }` - - r, err := initRuleEngine(js) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - resp, _ := r.ApproveListing(&core.ListRequest{ - Accounts: accs, - Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, - }) - if len(resp.Accounts) != len(accs) { - t.Errorf("Expected check to resolve to 'Approve'") - } -} - -func TestSignTxRequest(t *testing.T) { - t.Parallel() - js := ` - function ApproveTx(r){ - console.log("transaction.from", r.transaction.from); - console.log("transaction.to", r.transaction.to); - console.log("transaction.value", r.transaction.value); - console.log("transaction.nonce", r.transaction.nonce); - if(r.transaction.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"} - if(r.transaction.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"} - }` - - r, err := initRuleEngine(js) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - to, err := mixAddr("000000000000000000000000000000000000dead") - if err != nil { - t.Error(err) - return - } - from, err := mixAddr("0000000000000000000000000000000000001337") - - if err != nil { - t.Error(err) - return - } - t.Logf("to %v", to.Address().String()) - resp, err := r.ApproveTx(&core.SignTxRequest{ - Transaction: apitypes.SendTxArgs{ - From: *from, - To: to}, - Callinfo: nil, - Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, - }) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - if !resp.Approved { - t.Errorf("Expected check to resolve to 'Approve'") - } -} - -type dummyUI struct { - calls []string -} - -func (d *dummyUI) RegisterUIServer(api *core.UIServerAPI) { - panic("implement me") -} - -func (d *dummyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { - d.calls = append(d.calls, "OnInputRequired") - return core.UserInputResponse{}, nil -} - -func (d *dummyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { - d.calls = append(d.calls, "ApproveTx") - return core.SignTxResponse{}, core.ErrRequestDenied -} - -func (d *dummyUI) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { - d.calls = append(d.calls, "ApproveSignData") - return core.SignDataResponse{}, core.ErrRequestDenied -} - -func (d *dummyUI) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { - d.calls = append(d.calls, "ApproveListing") - return core.ListResponse{}, core.ErrRequestDenied -} - -func (d *dummyUI) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { - d.calls = append(d.calls, "ApproveNewAccount") - return core.NewAccountResponse{}, core.ErrRequestDenied -} - -func (d *dummyUI) ShowError(message string) { - d.calls = append(d.calls, "ShowError") -} - -func (d *dummyUI) ShowInfo(message string) { - d.calls = append(d.calls, "ShowInfo") -} - -func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) { - d.calls = append(d.calls, "OnApprovedTx") -} - -func (d *dummyUI) OnSignerStartup(info core.StartupInfo) { -} - -// TestForwarding tests that the rule-engine correctly dispatches requests to the next caller -func TestForwarding(t *testing.T) { - t.Parallel() - js := "" - ui := &dummyUI{make([]string, 0)} - jsBackend := storage.NewEphemeralStorage() - r, err := NewRuleEvaluator(ui, jsBackend) - if err != nil { - t.Fatalf("Failed to create js engine: %v", err) - } - if err = r.Init(js); err != nil { - t.Fatalf("Failed to load bootstrap js: %v", err) - } - r.ApproveSignData(nil) - r.ApproveTx(nil) - r.ApproveNewAccount(nil) - r.ApproveListing(nil) - r.ShowError("test") - r.ShowInfo("test") - - //This one is not forwarded - r.OnApprovedTx(ethapi.SignTransactionResult{}) - - expCalls := 6 - if len(ui.calls) != expCalls { - t.Errorf("Expected %d forwarded calls, got %d: %s", expCalls, len(ui.calls), strings.Join(ui.calls, ",")) - } -} - -func TestMissingFunc(t *testing.T) { - t.Parallel() - r, err := initRuleEngine(JS) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - - _, err = r.execute("MissingMethod", "test") - - if err == nil { - t.Error("Expected error") - } - - approved, err := r.checkApproval("MissingMethod", nil, nil) - if err == nil { - t.Errorf("Expected missing method to yield error'") - } - if approved { - t.Errorf("Expected missing method to cause non-approval") - } - t.Logf("Err %v", err) -} -func TestStorage(t *testing.T) { - t.Parallel() - js := ` - function testStorage(){ - storage.put("mykey", "myvalue") - a = storage.get("mykey") - - storage.put("mykey", ["a", "list"]) // Should result in "a,list" - a += storage.get("mykey") - - - storage.put("mykey", {"an": "object"}) // Should result in "[object Object]" - a += storage.get("mykey") - - - storage.put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}' - a += storage.get("mykey") - - a += storage.get("missingkey") //Missing keys should result in empty string - storage.put("","missing key==noop") // Can't store with 0-length key - a += storage.get("") // Should result in '' - - var b = new BigNumber(2) - var c = new BigNumber(16)//"0xf0",16) - var d = b.plus(c) - console.log(d) - return a - } -` - r, err := initRuleEngine(js) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - - v, err := r.execute("testStorage", nil) - - if err != nil { - t.Errorf("Unexpected error %v", err) - } - retval := v.ToString().String() - - if err != nil { - t.Errorf("Unexpected error %v", err) - } - exp := `myvaluea,list[object Object]{"an":"object"}` - if retval != exp { - t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval) - } - t.Logf("Err %v", err) -} - -const ExampleTxWindow = ` - function big(str){ - if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)} - return new BigNumber(str) - } - - // Time window: 1 week - var window = 1000* 3600*24*7; - - // Limit : 1 ether - var limit = new BigNumber("1e18"); - - function isLimitOk(transaction){ - var value = big(transaction.value) - // Start of our window function - var windowstart = new Date().getTime() - window; - - var txs = []; - var stored = storage.get('txs'); - - if(stored != ""){ - txs = JSON.parse(stored) - } - // First, remove all that have passed out of the time-window - var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart}); - console.log(txs, newtxs.length); - - // Secondly, aggregate the current sum - sum = new BigNumber(0) - - sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum); - console.log("ApproveTx > Sum so far", sum); - console.log("ApproveTx > Requested", value.toNumber()); - - // Would we exceed weekly limit ? - return sum.plus(value).lt(limit) - - } - function ApproveTx(r){ - console.log(r) - console.log(typeof(r)) - if (isLimitOk(r.transaction)){ - return "Approve" - } - return "Nope" - } - - /** - * OnApprovedTx(str) is called when a transaction has been approved and signed. The parameter - * 'response_str' contains the return value that will be sent to the external caller. - * The return value from this method is ignore - the reason for having this callback is to allow the - * ruleset to keep track of approved transactions. - * - * When implementing rate-limited rules, this callback should be used. - * If a rule responds with neither 'Approve' nor 'Reject' - the tx goes to manual processing. If the user - * then accepts the transaction, this method will be called. - * - * TLDR; Use this method to keep track of signed transactions, instead of using the data in ApproveTx. - */ - function OnApprovedTx(resp){ - var value = big(resp.tx.value) - var txs = [] - // Load stored transactions - var stored = storage.get('txs'); - if(stored != ""){ - txs = JSON.parse(stored) - } - // Add this to the storage - txs.push({tstamp: new Date().getTime(), value: value}); - storage.put("txs", JSON.stringify(txs)); - } - -` - -func dummyTx(value hexutil.Big) *core.SignTxRequest { - to, _ := mixAddr("000000000000000000000000000000000000dead") - from, _ := mixAddr("000000000000000000000000000000000000dead") - n := hexutil.Uint64(3) - gas := hexutil.Uint64(21000) - gasPrice := hexutil.Big(*big.NewInt(2000000)) - - return &core.SignTxRequest{ - Transaction: apitypes.SendTxArgs{ - From: *from, - To: to, - Value: value, - Nonce: n, - GasPrice: &gasPrice, - Gas: gas, - }, - Callinfo: []apitypes.ValidationInfo{ - {Typ: "Warning", Message: "All your base are belong to us"}, - }, - Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, - } -} - -func dummyTxWithV(value uint64) *core.SignTxRequest { - v := new(big.Int).SetUint64(value) - h := hexutil.Big(*v) - return dummyTx(h) -} - -func dummySigned(value *big.Int) *types.Transaction { - to := common.HexToAddress("000000000000000000000000000000000000dead") - gas := uint64(21000) - gasPrice := big.NewInt(2000000) - data := make([]byte, 0) - return types.NewTransaction(3, to, value, gas, gasPrice, data) -} - -func TestLimitWindow(t *testing.T) { - t.Parallel() - r, err := initRuleEngine(ExampleTxWindow) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - // 0.3 ether: 429D069189E0000 wei - v := new(big.Int).SetBytes(common.Hex2Bytes("0429D069189E0000")) - h := hexutil.Big(*v) - // The first three should succeed - for i := 0; i < 3; i++ { - unsigned := dummyTx(h) - resp, err := r.ApproveTx(unsigned) - if err != nil { - t.Errorf("Unexpected error %v", err) - } - if !resp.Approved { - t.Errorf("Expected check to resolve to 'Approve'") - } - // Create a dummy signed transaction - - response := ethapi.SignTransactionResult{ - Tx: dummySigned(v), - Raw: common.Hex2Bytes("deadbeef"), - } - r.OnApprovedTx(response) - } - // Fourth should fail - resp, _ := r.ApproveTx(dummyTx(h)) - if resp.Approved { - t.Errorf("Expected check to resolve to 'Reject'") - } -} - -// dontCallMe is used as a next-handler that does not want to be called - it invokes test failure -type dontCallMe struct { - t *testing.T -} - -func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInputResponse, error) { - d.t.Fatalf("Did not expect next-handler to be called") - return core.UserInputResponse{}, nil -} - -func (d *dontCallMe) RegisterUIServer(api *core.UIServerAPI) { -} - -func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { -} - -func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { - d.t.Fatalf("Did not expect next-handler to be called") - return core.SignTxResponse{}, core.ErrRequestDenied -} - -func (d *dontCallMe) ApproveSignData(request *core.SignDataRequest) (core.SignDataResponse, error) { - d.t.Fatalf("Did not expect next-handler to be called") - return core.SignDataResponse{}, core.ErrRequestDenied -} - -func (d *dontCallMe) ApproveListing(request *core.ListRequest) (core.ListResponse, error) { - d.t.Fatalf("Did not expect next-handler to be called") - return core.ListResponse{}, core.ErrRequestDenied -} - -func (d *dontCallMe) ApproveNewAccount(request *core.NewAccountRequest) (core.NewAccountResponse, error) { - d.t.Fatalf("Did not expect next-handler to be called") - return core.NewAccountResponse{}, core.ErrRequestDenied -} - -func (d *dontCallMe) ShowError(message string) { - d.t.Fatalf("Did not expect next-handler to be called") -} - -func (d *dontCallMe) ShowInfo(message string) { - d.t.Fatalf("Did not expect next-handler to be called") -} - -func (d *dontCallMe) OnApprovedTx(tx ethapi.SignTransactionResult) { - d.t.Fatalf("Did not expect next-handler to be called") -} - -// TestContextIsCleared tests that the rule-engine does not retain variables over several requests. -// if it does, that would be bad since developers may rely on that to store data, -// instead of using the disk-based data storage -func TestContextIsCleared(t *testing.T) { - t.Parallel() - js := ` - function ApproveTx(){ - if (typeof foobar == 'undefined') { - foobar = "Approve" - } - console.log(foobar) - if (foobar == "Approve"){ - foobar = "Reject" - }else{ - foobar = "Approve" - } - return foobar - } - ` - ui := &dontCallMe{t} - r, err := NewRuleEvaluator(ui, storage.NewEphemeralStorage()) - if err != nil { - t.Fatalf("Failed to create js engine: %v", err) - } - if err = r.Init(js); err != nil { - t.Fatalf("Failed to load bootstrap js: %v", err) - } - tx := dummyTxWithV(0) - r1, _ := r.ApproveTx(tx) - r2, _ := r.ApproveTx(tx) - if r1.Approved != r2.Approved { - t.Errorf("Expected execution context to be cleared between executions") - } -} - -func TestSignData(t *testing.T) { - t.Parallel() - js := `function ApproveListing(){ - return "Approve" -} -function ApproveSignData(r){ - if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa") - { - if(r.messages[0].value.indexOf("bazonk") >= 0){ - return "Approve" - } - return "Reject" - } - // Otherwise goes to manual processing -}` - r, err := initRuleEngine(js) - if err != nil { - t.Errorf("Couldn't create evaluator %v", err) - return - } - message := "baz bazonk foo" - hash, rawdata := accounts.TextAndHash([]byte(message)) - addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa") - - t.Logf("address %v %v\n", addr.String(), addr.Original()) - - nvt := []*apitypes.NameValueType{ - { - Name: "message", - Typ: "text/plain", - Value: message, - }, - } - resp, err := r.ApproveSignData(&core.SignDataRequest{ - Address: *addr, - Messages: nvt, - Hash: hash, - Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"}, - Rawdata: []byte(rawdata), - }) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if !resp.Approved { - t.Fatalf("Expected approved") - } -} From 369521becb0489d56ad8083874f417e2a07008f0 Mon Sep 17 00:00:00 2001 From: Yorick Downe <71337066+yorickdowne@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:48:45 +0100 Subject: [PATCH 69/76] cmd/utils: avoid extra newlines when reading era checksums (#35104) The checksum count during EraE import is off by one when `checksums.txt` ends its last line on a newline, as the pandaops file does. The current code would result in one empty string after the final `\n`, something like ``` []string{ "line1", "line2", "line3", "", } ``` Trim off the final `\n`, if it exists: `return strings.Split(strings.TrimRight(string(b), "\n"), "\n"), nil` --------- Co-authored-by: lightclient --- cmd/utils/cmd.go | 14 ++++++++++++-- cmd/utils/history_test.go | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cmd/utils/cmd.go b/cmd/utils/cmd.go index e490f613b3..3cb97ff947 100644 --- a/cmd/utils/cmd.go +++ b/cmd/utils/cmd.go @@ -242,11 +242,21 @@ func ImportChain(chain *core.BlockChain, fn string) error { } func readList(filename string) ([]string, error) { - b, err := os.ReadFile(filename) + f, err := os.Open(filename) if err != nil { return nil, err } - return strings.Split(string(b), "\n"), nil + defer f.Close() + + var lines []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return nil, err + } + return lines, nil } // ImportHistory imports Era1 files containing historical block information, diff --git a/cmd/utils/history_test.go b/cmd/utils/history_test.go index 56375f9ff5..e11068ae15 100644 --- a/cmd/utils/history_test.go +++ b/cmd/utils/history_test.go @@ -106,10 +106,14 @@ func TestHistoryImportAndExport(t *testing.T) { } // Read checksums. - b, err := os.ReadFile(filepath.Join(dir, "checksums.txt")) + checksumsFile := filepath.Join(dir, "checksums.txt") + b, err := os.ReadFile(checksumsFile) if err != nil { t.Fatalf("failed to read checksums: %v", err) } + + // Add a trailing newline to ensure checksum handling is defensive. + _ = os.WriteFile(checksumsFile, append(b, '\n'), 0644) checksums := strings.Split(string(b), "\n") // Verify each Era. From 7835a71dae92dd5f020f5d9ba05b5354c7278582 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 4 Jun 2026 05:51:27 +0800 Subject: [PATCH 70/76] core/types: fix length of BlobVersionedHashed can not be zero (#35065) check the len of BlobVersionedHashed in blob tx. --- core/types/transaction_marshalling.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index f76d4b92e9..e3950b2a76 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -396,6 +396,9 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { if dec.BlobVersionedHashes == nil { return errors.New("missing required field 'blobVersionedHashes' in transaction") } + if len(dec.BlobVersionedHashes) == 0 { + return errors.New("'blobVersionedHashes' must contain at least one hash") + } itx.BlobHashes = dec.BlobVersionedHashes // signature R From dcf6d0c13531a972c686c6df77bb1599aa69a5ba Mon Sep 17 00:00:00 2001 From: ozpool <151670776+ozpool@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:27:11 +0530 Subject: [PATCH 71/76] rpc: accept Windows reset error in websocket read limit test (#34928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary `TestServerWebsocketReadLimit/limit_with_large_request_-_should_fail` is flaky on `windows/amd64` (see [run 25364841576](https://github.com/ethereum/go-ethereum/actions/runs/25364841576/job/74378334589) referenced in #34877): ``` --- FAIL: TestServerWebsocketReadLimit/limit_with_large_request_-_should_fail (0.02s) server_test.go:279: unexpected error for read limit violation: read tcp 127.0.0.1:56703->127.0.0.1:56700: wsarecv: An existing connection was forcibly closed by the remote host. ``` When the server enforces the read limit and tears the connection down, the client's read can race the close frame. On Windows the OS surfaces that race as `wsarecv: An existing connection was forcibly closed by the remote host` instead of the gorilla `CloseError(1009)`, `websocket.ErrReadLimit`, or the POSIX `connection reset by peer` the test already tolerates. This change adds `"forcibly closed"` to the set of acceptable error substrings for the failure case, so the Windows reset is recognized as a valid signal that the server enforced the limit. ### Fixes #34877 ### Test plan - [x] `go test -count=5 -run TestServerWebsocketReadLimit ./rpc/` (darwin/arm64) — pass - [x] `go test ./rpc/...` — pass - [x] `go vet ./rpc/...` / `gofmt -l rpc/server_test.go` — clean - [ ] CI on `windows/amd64` confirms the flake no longer trips --------- Co-authored-by: lightclient --- rpc/server_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rpc/server_test.go b/rpc/server_test.go index a2b8af2b7f..13e723cad0 100644 --- a/rpc/server_test.go +++ b/rpc/server_test.go @@ -307,7 +307,7 @@ func TestServerWebsocketReadLimit(t *testing.T) { t.Fatalf("expected error for request size %d with limit %d, but got none", tc.testSize, tc.readLimit) } // Be tolerant about the exact error surfaced by gorilla/websocket. - // Prefer a CloseError with code 1009, but accept ErrReadLimit or an error string containing 1009/message too big. + // The error text can vary across platforms when the close races the read. var cerr *websocket.CloseError if errors.As(err, &cerr) { if cerr.Code != websocket.CloseMessageTooBig { @@ -316,7 +316,8 @@ func TestServerWebsocketReadLimit(t *testing.T) { } else if !errors.Is(err, websocket.ErrReadLimit) && !strings.Contains(strings.ToLower(err.Error()), "1009") && !strings.Contains(strings.ToLower(err.Error()), "message too big") && - !strings.Contains(strings.ToLower(err.Error()), "connection reset by peer") { + !strings.Contains(strings.ToLower(err.Error()), "connection reset by peer") && + !strings.Contains(strings.ToLower(err.Error()), "forcibly closed") { // Not the error we expect from exceeding the message size limit. t.Fatalf("unexpected error for read limit violation: %v", err) } From f5c62d0552e089634e2146fb0f532fdd028821b4 Mon Sep 17 00:00:00 2001 From: cui Date: Fri, 5 Jun 2026 01:17:46 +0800 Subject: [PATCH 72/76] core/types: BlobHashes should iterate Commitments (#35109) Previously was iterating Blobs, but that could cause panic if the sidecar is malformed. --- core/types/tx_blob.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index 31aadb5419..90603341e6 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -89,7 +89,7 @@ func NewBlobTxSidecar(version byte, blobs []kzg4844.Blob, commitments []kzg4844. func (sc *BlobTxSidecar) BlobHashes() []common.Hash { hasher := sha256.New() h := make([]common.Hash, len(sc.Commitments)) - for i := range sc.Blobs { + for i := range sc.Commitments { h[i] = kzg4844.CalcBlobHashV1(hasher, &sc.Commitments[i]) } return h From bc1967f088469b7d78607b75bd7df3e960d0df82 Mon Sep 17 00:00:00 2001 From: Jonathan Oppenheimer <147infiniti@gmail.com> Date: Thu, 4 Jun 2026 22:22:58 -0400 Subject: [PATCH 73/76] core/state/snapshot: snapshot generation shutdown race condition (#33540) ## Overview This PR fixes a race condition during blockchain shutdown where snapshot generation could continue accessing the trie database after it has been closed, leading to iterator errors. We noticed this in one of our nodes on https://github.com/ava-labs/avalanchego, which relies on an older version of geth with the same issue (so this behavior does happen!). During node shutdown, the following sequence occurs: 1. `BlockChain.Stop()` calls `snaps.Release()` to clean up snapshot resources 2. `Release()` only resets the cache but doesn't stop the generator goroutine 3. The trie database is then closed via `triedb.Close()` 4. The still-running generator attempts to iterate storage tries 5. Iterator fails because the database is closed (`"Generator failed to iterate storage trie"`) ## Problem There are three related bugs: 1. `Release()` doesn't stop generation: The `diskLayer.Release()` method only resets the cache without stopping ongoing snapshot generation, leaving the generator goroutine running after database closure. 2. `stopGeneration()` has an incorrect completion check: The `stopGeneration()` method checks `genMarker != nil` to determine if generation is running. However, `genMarker` is set to nil when generation completes successfully, even though the generator goroutine is still waiting for the abort signal at the end of `generate()`. See line 705 in `generate.go`: https://github.com/ethereum/go-ethereum/blob/eaaa5b716dcf97e94eb17a1469a7385a7101ffab/core/state/snapshot/generate.go#L699-L707 This means `stopGeneration()` returns early without sending the abort signal. 3. Node shutdown doesn't stop generation: During shutdown, no code path calls `stopGeneration()` or sends the abort signal to the generator, causing the generator to access a closed database and error. ## Fix - Modified `diskLayer.Release()` to call `stopGeneration()` before releasing resources - Added cancelation architecture, removing reliance on someone having to wait - Fixed `stopGeneration()` to properly and safely stop snapshot generation - Added `TestGenerateGoroutineLeak` to verify the fix and prevent regression. The test fails without the fix and passes with it. - The test creates a snapshot with active generation, waits for completion, then calls `Release()`, and uses `go.uber.org/goleak` to assert no generator goroutine survives. - Without the fix, the test fails: `Release()` returns without stopping the generator, which stays parked at `generate.go:705` waiting for an abort signal that never comes: ``` --- FAIL: TestGenerateGoroutineLeak (0.88s) generate_test.go: found unexpected goroutines: [Goroutine 6 in state chan receive, with core/state/snapshot.(*diskLayer).generate on top of the stack: core/state/snapshot.(*diskLayer).generate(...) core/state/snapshot/generate.go:705 created by core/state/snapshot.generateSnapshot core/state/snapshot/generate.go:79 ] ``` - With the fix, the test passes: `Release()` -> `stopGeneration()` blocks until the generator goroutine has fully exited, so nothing leaks Note that this fix follows the same pattern used in `Tree.Disable()` in https://github.com/ethereum/go-ethereum/pull/30040, which introduced `stopGeneration()` for use in `Disable()` and `Rebuild()` but didn't address the shutdown path. The test follows the same pattern used in `TestCheckSimBackendGoroutineLeak` --- cmd/keeper/go.sum | 2 + core/state/snapshot/disklayer.go | 48 +++++--- core/state/snapshot/generate.go | 62 ++++------ core/state/snapshot/generate_test.go | 162 +++++++++++++++++---------- core/state/snapshot/journal.go | 18 +-- core/state/snapshot/snapshot.go | 25 ++--- 6 files changed, 179 insertions(+), 138 deletions(-) diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum index 4d58974ab3..51a6a3fad2 100644 --- a/cmd/keeper/go.sum +++ b/cmd/keeper/go.sum @@ -127,6 +127,8 @@ go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisx go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= diff --git a/core/state/snapshot/disklayer.go b/core/state/snapshot/disklayer.go index 202e6c70ed..3a5864f117 100644 --- a/core/state/snapshot/disklayer.go +++ b/core/state/snapshot/disklayer.go @@ -38,9 +38,17 @@ type diskLayer struct { root common.Hash // Root hash of the base snapshot stale bool // Signals that the layer became stale (state progressed) - genMarker []byte // Marker for the state that's indexed during initial layer generation - genPending chan struct{} // Notification channel when generation is done (test synchronicity) - genAbort chan chan *generatorStats // Notification channel to abort generating the snapshot in this layer + genMarker []byte // Marker for the state that's indexed during initial layer generation + genPending chan struct{} // Notification channel when generation is done (test synchronicity) + + // Generator lifecycle management: + // - [cancel] is closed to request termination (broadcast). + // - [done] is closed by the generator goroutine on exit. + cancel chan struct{} + done chan struct{} + cancelOnce sync.Once + + genStats *generatorStats // Stats for snapshot generation (generation aborted/finished if non-nil) lock sync.RWMutex } @@ -49,6 +57,10 @@ type diskLayer struct { // Reset() in order to not leak memory. // OBS: It does not invoke Close on the diskdb func (dl *diskLayer) Release() error { + // Stop any ongoing snapshot generation to prevent it from accessing + // the database after it's closed during shutdown + dl.stopGeneration() + if dl.cache != nil { dl.cache.Reset() } @@ -184,17 +196,27 @@ func (dl *diskLayer) Update(blockHash common.Hash, accounts map[common.Hash][]by return newDiffLayer(dl, blockHash, accounts, storage) } -// stopGeneration aborts the state snapshot generation if it is currently running. +// stopGeneration requests cancellation of any running snapshot generation and +// blocks until the generator goroutine (if running) has fully terminated. +// +// Concurrency guarantees: +// - Thread-safe: May be called concurrently from multiple goroutines +// - Idempotent: Safe to call multiple times; subsequent calls have no effect +// - Blocking: Returns only after the generator goroutine (if any) has exited +// - Safe to call at any time, including when no generation is running +// +// After return, it is **guaranteed** that: +// - The generator goroutine has terminated +// - It is safe to proceed with cleanup operations (e.g. closing databases) func (dl *diskLayer) stopGeneration() { - dl.lock.RLock() - generating := dl.genMarker != nil - dl.lock.RUnlock() - if !generating { + cancel := dl.cancel + done := dl.done + if cancel == nil || done == nil { return } - if dl.genAbort != nil { - abort := make(chan *generatorStats) - dl.genAbort <- abort - <-abort - } + + dl.cancelOnce.Do(func() { + close(cancel) + }) + <-done } diff --git a/core/state/snapshot/generate.go b/core/state/snapshot/generate.go index 01fb55ea4c..2cb4c7d03c 100644 --- a/core/state/snapshot/generate.go +++ b/core/state/snapshot/generate.go @@ -50,6 +50,9 @@ var ( // errMissingTrie is returned if the target trie is missing while the generation // is running. In this case the generation is aborted and wait the new signal. errMissingTrie = errors.New("missing trie") + + // errAborted is returned when snapshot generation was interrupted/aborted + errAborted = errors.New("aborted") ) // generateSnapshot regenerates a brand new snapshot based on an existing state @@ -74,7 +77,8 @@ func generateSnapshot(diskdb ethdb.KeyValueStore, triedb *triedb.Database, cache cache: fastcache.New(cache * 1024 * 1024), genMarker: genMarker, genPending: make(chan struct{}), - genAbort: make(chan chan *generatorStats), + cancel: make(chan struct{}), + done: make(chan struct{}), } go base.generate(stats) log.Debug("Start snapshot generation", "root", root) @@ -467,12 +471,14 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi // checkAndFlush checks if an interruption signal is received or the // batch size has exceeded the allowance. func (dl *diskLayer) checkAndFlush(ctx *generatorContext, current []byte) error { - var abort chan *generatorStats + aborting := false select { - case abort = <-dl.genAbort: + case <-dl.cancel: + aborting = true default: } - if ctx.batch.ValueSize() > ethdb.IdealBatchSize || abort != nil { + + if ctx.batch.ValueSize() > ethdb.IdealBatchSize || aborting { if bytes.Compare(current, dl.genMarker) < 0 { log.Error("Snapshot generator went backwards", "current", fmt.Sprintf("%x", current), "genMarker", fmt.Sprintf("%x", dl.genMarker)) } @@ -490,9 +496,9 @@ func (dl *diskLayer) checkAndFlush(ctx *generatorContext, current []byte) error dl.genMarker = current dl.lock.Unlock() - if abort != nil { + if aborting { ctx.stats.Log("Aborting state snapshot generation", dl.root, current) - return newAbortErr(abort) // bubble up an error for interruption + return errAborted } // Don't hold the iterators too long, release them to let compactor works ctx.reopenIterator(snapAccount) @@ -648,10 +654,11 @@ func generateAccounts(ctx *generatorContext, dl *diskLayer, accMarker []byte) er // gathering and logging, since the method surfs the blocks as they arrive, often // being restarted. func (dl *diskLayer) generate(stats *generatorStats) { - var ( - accMarker []byte - abort chan *generatorStats - ) + if dl.done != nil { + defer close(dl.done) + } + + var accMarker []byte if len(dl.genMarker) > 0 { // []byte{} is the start, use nil for that accMarker = dl.genMarker[:common.HashLength] } @@ -669,15 +676,11 @@ func (dl *diskLayer) generate(stats *generatorStats) { defer ctx.close() if err := generateAccounts(ctx, dl, accMarker); err != nil { - // Extract the received interruption signal if exists - if aerr, ok := err.(*abortErr); ok { - abort = aerr.abort + // Check if error was due to abort + if err == errAborted { + stats.Log("Aborting state snapshot generation", dl.root, dl.genMarker) } - // Aborted by internal error, wait the signal - if abort == nil { - abort = <-dl.genAbort - } - abort <- stats + dl.genStats = stats return } // Snapshot fully generated, set the marker to nil. @@ -686,9 +689,7 @@ func (dl *diskLayer) generate(stats *generatorStats) { journalProgress(ctx.batch, nil, stats) if err := ctx.batch.Write(); err != nil { log.Error("Failed to flush batch", "err", err) - - abort = <-dl.genAbort - abort <- stats + dl.genStats = stats return } ctx.batch.Reset() @@ -698,12 +699,9 @@ func (dl *diskLayer) generate(stats *generatorStats) { dl.lock.Lock() dl.genMarker = nil + dl.genStats = stats close(dl.genPending) dl.lock.Unlock() - - // Someone will be looking for us, wait it out - abort = <-dl.genAbort - abort <- nil } // increaseKey increase the input key by one bit. Return nil if the entire @@ -717,17 +715,3 @@ func increaseKey(key []byte) []byte { } return nil } - -// abortErr wraps an interruption signal received to represent the -// generation is aborted by external processes. -type abortErr struct { - abort chan *generatorStats -} - -func newAbortErr(abort chan *generatorStats) error { - return &abortErr{abort: abort} -} - -func (err *abortErr) Error() string { - return "aborted" -} diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go index 7fb4c152dc..3421169116 100644 --- a/core/state/snapshot/generate_test.go +++ b/core/state/snapshot/generate_test.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/ethereum/go-ethereum/triedb/pathdb" "github.com/holiman/uint256" + "go.uber.org/goleak" ) func hashData(input []byte) common.Hash { @@ -74,10 +75,10 @@ func testGeneration(t *testing.T, scheme string) { } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation with existent flat state. @@ -115,10 +116,10 @@ func testGenerateExistentState(t *testing.T, scheme string) { } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } func checkSnapRoot(t *testing.T, snap *diskLayer, trieRoot common.Hash) { @@ -351,10 +352,10 @@ func testGenerateExistentStateWithWrongStorage(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation with existent flat state, where the flat state @@ -414,10 +415,10 @@ func testGenerateExistentStateWithWrongAccounts(t *testing.T, scheme string) { } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation errors out correctly in case of a missing trie @@ -454,10 +455,10 @@ func testGenerateCorruptAccountTrie(t *testing.T, scheme string) { case <-time.After(time.Second): // Not generated fast enough, hopefully blocked inside on missing trie node fail } - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation errors out correctly in case of a missing root @@ -498,10 +499,10 @@ func testGenerateMissingStorageTrie(t *testing.T, scheme string) { case <-time.After(time.Second): // Not generated fast enough, hopefully blocked inside on missing trie node fail } - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation errors out correctly in case of a missing trie @@ -540,10 +541,10 @@ func testGenerateCorruptStorageTrie(t *testing.T, scheme string) { case <-time.After(time.Second): // Not generated fast enough, hopefully blocked inside on missing trie node fail } - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation when an extra account with storage exists in the snap state. @@ -605,10 +606,10 @@ func testGenerateWithExtraAccounts(t *testing.T, scheme string) { } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } // If we now inspect the snap db, there should exist no extraneous storage items if data := rawdb.ReadStorageSnapshot(helper.diskdb, hashData([]byte("acc-2")), hashData([]byte("b-key-1"))); data != nil { t.Fatalf("expected slot to be removed, got %v", string(data)) @@ -666,10 +667,10 @@ func testGenerateWithManyExtraAccounts(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests this case @@ -715,10 +716,10 @@ func testGenerateWithExtraBeforeAndAfter(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // TestGenerateWithMalformedSnapdata tests what happes if we have some junk @@ -755,10 +756,10 @@ func testGenerateWithMalformedSnapdata(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } // If we now inspect the snap db, there should exist no extraneous storage items if data := rawdb.ReadStorageSnapshot(helper.diskdb, hashData([]byte("acc-2")), hashData([]byte("b-key-1"))); data != nil { t.Fatalf("expected slot to be removed, got %v", string(data)) @@ -792,10 +793,10 @@ func testGenerateFromEmptySnap(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation with existent flat state, where the flat state @@ -843,10 +844,10 @@ func testGenerateWithIncompleteStorage(t *testing.T, scheme string) { t.Errorf("Snapshot generation failed") } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } func incKey(key []byte) []byte { @@ -939,10 +940,10 @@ func testGenerateCompleteSnapshotWithDanglingStorage(t *testing.T, scheme string } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } } // Tests that snapshot generation with dangling storages. Dangling storage means @@ -976,8 +977,49 @@ func testGenerateBrokenSnapshotWithDanglingStorage(t *testing.T, scheme string) } checkSnapRoot(t, snap, root) - // Signal abortion to the generator and wait for it to tear down - stop := make(chan *generatorStats) - snap.genAbort <- stop - <-stop + // Stop the generator (if still running) and wait for it to exit. + if err := snap.Release(); err != nil { + t.Fatal(err) + } +} + +// TestGenerateGoroutineLeak verifies that Release() tears down the generator +// goroutine. Even after generation completes, the goroutine parks waiting for +// an abort signal. If Release() does not stop it, it lingers and can touch the +// database after it has been closed during shutdown. +func TestGenerateGoroutineLeak(t *testing.T) { + testGenerateGoroutineLeak(t, rawdb.HashScheme) + testGenerateGoroutineLeak(t, rawdb.PathScheme) +} + +// generateAndRelease builds a minimal state, runs snapshot generation to +// completion, and releases the resulting disk layer. +func generateAndRelease(t *testing.T, scheme string) { + t.Helper() + + helper := newHelper(scheme) + helper.addTrieAccount("acc-1", &types.StateAccount{Balance: uint256.NewInt(1), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}) + + _, snap := helper.CommitAndGenerate() + + // Wait for generation to run to completion. + select { + case <-snap.genPending: + case <-time.After(3 * time.Second): + t.Fatal("snapshot generation did not complete in time") + } + + if err := snap.Release(); err != nil { + t.Fatalf("Release returned error: %v", err) + } +} + +func testGenerateGoroutineLeak(t *testing.T, scheme string) { + generateAndRelease(t, scheme) + + // Snapshot the current goroutines now berfore verifying the run + // below leaks none of its own. + defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) + + generateAndRelease(t, scheme) } diff --git a/core/state/snapshot/journal.go b/core/state/snapshot/journal.go index 004dd5298a..e69754c320 100644 --- a/core/state/snapshot/journal.go +++ b/core/state/snapshot/journal.go @@ -179,7 +179,8 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *triedb.Database, root comm // if the background generation is allowed if !generator.Done && !noBuild { base.genPending = make(chan struct{}) - base.genAbort = make(chan chan *generatorStats) + base.cancel = make(chan struct{}) + base.done = make(chan struct{}) var origin uint64 if len(generator.Marker) >= 8 { @@ -199,16 +200,9 @@ func loadSnapshot(diskdb ethdb.KeyValueStore, triedb *triedb.Database, root comm // Journal terminates any in-progress snapshot generation, also implicitly pushing // the progress into the database. func (dl *diskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { - // If the snapshot is currently being generated, abort it - var stats *generatorStats - if dl.genAbort != nil { - abort := make(chan *generatorStats) - dl.genAbort <- abort + // If the snapshot is currently being generated, stop it + dl.stopGeneration() - if stats = <-abort; stats != nil { - stats.Log("Journalling in-progress snapshot", dl.root, dl.genMarker) - } - } // Ensure the layer didn't get stale dl.lock.RLock() defer dl.lock.RUnlock() @@ -216,8 +210,8 @@ func (dl *diskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { if dl.stale { return common.Hash{}, ErrSnapshotStale } - // Ensure the generator stats is written even if none was ran this cycle - journalProgress(dl.diskdb, dl.genMarker, stats) + // Ensure the generator marker is written even if none was ran this cycle + journalProgress(dl.diskdb, dl.genMarker, dl.genStats) log.Debug("Journalled disk layer", "root", dl.root) return dl.root, nil diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index f0f6296433..cd0a55fee6 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -492,7 +492,7 @@ func (t *Tree) cap(diff *diffLayer, layers int) *diskLayer { // there's a snapshot being generated currently. In that case, the trie // will move from underneath the generator so we **must** merge all the // partial data down into the snapshot and restart the generation. - if flattened.parent.(*diskLayer).genAbort == nil { + if flattened.parent.(*diskLayer).cancel == nil { return nil } } @@ -520,14 +520,10 @@ func diffToDisk(bottom *diffLayer) *diskLayer { var ( base = bottom.parent.(*diskLayer) batch = base.diskdb.NewBatch() - stats *generatorStats ) - // If the disk layer is running a snapshot generator, abort it - if base.genAbort != nil { - abort := make(chan *generatorStats) - base.genAbort <- abort - stats = <-abort - } + // Attempt to stop generation (if not already stopped) + base.stopGeneration() + // Put the deletion in the batch writer, flush all updates in the final step. rawdb.DeleteSnapshotRoot(batch) @@ -606,8 +602,8 @@ func diffToDisk(bottom *diffLayer) *diskLayer { // Update the snapshot block marker and write any remainder data rawdb.WriteSnapshotRoot(batch, bottom.root) - // Write out the generator progress marker and report - journalProgress(batch, base.genMarker, stats) + // Write out the generator progress marker + journalProgress(batch, base.genMarker, base.genStats) // Flush all the updates in the single db operation. Ensure the // disk layer transition is atomic. @@ -626,12 +622,13 @@ func diffToDisk(bottom *diffLayer) *diskLayer { // If snapshot generation hasn't finished yet, port over all the starts and // continue where the previous round left off. // - // Note, the `base.genAbort` comparison is not used normally, it's checked + // Note, the `base.genPending` comparison is not used normally, it's checked // to allow the tests to play with the marker without triggering this path. - if base.genMarker != nil && base.genAbort != nil { + if base.genMarker != nil && base.genPending != nil { res.genMarker = base.genMarker - res.genAbort = make(chan chan *generatorStats) - go res.generate(stats) + res.cancel = make(chan struct{}) + res.done = make(chan struct{}) + go res.generate(base.genStats) } return res } From 13d8df63f45125c203bfa20d8b4929aa869b17d5 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 5 Jun 2026 10:44:41 +0800 Subject: [PATCH 74/76] core/types/bal: improve the bal validation (#35110) https://github.com/ethereum/EIPs/commit/cb1364d60e14a24fe1a94d4303661cdbf8c68271 --- core/types/bal/bal_encoding.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 612d2f8777..522192cee8 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -178,6 +178,11 @@ func isStrictlySortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool { // which are ordered ascending by transaction index and contain no duplicate // modifications for a given index. func (e *encodingSlotChanges) validate(maxBALIndex int) error { + // Each SlotChanges entry MUST contain at least one StorageChange. + if len(e.SlotChanges) == 0 { + return errors.New("empty slot changes") + } + // Each storage key MUST appear at most once in storage_changes per account. if !isStrictlySortedFunc(e.SlotChanges, func(a, b encodingStorageWrite) int { return cmp.Compare(a.BlockAccessIndex, b.BlockAccessIndex) }) { From 1cb6c8346395407b856caad253672f4c4abedff2 Mon Sep 17 00:00:00 2001 From: jeevan-sid Date: Fri, 5 Jun 2026 16:44:14 +0530 Subject: [PATCH 75/76] feat: disallow noreceipts profile in erastore --- core/rawdb/eradb/eradb.go | 78 ++++++++++++++++++++-------------- core/rawdb/eradb/eradb_test.go | 28 ++++++++++++ 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/core/rawdb/eradb/eradb.go b/core/rawdb/eradb/eradb.go index d715c824ed..21db083a10 100644 --- a/core/rawdb/eradb/eradb.go +++ b/core/rawdb/eradb/eradb.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// Package eradb implements a history backend using era1 files. +// Package eradb implements a history backend using era1 and ere files. package eradb import ( @@ -23,10 +23,12 @@ import ( "fmt" "io/fs" "path/filepath" + "strings" "sync" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/internal/era" + "github.com/ethereum/go-ethereum/internal/era/execdb" "github.com/ethereum/go-ethereum/internal/era/onedb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" @@ -36,7 +38,7 @@ const openFileLimit = 64 var errClosed = errors.New("era store is closed") -// Store manages read access to a directory of era1 files. +// Store manages read access to a directory of era1 and ere files. // The getter methods are thread-safe. type Store struct { datadir string @@ -52,7 +54,7 @@ type Store struct { type fileCacheEntry struct { refcount int // reference count. This is protected by Store.mu! opened chan struct{} // signals opening of file has completed - file *onedb.Era // the file + file era.Era // the file (era1 or ere) err error // error from opening the file } @@ -250,7 +252,7 @@ func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileC } // fileOpened is called after an era file has been successfully opened. -func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era) { +func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file era.Era) { db.mu.Lock() defer db.mu.Unlock() @@ -283,32 +285,46 @@ func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error entry.err = err } -func (db *Store) openEraFile(epoch uint64) (*onedb.Era, error) { - // File name scheme is --. - glob := fmt.Sprintf("*-%05d-*.era1", epoch) - matches, err := filepath.Glob(filepath.Join(db.datadir, glob)) - if err != nil { - return nil, err +func (db *Store) openEraFile(epoch uint64) (era.Era, error) { + // File name scheme is --. + // Try era1 first, then ere. + for _, ext := range []string{"era1", "ere"} { + glob := fmt.Sprintf("*-%05d-*.%s", epoch, ext) + matches, err := filepath.Glob(filepath.Join(db.datadir, glob)) + if err != nil { + return nil, err + } + if len(matches) > 1 { + return nil, fmt.Errorf("multiple %s files found for epoch %d", ext, epoch) + } + if len(matches) == 0 { + continue + } + filename := matches[0] + var e era.Era + switch ext { + case "era1": + e, err = onedb.Open(filename) + case "ere": + // The era store serves receipts via RPC. Reject noreceipts + // profiles to avoid silently returning empty receipt data. + if strings.Contains(filepath.Base(filename), "-noreceipts") { + return nil, fmt.Errorf("era store does not support noreceipts profile: %s", filepath.Base(filename)) + } + e, err = execdb.Open(filename) + } + if err != nil { + return nil, err + } + // Sanity-check start block. + if e.Start()%uint64(era.MaxSize) != 0 { + e.Close() + return nil, fmt.Errorf("%s file has invalid boundary. %d %% %d != 0", ext, e.Start(), era.MaxSize) + } + log.Debug("Opened era file", "type", ext, "epoch", epoch) + return e, nil } - if len(matches) > 1 { - return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch) - } - if len(matches) == 0 { - return nil, fs.ErrNotExist - } - filename := matches[0] - - e, err := onedb.Open(filename) - if err != nil { - return nil, err - } - // Sanity-check start block. - if e.Start()%uint64(era.MaxSize) != 0 { - e.Close() - return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxSize) - } - log.Debug("Opened era1 file", "epoch", epoch) - return e.(*onedb.Era), nil + return nil, fs.ErrNotExist } // doneWithFile signals that the caller has finished using a file. @@ -339,9 +355,9 @@ func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) { closeErr := entry.file.Close() if closeErr == nil { - log.Debug("Closed era1 file", "epoch", epoch) + log.Debug("Closed era file", "epoch", epoch) } else { - log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr) + log.Warn("Error closing era file", "epoch", epoch, "err", closeErr) } return true } diff --git a/core/rawdb/eradb/eradb_test.go b/core/rawdb/eradb/eradb_test.go index 41047dbbe9..5c442b9695 100644 --- a/core/rawdb/eradb/eradb_test.go +++ b/core/rawdb/eradb/eradb_test.go @@ -17,6 +17,8 @@ package eradb import ( + "os" + "path/filepath" "sync" "testing" @@ -48,6 +50,32 @@ func TestEraDatabase(t *testing.T) { assert.Equal(t, 3, len(receipts), "receipts length mismatch") } +func TestEraStoreRejectsNoReceiptsProfile(t *testing.T) { + dir := t.TempDir() + stubName := "mainnet-00000-deadbeef-noreceipts.ere" + stubPath := filepath.Join(dir, stubName) + + // Write a non-empty stub so the glob finds the file. Contents don't matter + // because the noreceipts check fires before execdb.Open is called. + err := os.WriteFile(stubPath, []byte("stub"), 0644) + require.NoError(t, err) + + db, err := New(dir) + require.NoError(t, err) + defer db.Close() + + // Any block in epoch 0 should trigger the same rejection. + const block = uint64(0) + + _, err = db.GetRawBody(block) + require.Error(t, err) + assert.Contains(t, err.Error(), "era store does not support noreceipts profile") + + _, err = db.GetRawReceipts(block) + require.Error(t, err) + assert.Contains(t, err.Error(), "era store does not support noreceipts profile") +} + func TestEraDatabaseConcurrentOpen(t *testing.T) { db, err := New("testdata") require.NoError(t, err) From 85d4b792e6e5acc127722a3b7e731c4f2ea90fe2 Mon Sep 17 00:00:00 2001 From: jeevan-sid Date: Fri, 5 Jun 2026 17:18:10 +0530 Subject: [PATCH 76/76] feat: lint --- cmd/keeper/go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/keeper/go.mod b/cmd/keeper/go.mod index b2caee8b63..e51dac185e 100644 --- a/cmd/keeper/go.mod +++ b/cmd/keeper/go.mod @@ -27,6 +27,7 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect