From 2d95ba7d15879677cb451bbaa5d2c34c2b4f323d Mon Sep 17 00:00:00 2001 From: Daniel Katzan <108216499+dkatzan@users.noreply.github.com> Date: Thu, 31 Jul 2025 08:34:17 +0700 Subject: [PATCH 1/8] core/types: expose sigHash as Hash for SetCodeAuthorization (#32298) --- core/types/tx_setcode.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/types/tx_setcode.go b/core/types/tx_setcode.go index b8e38ef1f7..f2281d4ae7 100644 --- a/core/types/tx_setcode.go +++ b/core/types/tx_setcode.go @@ -89,7 +89,7 @@ type authorizationMarshaling struct { // SignSetCode creates a signed the SetCode authorization. func SignSetCode(prv *ecdsa.PrivateKey, auth SetCodeAuthorization) (SetCodeAuthorization, error) { - sighash := auth.sigHash() + sighash := auth.SigHash() sig, err := crypto.Sign(sighash[:], prv) if err != nil { return SetCodeAuthorization{}, err @@ -105,7 +105,8 @@ func SignSetCode(prv *ecdsa.PrivateKey, auth SetCodeAuthorization) (SetCodeAutho }, nil } -func (a *SetCodeAuthorization) sigHash() common.Hash { +// SigHash returns the hash of SetCodeAuthorization for signing. +func (a *SetCodeAuthorization) SigHash() common.Hash { return prefixedRlpHash(0x05, []any{ a.ChainID, a.Address, @@ -115,7 +116,7 @@ func (a *SetCodeAuthorization) sigHash() common.Hash { // Authority recovers the the authorizing account of an authorization. func (a *SetCodeAuthorization) Authority() (common.Address, error) { - sighash := a.sigHash() + sighash := a.SigHash() if !crypto.ValidateSignatureValues(a.V, a.R.ToBig(), a.S.ToBig(), true) { return common.Address{}, ErrInvalidSig } From 0814d991aba32ce4f2a0253f1c06c2da22788408 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 31 Jul 2025 09:48:31 +0800 Subject: [PATCH 2/8] common/hexutil: replace customized bit sizer with bit.Uintsize (#32304) --- common/hexutil/hexutil.go | 5 ++--- common/hexutil/json_test.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/hexutil/hexutil.go b/common/hexutil/hexutil.go index d3201850a8..d6b6b867f2 100644 --- a/common/hexutil/hexutil.go +++ b/common/hexutil/hexutil.go @@ -34,11 +34,10 @@ import ( "encoding/hex" "fmt" "math/big" + "math/bits" "strconv" ) -const uintBits = 32 << (uint64(^uint(0)) >> 63) - // Errors var ( ErrEmptyString = &decError{"empty hex string"} @@ -48,7 +47,7 @@ var ( ErrEmptyNumber = &decError{"hex string \"0x\""} ErrLeadingZero = &decError{"hex number with leading zero digits"} ErrUint64Range = &decError{"hex number > 64 bits"} - ErrUintRange = &decError{fmt.Sprintf("hex number > %d bits", uintBits)} + ErrUintRange = &decError{fmt.Sprintf("hex number > %d bits", bits.UintSize)} ErrBig256Range = &decError{"hex number > 256 bits"} ) diff --git a/common/hexutil/json_test.go b/common/hexutil/json_test.go index 7cca300951..a014438458 100644 --- a/common/hexutil/json_test.go +++ b/common/hexutil/json_test.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "math/big" + "math/bits" "testing" "github.com/holiman/uint256" @@ -384,7 +385,7 @@ func TestUnmarshalUint(t *testing.T) { for _, test := range unmarshalUintTests { var v Uint err := json.Unmarshal([]byte(test.input), &v) - if uintBits == 32 && test.wantErr32bit != nil { + if bits.UintSize == 32 && test.wantErr32bit != nil { checkError(t, test.input, err, test.wantErr32bit) continue } From 4d9d72806ccd09ce0d452abf77dc77aa3413b972 Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 31 Jul 2025 09:53:31 +0800 Subject: [PATCH 3/8] accounts/abi: precompile regex (#32301) --- accounts/abi/abigen/bind.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/accounts/abi/abigen/bind.go b/accounts/abi/abigen/bind.go index 56e5e214de..08624b04ba 100644 --- a/accounts/abi/abigen/bind.go +++ b/accounts/abi/abigen/bind.go @@ -33,6 +33,10 @@ import ( "github.com/ethereum/go-ethereum/log" ) +var ( + intRegex = regexp.MustCompile(`(u)?int([0-9]*)`) +) + func isKeyWord(arg string) bool { switch arg { case "break": @@ -299,7 +303,7 @@ func bindBasicType(kind abi.Type) string { case abi.AddressTy: return "common.Address" case abi.IntTy, abi.UintTy: - parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String()) + parts := intRegex.FindStringSubmatch(kind.String()) switch parts[2] { case "8", "16", "32", "64": return fmt.Sprintf("%sint%s", parts[1], parts[2]) From d4a3bf1b23e3972fb82e085c0e29fe2c4647ed5c Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 31 Jul 2025 12:13:36 +0200 Subject: [PATCH 4/8] cmd/devp2p/internal/v4test: add test for ENRRequest (#32303) This adds a cross-client protocol test for a recently discovered bug in Nethermind. --- cmd/devp2p/internal/v4test/discv4tests.go | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/cmd/devp2p/internal/v4test/discv4tests.go b/cmd/devp2p/internal/v4test/discv4tests.go index 963df6cdbc..de97d7a276 100644 --- a/cmd/devp2p/internal/v4test/discv4tests.go +++ b/cmd/devp2p/internal/v4test/discv4tests.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/utesting" "github.com/ethereum/go-ethereum/p2p/discover/v4wire" + "github.com/ethereum/go-ethereum/p2p/enode" ) const ( @@ -501,6 +502,36 @@ func FindnodeAmplificationWrongIP(t *utesting.T) { } } +func ENRRequest(t *utesting.T) { + t.Log(`This test sends an ENRRequest packet and expects a response containing a valid ENR.`) + + te := newTestEnv(Remote, Listen1, Listen2) + defer te.close() + bond(t, te) + + req := &v4wire.ENRRequest{Expiration: futureExpiration()} + hash := te.send(te.l1, req) + + response, _, err := te.read(te.l1) + if err != nil { + t.Fatal("read error:", err) + } + enrResp, ok := response.(*v4wire.ENRResponse) + if !ok { + t.Fatalf("expected ENRResponse packet, got %T", response) + } + if !bytes.Equal(enrResp.ReplyTok, hash) { + t.Errorf("wrong hash in response packet: got %x, want %x", enrResp.ReplyTok, hash) + } + node, err := enode.New(enode.ValidSchemes, &enrResp.Record) + if err != nil { + t.Errorf("invalid record in response: %v", err) + } + if node.ID() != te.remote.ID() { + t.Errorf("wrong node ID in response: got %v, want %v", node.ID(), te.remote.ID()) + } +} + var AllTests = []utesting.Test{ {Name: "Ping/Basic", Fn: BasicPing}, {Name: "Ping/WrongTo", Fn: PingWrongTo}, @@ -510,6 +541,7 @@ var AllTests = []utesting.Test{ {Name: "Ping/PastExpiration", Fn: PingPastExpiration}, {Name: "Ping/WrongPacketType", Fn: WrongPacketType}, {Name: "Ping/BondThenPingWithWrongFrom", Fn: BondThenPingWithWrongFrom}, + {Name: "ENRRequest", Fn: ENRRequest}, {Name: "Findnode/WithoutEndpointProof", Fn: FindnodeWithoutEndpointProof}, {Name: "Findnode/BasicFindnode", Fn: BasicFindnode}, {Name: "Findnode/UnsolicitedNeighbors", Fn: UnsolicitedNeighbors}, From 23da91f73b83eb1d101889b2f773c6f0a80a8f15 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Fri, 1 Aug 2025 10:23:23 +0800 Subject: [PATCH 5/8] trie: reduce the memory allocation in trie hashing (#31902) This pull request optimizes trie hashing by reducing memory allocation overhead. Specifically: - define a fullNodeEncoder pool to reuse encoders and avoid memory allocations. - simplify the encoding logic for shortNode and fullNode by getting rid of the Go interfaces. --- trie/hasher.go | 181 ++++++++++++++++++++++++---------------------- trie/iterator.go | 6 +- trie/node.go | 4 - trie/node_enc.go | 23 ++++-- trie/proof.go | 14 +--- trie/trie.go | 6 +- trie/trie_test.go | 1 - 7 files changed, 122 insertions(+), 113 deletions(-) diff --git a/trie/hasher.go b/trie/hasher.go index 16606808c9..a2a1f5b662 100644 --- a/trie/hasher.go +++ b/trie/hasher.go @@ -17,6 +17,8 @@ package trie import ( + "bytes" + "fmt" "sync" "github.com/ethereum/go-ethereum/crypto" @@ -54,7 +56,7 @@ func returnHasherToPool(h *hasher) { } // hash collapses a node down into a hash node. -func (h *hasher) hash(n node, force bool) node { +func (h *hasher) hash(n node, force bool) []byte { // Return the cached hash if it's available if hash, _ := n.cache(); hash != nil { return hash @@ -62,101 +64,110 @@ func (h *hasher) hash(n node, force bool) node { // Trie not processed yet, walk the children switch n := n.(type) { case *shortNode: - collapsed := h.hashShortNodeChildren(n) - hashed := h.shortnodeToHash(collapsed, force) - if hn, ok := hashed.(hashNode); ok { - n.flags.hash = hn - } else { - n.flags.hash = nil + enc := h.encodeShortNode(n) + if len(enc) < 32 && !force { + // Nodes smaller than 32 bytes are embedded directly in their parent. + // In such cases, return the raw encoded blob instead of the node hash. + // It's essential to deep-copy the node blob, as the underlying buffer + // of enc will be reused later. + buf := make([]byte, len(enc)) + copy(buf, enc) + return buf } - return hashed + hash := h.hashData(enc) + n.flags.hash = hash + return hash + case *fullNode: - collapsed := h.hashFullNodeChildren(n) - hashed := h.fullnodeToHash(collapsed, force) - if hn, ok := hashed.(hashNode); ok { - n.flags.hash = hn - } else { - n.flags.hash = nil + enc := h.encodeFullNode(n) + if len(enc) < 32 && !force { + // Nodes smaller than 32 bytes are embedded directly in their parent. + // In such cases, return the raw encoded blob instead of the node hash. + // It's essential to deep-copy the node blob, as the underlying buffer + // of enc will be reused later. + buf := make([]byte, len(enc)) + copy(buf, enc) + return buf } - return hashed - default: - // Value and hash nodes don't have children, so they're left as were + hash := h.hashData(enc) + n.flags.hash = hash + return hash + + case hashNode: + // hash nodes don't have children, so they're left as were return n - } -} -// hashShortNodeChildren returns a copy of the supplied shortNode, with its child -// being replaced by either the hash or an embedded node if the child is small. -func (h *hasher) hashShortNodeChildren(n *shortNode) *shortNode { - var collapsed shortNode - collapsed.Key = hexToCompact(n.Key) - switch n.Val.(type) { - case *fullNode, *shortNode: - collapsed.Val = h.hash(n.Val, false) default: - collapsed.Val = n.Val + panic(fmt.Errorf("unexpected node type, %T", n)) } - return &collapsed } -// hashFullNodeChildren returns a copy of the supplied fullNode, with its child -// being replaced by either the hash or an embedded node if the child is small. -func (h *hasher) hashFullNodeChildren(n *fullNode) *fullNode { - var children [17]node +// encodeShortNode encodes the provided shortNode into the bytes. Notably, the +// return slice must be deep-copied explicitly, otherwise the underlying slice +// will be reused later. +func (h *hasher) encodeShortNode(n *shortNode) []byte { + // Encode leaf node + if hasTerm(n.Key) { + var ln leafNodeEncoder + ln.Key = hexToCompact(n.Key) + ln.Val = n.Val.(valueNode) + ln.encode(h.encbuf) + return h.encodedBytes() + } + // Encode extension node + var en extNodeEncoder + en.Key = hexToCompact(n.Key) + en.Val = h.hash(n.Val, false) + en.encode(h.encbuf) + return h.encodedBytes() +} + +// fnEncoderPool is the pool for storing shared fullNode encoder to mitigate +// the significant memory allocation overhead. +var fnEncoderPool = sync.Pool{ + New: func() interface{} { + var enc fullnodeEncoder + return &enc + }, +} + +// encodeFullNode encodes the provided fullNode into the bytes. Notably, the +// return slice must be deep-copied explicitly, otherwise the underlying slice +// will be reused later. +func (h *hasher) encodeFullNode(n *fullNode) []byte { + fn := fnEncoderPool.Get().(*fullnodeEncoder) + fn.reset() + if h.parallel { var wg sync.WaitGroup for i := 0; i < 16; i++ { - if child := n.Children[i]; child != nil { - wg.Add(1) - go func(i int) { - hasher := newHasher(false) - children[i] = hasher.hash(child, false) - returnHasherToPool(hasher) - wg.Done() - }(i) - } else { - children[i] = nilValueNode + if n.Children[i] == nil { + continue } + wg.Add(1) + go func(i int) { + defer wg.Done() + + h := newHasher(false) + fn.Children[i] = h.hash(n.Children[i], false) + returnHasherToPool(h) + }(i) } wg.Wait() } else { for i := 0; i < 16; i++ { if child := n.Children[i]; child != nil { - children[i] = h.hash(child, false) - } else { - children[i] = nilValueNode + fn.Children[i] = h.hash(child, false) } } } if n.Children[16] != nil { - children[16] = n.Children[16] + fn.Children[16] = n.Children[16].(valueNode) } - return &fullNode{flags: nodeFlag{}, Children: children} -} + fn.encode(h.encbuf) + fnEncoderPool.Put(fn) -// shortNodeToHash computes the hash of the given shortNode. The shortNode must -// first be collapsed, with its key converted to compact form. If the RLP-encoded -// node data is smaller than 32 bytes, the node itself is returned. -func (h *hasher) shortnodeToHash(n *shortNode, force bool) node { - n.encode(h.encbuf) - enc := h.encodedBytes() - - if len(enc) < 32 && !force { - return n // Nodes smaller than 32 bytes are stored inside their parent - } - return h.hashData(enc) -} - -// fullnodeToHash computes the hash of the given fullNode. If the RLP-encoded -// node data is smaller than 32 bytes, the node itself is returned. -func (h *hasher) fullnodeToHash(n *fullNode, force bool) node { - n.encode(h.encbuf) - enc := h.encodedBytes() - - if len(enc) < 32 && !force { - return n // Nodes smaller than 32 bytes are stored inside their parent - } - return h.hashData(enc) + return h.encodedBytes() } // encodedBytes returns the result of the last encoding operation on h.encbuf. @@ -175,9 +186,10 @@ func (h *hasher) encodedBytes() []byte { return h.tmp } -// hashData hashes the provided data -func (h *hasher) hashData(data []byte) hashNode { - n := make(hashNode, 32) +// hashData hashes the provided data. It is safe to modify the returned slice after +// the function returns. +func (h *hasher) hashData(data []byte) []byte { + n := make([]byte, 32) h.sha.Reset() h.sha.Write(data) h.sha.Read(n) @@ -192,20 +204,17 @@ func (h *hasher) hashDataTo(dst, data []byte) { h.sha.Read(dst) } -// proofHash is used to construct trie proofs, and returns the 'collapsed' -// node (for later RLP encoding) as well as the hashed node -- unless the -// node is smaller than 32 bytes, in which case it will be returned as is. -// This method does not do anything on value- or hash-nodes. -func (h *hasher) proofHash(original node) (collapsed, hashed node) { +// proofHash is used to construct trie proofs, returning the rlp-encoded node blobs. +// Note, only resolved node (shortNode or fullNode) is expected for proofing. +// +// It is safe to modify the returned slice after the function returns. +func (h *hasher) proofHash(original node) []byte { switch n := original.(type) { case *shortNode: - sn := h.hashShortNodeChildren(n) - return sn, h.shortnodeToHash(sn, false) + return bytes.Clone(h.encodeShortNode(n)) case *fullNode: - fn := h.hashFullNodeChildren(n) - return fn, h.fullnodeToHash(fn, false) + return bytes.Clone(h.encodeFullNode(n)) default: - // Value and hash nodes don't have children, so they're left as were - return n, n + panic(fmt.Errorf("unexpected node type, %T", original)) } } diff --git a/trie/iterator.go b/trie/iterator.go index fa01611063..e6fedf2430 100644 --- a/trie/iterator.go +++ b/trie/iterator.go @@ -240,9 +240,9 @@ func (it *nodeIterator) LeafProof() [][]byte { for i, item := range it.stack[:len(it.stack)-1] { // Gather nodes that end up as hash nodes (or the root) - node, hashed := hasher.proofHash(item.node) - if _, ok := hashed.(hashNode); ok || i == 0 { - proofs = append(proofs, nodeToBytes(node)) + enc := hasher.proofHash(item.node) + if len(enc) >= 32 || i == 0 { + proofs = append(proofs, enc) } } return proofs diff --git a/trie/node.go b/trie/node.go index 96f077ebbb..74fac4fd4e 100644 --- a/trie/node.go +++ b/trie/node.go @@ -68,10 +68,6 @@ type ( } ) -// nilValueNode is used when collapsing internal trie nodes for hashing, since -// unset children need to serialize correctly. -var nilValueNode = valueNode(nil) - // EncodeRLP encodes a full node into the consensus RLP format. func (n *fullNode) EncodeRLP(w io.Writer) error { eb := rlp.NewEncoderBuffer(w) diff --git a/trie/node_enc.go b/trie/node_enc.go index c95587eeab..02b93ee6f3 100644 --- a/trie/node_enc.go +++ b/trie/node_enc.go @@ -42,18 +42,29 @@ func (n *fullNode) encode(w rlp.EncoderBuffer) { func (n *fullnodeEncoder) encode(w rlp.EncoderBuffer) { offset := w.List() - for _, c := range n.Children { - if c == nil { + for i, c := range n.Children { + if len(c) == 0 { w.Write(rlp.EmptyString) - } else if len(c) < 32 { - w.Write(c) // rawNode } else { - w.WriteBytes(c) // hashNode + // valueNode or hashNode + if i == 16 || len(c) >= 32 { + w.WriteBytes(c) + } else { + w.Write(c) // rawNode + } } } w.ListEnd(offset) } +func (n *fullnodeEncoder) reset() { + for i, c := range n.Children { + if len(c) != 0 { + n.Children[i] = n.Children[i][:0] + } + } +} + func (n *shortNode) encode(w rlp.EncoderBuffer) { offset := w.List() w.WriteBytes(n.Key) @@ -70,7 +81,7 @@ func (n *extNodeEncoder) encode(w rlp.EncoderBuffer) { w.WriteBytes(n.Key) if n.Val == nil { - w.Write(rlp.EmptyString) + w.Write(rlp.EmptyString) // theoretically impossible to happen } else if len(n.Val) < 32 { w.Write(n.Val) // rawNode } else { diff --git a/trie/proof.go b/trie/proof.go index 751d6f620f..53b7acc30c 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -85,16 +86,9 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { defer returnHasherToPool(hasher) for i, n := range nodes { - var hn node - n, hn = hasher.proofHash(n) - if hash, ok := hn.(hashNode); ok || i == 0 { - // If the node's database encoding is a hash (or is the - // root node), it becomes a proof element. - enc := nodeToBytes(n) - if !ok { - hash = hasher.hashData(enc) - } - proofDb.Put(hash, enc) + enc := hasher.proofHash(n) + if len(enc) >= 32 || i == 0 { + proofDb.Put(crypto.Keccak256(enc), enc) } } return nil diff --git a/trie/trie.go b/trie/trie.go index fdb4da9be4..222bf8b1f0 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -626,7 +626,7 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) { // Hash returns the root hash of the trie. It does not write to the // database and can be used even if the trie doesn't have one. func (t *Trie) Hash() common.Hash { - return common.BytesToHash(t.hashRoot().(hashNode)) + return common.BytesToHash(t.hashRoot()) } // Commit collects all dirty nodes in the trie and replaces them with the @@ -677,9 +677,9 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) { } // hashRoot calculates the root hash of the given trie -func (t *Trie) hashRoot() node { +func (t *Trie) hashRoot() []byte { if t.root == nil { - return hashNode(types.EmptyRootHash.Bytes()) + return types.EmptyRootHash.Bytes() } // If the number of changes is below 100, we let one thread handle it h := newHasher(t.unhashed >= 100) diff --git a/trie/trie_test.go b/trie/trie_test.go index b806ae6b0c..edd85677fe 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -863,7 +863,6 @@ func (s *spongeDb) Flush() { s.sponge.Write([]byte(key)) s.sponge.Write([]byte(s.values[key])) } - fmt.Println(len(s.keys)) } // spongeBatch is a dummy batch which immediately writes to the underlying spongedb From 17d65e9451111288d96f8fb6befb8544a94b6d50 Mon Sep 17 00:00:00 2001 From: lmittmann <3458786+lmittmann@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:57:38 +0200 Subject: [PATCH 6/8] core/vm: add configurable jumpdest analysis cache (#32143) This adds a method on vm.EVM to set the jumpdest cache implementation. It can be used to maintain an analysis cache across VM invocations, to improve performance by skipping the analysis for already known contracts. --------- Co-authored-by: lmittmann Co-authored-by: Felix Lange --- core/vm/analysis_legacy.go | 20 +++++++------- core/vm/analysis_legacy_test.go | 2 +- core/vm/contract.go | 16 +++++------ core/vm/evm.go | 12 ++++++--- core/vm/jumpdests.go | 47 +++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 core/vm/jumpdests.go diff --git a/core/vm/analysis_legacy.go b/core/vm/analysis_legacy.go index 38af9084ac..a445e2048e 100644 --- a/core/vm/analysis_legacy.go +++ b/core/vm/analysis_legacy.go @@ -25,16 +25,16 @@ const ( set7BitsMask = uint16(0b111_1111) ) -// bitvec is a bit vector which maps bytes in a program. +// BitVec is a bit vector which maps bytes in a program. // An unset bit means the byte is an opcode, a set bit means // it's data (i.e. argument of PUSHxx). -type bitvec []byte +type BitVec []byte -func (bits bitvec) set1(pos uint64) { +func (bits BitVec) set1(pos uint64) { bits[pos/8] |= 1 << (pos % 8) } -func (bits bitvec) setN(flag uint16, pos uint64) { +func (bits BitVec) setN(flag uint16, pos uint64) { a := flag << (pos % 8) bits[pos/8] |= byte(a) if b := byte(a >> 8); b != 0 { @@ -42,13 +42,13 @@ func (bits bitvec) setN(flag uint16, pos uint64) { } } -func (bits bitvec) set8(pos uint64) { +func (bits BitVec) set8(pos uint64) { a := byte(0xFF << (pos % 8)) bits[pos/8] |= a bits[pos/8+1] = ^a } -func (bits bitvec) set16(pos uint64) { +func (bits BitVec) set16(pos uint64) { a := byte(0xFF << (pos % 8)) bits[pos/8] |= a bits[pos/8+1] = 0xFF @@ -56,23 +56,23 @@ func (bits bitvec) set16(pos uint64) { } // codeSegment checks if the position is in a code segment. -func (bits *bitvec) codeSegment(pos uint64) bool { +func (bits *BitVec) codeSegment(pos uint64) bool { return (((*bits)[pos/8] >> (pos % 8)) & 1) == 0 } // codeBitmap collects data locations in code. -func codeBitmap(code []byte) bitvec { +func codeBitmap(code []byte) BitVec { // The bitmap is 4 bytes longer than necessary, in case the code // ends with a PUSH32, the algorithm will set bits on the // bitvector outside the bounds of the actual code. - bits := make(bitvec, len(code)/8+1+4) + bits := make(BitVec, len(code)/8+1+4) return codeBitmapInternal(code, bits) } // codeBitmapInternal is the internal implementation of codeBitmap. // It exists for the purpose of being able to run benchmark tests // without dynamic allocations affecting the results. -func codeBitmapInternal(code, bits bitvec) bitvec { +func codeBitmapInternal(code, bits BitVec) BitVec { for pc := uint64(0); pc < uint64(len(code)); { op := OpCode(code[pc]) pc++ diff --git a/core/vm/analysis_legacy_test.go b/core/vm/analysis_legacy_test.go index 471d2b4ffb..f84a4abc92 100644 --- a/core/vm/analysis_legacy_test.go +++ b/core/vm/analysis_legacy_test.go @@ -90,7 +90,7 @@ func BenchmarkJumpdestOpAnalysis(bench *testing.B) { for i := range code { code[i] = byte(op) } - bits := make(bitvec, len(code)/8+1+4) + bits := make(BitVec, len(code)/8+1+4) b.ResetTimer() for i := 0; i < b.N; i++ { clear(bits) diff --git a/core/vm/contract.go b/core/vm/contract.go index 0eaa91d959..165ca833f8 100644 --- a/core/vm/contract.go +++ b/core/vm/contract.go @@ -31,8 +31,8 @@ type Contract struct { caller common.Address address common.Address - jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis. - analysis bitvec // Locally cached result of JUMPDEST analysis + jumpDests JumpDestCache // Aggregated result of JUMPDEST analysis. + analysis BitVec // Locally cached result of JUMPDEST analysis Code []byte CodeHash common.Hash @@ -47,15 +47,15 @@ type Contract struct { } // NewContract returns a new contract environment for the execution of EVM. -func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests map[common.Hash]bitvec) *Contract { - // Initialize the jump analysis map if it's nil, mostly for tests +func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) *Contract { + // Initialize the jump analysis cache if it's nil, mostly for tests if jumpDests == nil { - jumpDests = make(map[common.Hash]bitvec) + jumpDests = newMapJumpDests() } return &Contract{ caller: caller, address: address, - jumpdests: jumpDests, + jumpDests: jumpDests, Gas: gas, value: value, } @@ -87,12 +87,12 @@ func (c *Contract) isCode(udest uint64) bool { // contracts ( not temporary initcode), we store the analysis in a map if c.CodeHash != (common.Hash{}) { // Does parent context have the analysis? - analysis, exist := c.jumpdests[c.CodeHash] + analysis, exist := c.jumpDests.Load(c.CodeHash) if !exist { // Do the analysis and save in parent context // We do not need to store it in c.analysis analysis = codeBitmap(c.Code) - c.jumpdests[c.CodeHash] = analysis + c.jumpDests.Store(c.CodeHash, analysis) } // Also stash it in current contract for faster access c.analysis = analysis diff --git a/core/vm/evm.go b/core/vm/evm.go index b45a434545..143b7e08a2 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -122,9 +122,8 @@ type EVM struct { // precompiles holds the precompiled contracts for the current epoch precompiles map[common.Address]PrecompiledContract - // jumpDests is the aggregated result of JUMPDEST analysis made through - // the life cycle of EVM. - jumpDests map[common.Hash]bitvec + // jumpDests stores results of JUMPDEST analysis. + jumpDests JumpDestCache } // NewEVM constructs an EVM instance with the supplied block context, state @@ -138,7 +137,7 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon Config: config, chainConfig: chainConfig, chainRules: chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time), - jumpDests: make(map[common.Hash]bitvec), + jumpDests: newMapJumpDests(), } evm.precompiles = activePrecompiledContracts(evm.chainRules) evm.interpreter = NewEVMInterpreter(evm) @@ -152,6 +151,11 @@ func (evm *EVM) SetPrecompiles(precompiles PrecompiledContracts) { evm.precompiles = precompiles } +// SetJumpDestCache configures the analysis cache. +func (evm *EVM) SetJumpDestCache(jumpDests JumpDestCache) { + evm.jumpDests = jumpDests +} + // SetTxContext resets the EVM with a new transaction context. // This is not threadsafe and should only be done very cautiously. func (evm *EVM) SetTxContext(txCtx TxContext) { diff --git a/core/vm/jumpdests.go b/core/vm/jumpdests.go new file mode 100644 index 0000000000..1a30c1943f --- /dev/null +++ b/core/vm/jumpdests.go @@ -0,0 +1,47 @@ +// Copyright 2024 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 vm + +import "github.com/ethereum/go-ethereum/common" + +// JumpDestCache represents the cache of jumpdest analysis results. +type JumpDestCache interface { + // Load retrieves the cached jumpdest analysis for the given code hash. + // Returns the BitVec and true if found, or nil and false if not cached. + Load(codeHash common.Hash) (BitVec, bool) + + // Store saves the jumpdest analysis for the given code hash. + Store(codeHash common.Hash, vec BitVec) +} + +// mapJumpDests is the default implementation of JumpDests using a map. +// This implementation is not thread-safe and is meant to be used per EVM instance. +type mapJumpDests map[common.Hash]BitVec + +// newMapJumpDests creates a new map-based JumpDests implementation. +func newMapJumpDests() JumpDestCache { + return make(mapJumpDests) +} + +func (j mapJumpDests) Load(codeHash common.Hash) (BitVec, bool) { + vec, ok := j[codeHash] + return vec, ok +} + +func (j mapJumpDests) Store(codeHash common.Hash, vec BitVec) { + j[codeHash] = vec +} From 9c58810e717e7c48dff31ebc7280ba47675597a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Andr=C3=B3il?= Date: Fri, 1 Aug 2025 14:00:00 +0200 Subject: [PATCH 7/8] eth: fix typos and outdated comments (#32324) --- eth/ethconfig/config.go | 2 +- eth/filters/filter.go | 1 - eth/filters/filter_system.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 7eba6a6408..82c3c500a7 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -154,7 +154,7 @@ type Config struct { // RPCEVMTimeout is the global timeout for eth-call. RPCEVMTimeout time.Duration - // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for + // RPCTxFeeCap is the global transaction fee (price * gas limit) cap for // send-transaction variants. The unit is ether. RPCTxFeeCap float64 diff --git a/eth/filters/filter.go b/eth/filters/filter.go index ada478ae1d..c4d787eb22 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -456,7 +456,6 @@ func (f *Filter) blockLogs(ctx context.Context, header *types.Header) ([]*types. // checkMatches checks if the receipts belonging to the given header contain any log events that // match the filter criteria. This function is called when the bloom filter signals a potential match. -// skipFilter signals all logs of the given block are requested. func (f *Filter) checkMatches(ctx context.Context, header *types.Header) ([]*types.Log, error) { hash := header.Hash() // Logs in cache are partially filled with context data diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 82c91266c0..751cd417e8 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -207,7 +207,7 @@ type EventSystem struct { } // NewEventSystem creates a new manager that listens for event on the given mux, -// parses and filters them. It uses the all map to retrieve filter changes. The +// parses and filters them. It uses an internal map to retrieve filter changes. The // work loop holds its own index that is used to forward events to filters. // // The returned manager has a loop that needs to be stopped with the Stop function From 038ff766ff9f9d89c1593c75edeb101c0801a0a4 Mon Sep 17 00:00:00 2001 From: Ceyhun Onur Date: Fri, 1 Aug 2025 18:14:30 +0300 Subject: [PATCH 8/8] eth/filters: fix error when blockHash is used with fromBlock/toBlock (#31877) This introduces an error when the filter has both `blockHash` and `fromBlock`/`toBlock`, since these are mutually exclusive. Seems the tests were actually returning `not found` error, which went undetected since there was no check on the actual returned error in the test. --- eth/filters/api.go | 8 ++++ eth/filters/filter.go | 2 +- eth/filters/filter_system_test.go | 67 +++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/eth/filters/api.go b/eth/filters/api.go index 232af23957..c929810a12 100644 --- a/eth/filters/api.go +++ b/eth/filters/api.go @@ -38,6 +38,8 @@ var ( errInvalidTopic = errors.New("invalid topic(s)") errFilterNotFound = errors.New("filter not found") errInvalidBlockRange = errors.New("invalid block range params") + errUnknownBlock = errors.New("unknown block") + errBlockHashWithRange = errors.New("can't specify fromBlock/toBlock with blockHash") errPendingLogsUnsupported = errors.New("pending logs are not supported") errExceedMaxTopics = errors.New("exceed max topics") errExceedMaxAddresses = errors.New("exceed max addresses") @@ -348,8 +350,13 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type if len(crit.Addresses) > maxAddresses { return nil, errExceedMaxAddresses } + var filter *Filter if crit.BlockHash != nil { + if crit.FromBlock != nil || crit.ToBlock != nil { + return nil, errBlockHashWithRange + } + // Block filter requested, construct a single-shot filter filter = api.sys.NewBlockFilter(*crit.BlockHash, crit.Addresses, crit.Topics) } else { @@ -372,6 +379,7 @@ func (api *FilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*type // Construct the range filter filter = api.sys.NewRangeFilter(begin, end, crit.Addresses, crit.Topics) } + // Run the filter and return all the logs logs, err := filter.Logs(ctx) if err != nil { diff --git a/eth/filters/filter.go b/eth/filters/filter.go index c4d787eb22..1a9918d0ee 100644 --- a/eth/filters/filter.go +++ b/eth/filters/filter.go @@ -85,7 +85,7 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) { return nil, err } if header == nil { - return nil, errors.New("unknown block") + return nil, errUnknownBlock } if header.Number.Uint64() < f.sys.backend.HistoryPruningCutoff() { return nil, &history.PrunedHistoryError{} diff --git a/eth/filters/filter_system_test.go b/eth/filters/filter_system_test.go index 3e686ca2eb..013c1ae527 100644 --- a/eth/filters/filter_system_test.go +++ b/eth/filters/filter_system_test.go @@ -450,24 +450,65 @@ func TestInvalidGetLogsRequest(t *testing.T) { t.Parallel() var ( - db = rawdb.NewMemoryDatabase() - _, sys = newTestFilterSystem(db, Config{}) - api = NewFilterAPI(sys) - blockHash = common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111") + genesis = &core.Genesis{ + Config: params.TestChainConfig, + BaseFee: big.NewInt(params.InitialBaseFee), + } + db, blocks, _ = core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 10, func(i int, gen *core.BlockGen) {}) + _, sys = newTestFilterSystem(db, Config{}) + api = NewFilterAPI(sys) + blockHash = blocks[0].Hash() + unknownBlockHash = common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111") ) - // Reason: Cannot specify both BlockHash and FromBlock/ToBlock) - testCases := []FilterCriteria{ - 0: {BlockHash: &blockHash, FromBlock: big.NewInt(100)}, - 1: {BlockHash: &blockHash, ToBlock: big.NewInt(500)}, - 2: {BlockHash: &blockHash, FromBlock: big.NewInt(rpc.LatestBlockNumber.Int64())}, - 3: {BlockHash: &blockHash, Topics: [][]common.Hash{{}, {}, {}, {}, {}}}, - 4: {BlockHash: &blockHash, Addresses: make([]common.Address, maxAddresses+1)}, + // Insert the blocks into the chain so filter can look them up + blockchain, err := core.NewBlockChain(db, genesis, ethash.NewFaker(), nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + if n, err := blockchain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + + type testcase struct { + f FilterCriteria + err error + } + testCases := []testcase{ + { + f: FilterCriteria{BlockHash: &blockHash, FromBlock: big.NewInt(100)}, + err: errBlockHashWithRange, + }, + { + f: FilterCriteria{BlockHash: &blockHash, ToBlock: big.NewInt(500)}, + err: errBlockHashWithRange, + }, + { + f: FilterCriteria{BlockHash: &blockHash, FromBlock: big.NewInt(rpc.LatestBlockNumber.Int64())}, + err: errBlockHashWithRange, + }, + { + f: FilterCriteria{BlockHash: &unknownBlockHash}, + err: errUnknownBlock, + }, + { + f: FilterCriteria{BlockHash: &blockHash, Topics: [][]common.Hash{{}, {}, {}, {}, {}}}, + err: errExceedMaxTopics, + }, + { + f: FilterCriteria{BlockHash: &blockHash, Topics: [][]common.Hash{{}, {}, {}, {}, {}}}, + err: errExceedMaxTopics, + }, + { + f: FilterCriteria{BlockHash: &blockHash, Addresses: make([]common.Address, maxAddresses+1)}, + err: errExceedMaxAddresses, + }, } for i, test := range testCases { - if _, err := api.GetLogs(context.Background(), test); err == nil { - t.Errorf("Expected Logs for case #%d to fail", i) + _, err := api.GetLogs(context.Background(), test.f) + if !errors.Is(err, test.err) { + t.Errorf("case %d: wrong error: %q\nwant: %q", i, err, test.err) } } }