go-ethereum/core/state/journal_test.go
rjl493456442 b2aa6987de
core/state: track the block-level accessList (#34803)
This PR extends the journal to track the pre-transaction values of
mutated balances, nonces, and code.

At the end of the transaction, these values are used to filter out no-op
changes, such as balance transitions from a-> b->a. These changes are
excluded from the block-level access list.

Additionally, there is a dedicated `bal.ConstructionBlockAccessList`
objects for gathering the state reads and writes within the current
transaction. These state writes will be keyed by the block accessList
index.

---------

Co-authored-by: jwasinger <j-wasinger@hotmail.com>
2026-05-13 20:38:47 +08:00

219 lines
7.3 KiB
Go

// 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 <http://www.gnu.org/licenses/>.
package state
import (
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"
)
// fuzzJournalAddrs is a small fixed pool used by the fuzz harness to force
// repeated collisions on the same account, which exercises the multi-entry
// path in the journal's mutation tracking and originals cleanup on revert.
// It deliberately excludes the RIPEMD-160 precompile (0x03), which has a
// consensus-level touch/revert exception that would complicate invariants.
var fuzzJournalAddrs = []common.Address{
common.BytesToAddress([]byte{0x11}),
common.BytesToAddress([]byte{0x22}),
common.BytesToAddress([]byte{0x44}),
}
// checkJournalInvariants validates that:
// - journal.mutations exactly reflects the dirty entries currently in
// journal.entries (per-kind counts and mask match what you'd get by
// walking the entries from scratch).
// - journal.originals mirrors that set for the three tracked metadata kinds
// (balance/nonce/code): a *Set flag is true iff the account currently has
// at least one corresponding entry in the journal.
// - An address is present in originals only if it also has at least one
// tracked-kind mutation in the journal.
func checkJournalInvariants(t *testing.T, j *journal) {
t.Helper()
// Reconstruct the expected per-address counts from the live entries.
expected := make(map[common.Address]*journalMutationCounts)
for _, e := range j.entries {
addr, kind, dirty := e.mutation()
if !dirty {
continue
}
c := expected[addr]
if c == nil {
c = &journalMutationCounts{}
expected[addr] = c
}
c.add(kind)
}
if len(j.mutations) != len(expected) {
t.Fatalf("mutations size %d, want %d", len(j.mutations), len(expected))
}
for addr, state := range j.mutations {
want, ok := expected[addr]
if !ok {
t.Fatalf("mutations has extra address %x", addr)
}
if state.counts != *want {
t.Fatalf("addr %x: counts=%+v want=%+v", addr, state.counts, *want)
}
// First-touch *Set flags must mirror the live per-kind counts.
if state.balanceSet != (want[journalMutationKindBalance] > 0) {
t.Fatalf("addr %x: balanceSet=%v want=%v (balance count=%d)",
addr, state.balanceSet, want[journalMutationKindBalance] > 0, want[journalMutationKindBalance])
}
if state.nonceSet != (want[journalMutationKindNonce] > 0) {
t.Fatalf("addr %x: nonceSet=%v want=%v (nonce count=%d)",
addr, state.nonceSet, want[journalMutationKindNonce] > 0, want[journalMutationKindNonce])
}
if state.codeSet != (want[journalMutationKindCode] > 0) {
t.Fatalf("addr %x: codeSet=%v want=%v (code count=%d)",
addr, state.codeSet, want[journalMutationKindCode] > 0, want[journalMutationKindCode])
}
}
}
// FuzzJournal drives a randomised sequence of state mutations, snapshots and
// reverts against a fresh StateDB and validates the journal's internal
// bookkeeping invariants after every step. It also asserts that reverting
// back to the root snapshot empties mutations, originals and entries
// completely. The seed corpus ensures the test also runs as a regular unit
// test via `go test -run FuzzJournal`.
func FuzzJournal(f *testing.F) {
seeds := [][]byte{
// balance then full revert (simplest a→b→a case).
{0x00, 0x00, 0x05, 0x05, 0x00},
// balance+nonce+code mixed, then revert to root.
{0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x05, 0x00},
// snapshot, mutate, revert, mutate again.
{0x04, 0x00, 0x00, 0x07, 0x05, 0x00, 0x00, 0x01, 0x05},
// storage interleaved with metadata.
{0x03, 0x00, 0x01, 0x00, 0x01, 0x05, 0x03, 0x02, 0x02, 0x04, 0x03, 0x01, 0x07},
// many ops, no explicit revert — exercises steady-state invariants.
{0x00, 0x01, 0x02, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02,
0x03, 0x04, 0x00, 0x01, 0x02, 0x00, 0x06, 0x08, 0x0a, 0x0c},
}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, data []byte) {
sdb, err := New(types.EmptyRootHash, NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
root := sdb.Snapshot()
// Stack of snapshot IDs taken during the fuzz loop.
var pending []int
// readByte returns the next byte and advances the cursor. Returns
// (0, false) if exhausted.
i := 0
readByte := func() (byte, bool) {
if i >= len(data) {
return 0, false
}
b := data[i]
i++
return b, true
}
for {
op, ok := readByte()
if !ok {
break
}
switch op % 6 {
case 0: // SetBalance
a, ok1 := readByte()
v, ok2 := readByte()
if !ok1 || !ok2 {
break
}
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
sdb.SetBalance(addr, uint256.NewInt(uint64(v)), tracing.BalanceChangeUnspecified)
case 1: // SetNonce
a, ok1 := readByte()
n, ok2 := readByte()
if !ok1 || !ok2 {
break
}
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
sdb.SetNonce(addr, uint64(n), tracing.NonceChangeUnspecified)
case 2: // SetCode
a, ok1 := readByte()
l, ok2 := readByte()
if !ok1 || !ok2 {
break
}
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
code := make([]byte, int(l)%8)
for k := range code {
b, ok := readByte()
if !ok {
break
}
code[k] = b
}
sdb.SetCode(addr, code, tracing.CodeChangeUnspecified)
case 3: // SetState (storage; tracked as mutation kind, no original)
a, ok1 := readByte()
k, ok2 := readByte()
v, ok3 := readByte()
if !ok1 || !ok2 || !ok3 {
break
}
addr := fuzzJournalAddrs[int(a)%len(fuzzJournalAddrs)]
sdb.SetState(addr,
common.BytesToHash([]byte{k}),
common.BytesToHash([]byte{v}))
case 4: // Snapshot
pending = append(pending, sdb.Snapshot())
case 5: // RevertToSnapshot
if len(pending) == 0 {
break
}
sel, ok := readByte()
if !ok {
break
}
idx := int(sel) % len(pending)
sdb.RevertToSnapshot(pending[idx])
pending = pending[:idx]
}
checkJournalInvariants(t, sdb.journal)
}
// After reverting to the root snapshot, the journal must be fully
// drained: no entries, no mutations, no originals. This is the core
// guarantee the user cares about — "all mutations against a single
// account reverted" taken to its limit across every account.
sdb.RevertToSnapshot(root)
checkJournalInvariants(t, sdb.journal)
if n := len(sdb.journal.entries); n != 0 {
t.Fatalf("entries not drained after revert-to-root: %d remain", n)
}
if n := len(sdb.journal.mutations); n != 0 {
t.Fatalf("mutations not drained after revert-to-root: %d remain", n)
}
})
}