From 29d00f5ee8297e3465723d1f4c4fd23336ce15d0 Mon Sep 17 00:00:00 2001 From: yperbasis Date: Tue, 14 Jul 2026 13:59:08 +0200 Subject: [PATCH 1/2] execution/types/accounts, db/state/execctx: extract codeHash without a full account decode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split from #22159 (StateCache review findings #22120, finding 7). codeHashForAddr fully decoded every account record it touched — balance parse plus codeHash interning per mem hit — just to read one field. DeserialiseV3CodeHash parses the SerialiseV3 layout only up to and including the codeHash field, is bounds-safe on truncated records (nil on malformed input, unlike the full decoder), returns nil for the empty and zero sentinels to match CodeHash.IsEmpty, and returns a subslice of enc valid only while enc is — every call site consumes it synchronously within the tx. --- db/state/execctx/domain_shared.go | 30 ++------- execution/types/accounts/account.go | 34 ++++++++++ execution/types/accounts/account_test.go | 80 ++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 26 deletions(-) diff --git a/db/state/execctx/domain_shared.go b/db/state/execctx/domain_shared.go index ed76fc775aa..57e53e19d02 100644 --- a/db/state/execctx/domain_shared.go +++ b/db/state/execctx/domain_shared.go @@ -1415,11 +1415,11 @@ func (sd *SharedDomains) codeHashForAddr(tx kv.TemporalTx, addr []byte, txNum ui // on flush. Route mem-first; the LRU is a committed-state layer that may only // answer once mem has missed. if v, _, ok := sd.mem.GetLatest(kv.AccountsDomain, addr); ok { - return decodeAccountCodeHash(v) + return accounts.DeserialiseV3CodeHash(v) } if sd.parent != nil { if v, _, ok := sd.parent.mem.GetLatest(kv.AccountsDomain, addr); ok { - return decodeAccountCodeHash(v) + return accounts.DeserialiseV3CodeHash(v) } } @@ -1440,14 +1440,14 @@ func (sd *SharedDomains) codeHashForAddr(tx kv.TemporalTx, addr []byte, txNum ui resolve := func() []byte { if sd.stateCache != nil { if v, ok := sd.stateCache.Get(kv.AccountsDomain, addr); ok { - return decodeAccountCodeHash(v) + return accounts.DeserialiseV3CodeHash(v) } } v, _, err := tx.GetLatest(kv.AccountsDomain, addr) if err != nil || len(v) == 0 { return nil } - return decodeAccountCodeHash(v) + return accounts.DeserialiseV3CodeHash(v) } h := resolve() @@ -1465,28 +1465,6 @@ func (sd *SharedDomains) codeHashForAddr(tx kv.TemporalTx, addr []byte, txNum ui return h } -// decodeAccountCodeHash extracts the codeHash from an account's encoded -// (DecodeForStorage) bytes. Returns nil on decode error or when the account -// has no code (empty codeHash). -func decodeAccountCodeHash(enc []byte) []byte { - if len(enc) == 0 { - return nil - } - var acc accounts.Account - // AccountsDomain values are SerialiseV3-encoded, so they must be decoded - // with DeserialiseV3. DecodeForStorage is the legacy MDBX bitmask format - // with an incompatible binary layout; applied to V3 bytes it silently - // misparses and leaves CodeHash empty. - if err := accounts.DeserialiseV3(&acc, enc); err != nil { - return nil - } - if acc.CodeHash.IsEmpty() { - return nil - } - h := acc.CodeHash.Value() - return h[:] -} - func (sd *SharedDomains) Metrics() *kvmetrics.DomainMetrics { return &sd.metrics } diff --git a/execution/types/accounts/account.go b/execution/types/accounts/account.go index 69378516720..5251e376237 100644 --- a/execution/types/accounts/account.go +++ b/execution/types/accounts/account.go @@ -17,6 +17,7 @@ package accounts import ( + "bytes" "fmt" "io" "math/bits" @@ -26,6 +27,7 @@ import ( "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" "github.com/erigontech/erigon/execution/rlp" ) @@ -641,6 +643,38 @@ func DeserialiseV3(a *Account, enc []byte) error { return nil } +// DeserialiseV3CodeHash extracts just the codeHash field from a +// SerialiseV3-encoded account, skipping the full decode (balance parse, +// codeHash interning) that DeserialiseV3 pays. It parses only up to and +// including the codeHash field — later fields are not validated. Returns a +// subslice of enc — valid only while enc is — or nil when the record is +// malformed up to that field or the account has no code (including +// non-canonical spellings of the empty or zero sentinel, which +// CodeHash.IsEmpty treats as no-code). +func DeserialiseV3CodeHash(enc []byte) []byte { + pos := 0 + for range 2 { // skip the length-prefixed nonce and balance fields + if pos >= len(enc) { + return nil + } + pos += 1 + int(enc[pos]) + } + if pos >= len(enc) { + return nil + } + codeHashBytes := int(enc[pos]) + pos++ + if codeHashBytes != length.Hash || pos+codeHashBytes > len(enc) { + return nil + } + h := enc[pos : pos+codeHashBytes] + var zero common.Hash + if bytes.Equal(h, zero[:]) || bytes.Equal(h, empty.CodeHash[:]) { + return nil + } + return h +} + func SerialiseV3(a *Account) []byte { var l int l++ diff --git a/execution/types/accounts/account_test.go b/execution/types/accounts/account_test.go index eb25a8a8e64..57885f5a08f 100644 --- a/execution/types/accounts/account_test.go +++ b/execution/types/accounts/account_test.go @@ -17,6 +17,7 @@ package accounts import ( + "bytes" "testing" "github.com/holiman/uint256" @@ -76,6 +77,85 @@ func TestEmptyAccount_BufferStrangeBehaviour(t *testing.T) { isIncarnationEqual(t, a.Incarnation, decodedAcc.Incarnation) } +func TestDeserialiseV3CodeHash(t *testing.T) { + t.Parallel() + balances := []uint256.Int{{}, *uint256.NewInt(1), *uint256.NewInt(1e18), *new(uint256.Int).Lsh(uint256.NewInt(1), 200)} + nonces := []uint64{0, 1, 255, 1 << 40} + codeHashes := []CodeHash{EmptyCodeHash, InternCodeHash(common.BytesToHash(crypto.Keccak256([]byte{1, 2, 3})))} + incarnations := []uint64{0, 7} + + for _, nonce := range nonces { + for i := range balances { + for _, ch := range codeHashes { + for _, inc := range incarnations { + a := Account{Nonce: nonce, Balance: balances[i], CodeHash: ch, Incarnation: inc} + enc := SerialiseV3(&a) + + var full Account + if err := DeserialiseV3(&full, enc); err != nil { + t.Fatal(err) + } + got := DeserialiseV3CodeHash(enc) + if full.CodeHash.IsEmpty() { + if got != nil { + t.Fatalf("empty codeHash must extract as nil, got %x (acc %+v)", got, a) + } + } else { + want := full.CodeHash.Value() + if !bytes.Equal(got, want[:]) { + t.Fatalf("extracted %x, want %x (acc %+v)", got, want, a) + } + } + } + } + } + } +} + +func TestDeserialiseV3CodeHashMalformed(t *testing.T) { + t.Parallel() + a := Account{ + Nonce: 255, + Balance: *uint256.NewInt(1e18), + CodeHash: InternCodeHash(common.BytesToHash(crypto.Keccak256([]byte{1, 2, 3}))), + Incarnation: 4, + } + enc := SerialiseV3(&a) + // [1+nonce][1+balance][1+codeHash]... — the codeHash field is complete at: + codeHashEnd := 1 + int(enc[0]) + 1 + codeHashEnd += int(enc[codeHashEnd-1]) + 1 + codeHashEnd += int(enc[codeHashEnd-1]) + // Any truncation cutting into (or before) the codeHash must yield nil, + // never an out-of-bounds read; beyond it the codeHash is extractable. + for cut := 0; cut <= len(enc); cut++ { + got := DeserialiseV3CodeHash(enc[:cut]) + if cut < codeHashEnd && got != nil { + t.Fatalf("cut=%d (codeHash complete at %d): expected nil, got %x", cut, codeHashEnd, got) + } + if cut >= codeHashEnd && got == nil { + t.Fatalf("cut=%d (codeHash complete at %d): expected hash, got nil", cut, codeHashEnd) + } + } + if got := DeserialiseV3CodeHash(nil); got != nil { + t.Fatalf("nil input: expected nil, got %x", got) + } + // A record claiming a non-32-byte codeHash is malformed for extraction. + odd := append([]byte{0, 0, 31}, make([]byte, 31)...) + if got := DeserialiseV3CodeHash(odd); got != nil { + t.Fatalf("non-32-byte codeHash field: expected nil, got %x", got) + } + // Non-canonical records spelling out the no-code sentinels (canonical + // SerialiseV3 writes length 0 instead) must extract as nil, matching + // CodeHash.IsEmpty. + for _, sentinel := range [][]byte{make([]byte, 32), empty.CodeHash[:]} { + rec := append([]byte{0, 0, 32}, sentinel...) + rec = append(rec, 0) + if got := DeserialiseV3CodeHash(rec); got != nil { + t.Fatalf("sentinel codeHash %x: expected nil, got %x", sentinel, got) + } + } +} + func TestAccountEncodeWithCode(t *testing.T) { t.Parallel() a := Account{ From 6c04496449b840753987c47ee900f7cfdc96ad53 Mon Sep 17 00:00:00 2001 From: yperbasis Date: Wed, 15 Jul 2026 12:24:35 +0200 Subject: [PATCH 2/2] execution/types/accounts: compare extracted code hash directly --- execution/types/accounts/account.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/execution/types/accounts/account.go b/execution/types/accounts/account.go index 5251e376237..9de21fbee42 100644 --- a/execution/types/accounts/account.go +++ b/execution/types/accounts/account.go @@ -17,7 +17,6 @@ package accounts import ( - "bytes" "fmt" "io" "math/bits" @@ -668,8 +667,7 @@ func DeserialiseV3CodeHash(enc []byte) []byte { return nil } h := enc[pos : pos+codeHashBytes] - var zero common.Hash - if bytes.Equal(h, zero[:]) || bytes.Equal(h, empty.CodeHash[:]) { + if ch := common.Hash(h); ch == (common.Hash{}) || ch == empty.CodeHash { return nil } return h