From 2915a9c30da7ec74c78eb43776245fbbe5f0d4ad Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 9 Apr 2026 12:22:24 +0200 Subject: [PATCH] core/state,triedb/pathdb: add integration tests for storage tombstone, multi-block evolution, and contract code generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review test gaps T8, T9, T10. TestBintrieFlatReaderStorageTombstone (T8): Write slot=0x42 in block 1; clear to zero in block 2. Read at block 2 → common.Hash{} (tombstone, not absent). Read at block 1 → 0x42 (original value preserved in the diff layer chain). TestBintrieFlatReaderMultiBlockEvolution (T9): Write nonce=1/balance=100 in block 1, nonce=2 in block 2, balance=200 in block 3. Open StateReaders at each root and assert the correct snapshot for each block. Validates diff-layer chaining under the bintrie path. TestBintrieGeneratorWithContractCode (T10): Build a bintrie with a contract having ~100 bytes of code (4 chunks at offsets 128..131). Run the generator and verify code-chunk offsets appear in the resulting stem blob. Validates the plan's claim that code chunks are handled by the generator. --- core/state/reader_bintrie_test.go | 124 +++++++++++++++++++++++++ triedb/pathdb/generate_bintrie_test.go | 68 ++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/core/state/reader_bintrie_test.go b/core/state/reader_bintrie_test.go index b61a533979..518b82242a 100644 --- a/core/state/reader_bintrie_test.go +++ b/core/state/reader_bintrie_test.go @@ -348,3 +348,127 @@ func TestBintrieFlatReaderMultipleOffsetsPerStem(t *testing.T) { } } } + +// TestBintrieFlatReaderStorageTombstone verifies the bintrie "tombstone" +// convention: a storage slot set to zero is present-with-32-zero-bytes, +// which must be distinguishable from "never written" (absent). This is +// the A16/T8 integration test. +func TestBintrieFlatReaderStorageTombstone(t *testing.T) { + disk := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults) + sdb := NewDatabase(tdb, nil) + + addr := common.HexToAddress("0xABCDEF0123456789ABCDEF0123456789ABCDEF01") + slot := common.HexToHash("0x07") + nonZero := common.HexToHash("0x42") + + // Block 1: set slot to non-zero. + state1, _ := New(types.EmptyVerkleHash, sdb) + state1.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified) + state1.SetState(addr, slot, nonZero) + root1, err := state1.Commit(0, true, false) + if err != nil { + t.Fatalf("commit block 1: %v", err) + } + + // Block 2: set the same slot to zero (the bintrie writes 32 zero + // bytes as a tombstone rather than deleting the offset). + state2, _ := New(root1, sdb) + state2.SetState(addr, slot, common.Hash{}) + root2, err := state2.Commit(1, true, false) + if err != nil { + t.Fatalf("commit block 2: %v", err) + } + + // Read at block 2: should be the zero hash. + reader2, err := sdb.StateReader(root2) + if err != nil { + t.Fatalf("StateReader(block2): %v", err) + } + got2, err := reader2.Storage(addr, slot) + if err != nil { + t.Fatalf("Storage(block2): %v", err) + } + if got2 != (common.Hash{}) { + t.Errorf("block 2 slot: got %x, want zero", got2) + } + + // Read at block 1: should still be the non-zero value. + reader1, err := sdb.StateReader(root1) + if err != nil { + t.Fatalf("StateReader(block1): %v", err) + } + got1, err := reader1.Storage(addr, slot) + if err != nil { + t.Fatalf("Storage(block1): %v", err) + } + if got1 != nonZero { + t.Errorf("block 1 slot: got %x, want %x", got1, nonZero) + } +} + +// TestBintrieFlatReaderMultiBlockEvolution verifies that diff-layer +// chaining works correctly across multiple blocks for the bintrie path. +// This is the A16/T9 integration test. +func TestBintrieFlatReaderMultiBlockEvolution(t *testing.T) { + disk := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults) + sdb := NewDatabase(tdb, nil) + + addr := common.HexToAddress("0xDeaDBeefDeaDBeefDeaDBeefDeaDBeefDeaDBeef") + + // Block 1: nonce=1, balance=100 + state1, _ := New(types.EmptyVerkleHash, sdb) + state1.SetBalance(addr, uint256.NewInt(100), tracing.BalanceChangeUnspecified) + state1.SetNonce(addr, 1, tracing.NonceChangeUnspecified) + root1, err := state1.Commit(0, true, false) + if err != nil { + t.Fatalf("commit block 1: %v", err) + } + + // Block 2: nonce=2 (balance unchanged at 100) + state2, _ := New(root1, sdb) + state2.SetNonce(addr, 2, tracing.NonceChangeUnspecified) + root2, err := state2.Commit(1, true, false) + if err != nil { + t.Fatalf("commit block 2: %v", err) + } + + // Block 3: balance=200 (nonce unchanged at 2) + state3, _ := New(root2, sdb) + state3.SetBalance(addr, uint256.NewInt(200), tracing.BalanceChangeUnspecified) + root3, err := state3.Commit(2, true, false) + if err != nil { + t.Fatalf("commit block 3: %v", err) + } + + // Read at each root and verify the expected snapshot. + for _, tc := range []struct { + name string + root common.Hash + nonce uint64 + balance uint64 + }{ + {"block1", root1, 1, 100}, + {"block2", root2, 2, 100}, + {"block3", root3, 2, 200}, + } { + reader, err := sdb.StateReader(tc.root) + if err != nil { + t.Fatalf("%s StateReader: %v", tc.name, err) + } + got, err := reader.Account(addr) + if err != nil { + t.Fatalf("%s Account: %v", tc.name, err) + } + if got == nil { + t.Fatalf("%s: account is nil", tc.name) + } + if got.Nonce != tc.nonce { + t.Errorf("%s nonce: got %d, want %d", tc.name, got.Nonce, tc.nonce) + } + if got.Balance.Uint64() != tc.balance { + t.Errorf("%s balance: got %d, want %d", tc.name, got.Balance.Uint64(), tc.balance) + } + } +} diff --git a/triedb/pathdb/generate_bintrie_test.go b/triedb/pathdb/generate_bintrie_test.go index d620d7d3c6..17a0f02520 100644 --- a/triedb/pathdb/generate_bintrie_test.go +++ b/triedb/pathdb/generate_bintrie_test.go @@ -302,3 +302,71 @@ func TestBintrieGeneratorResumeMidStem(t *testing.T) { } } } + +// TestBintrieGeneratorWithContractCode verifies that the generator +// correctly writes code-chunk offsets (128..255) into stem blobs for +// contracts with non-trivial code. This is the A16/T10 test. +func TestBintrieGeneratorWithContractCode(t *testing.T) { + db := rawdb.NewMemoryDatabase() + + // Build a bintrie with one contract that has ~100 bytes of code. + // Per EIP-7864, code is chunked into 31-byte pieces starting at + // offset 128 of the account's stem. + tr, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, &bintrieDiskStore{db: db}) + if err != nil { + t.Fatalf("new bintrie: %v", err) + } + addr := common.HexToAddress("0xContractContractContractContractContrac") + code := make([]byte, 100) + for i := range code { + code[i] = byte(i) + } + if err := tr.UpdateAccount(addr, &types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(1000), + CodeHash: types.EmptyCodeHash[:], + }, len(code)); err != nil { + t.Fatalf("UpdateAccount: %v", err) + } + codeHash := common.BytesToHash(types.EmptyCodeHash[:]) + if err := tr.UpdateContractCode(addr, codeHash, code); err != nil { + t.Fatalf("UpdateContractCode: %v", err) + } + root, nodes := tr.Commit(false) + + // Persist trie nodes + batch := db.NewBatch() + for path, node := range nodes.Nodes { + if !node.IsDeleted() { + rawdb.WriteAccountTrieNode(batch, []byte(path), node.Blob) + } + } + if err := batch.Write(); err != nil { + t.Fatalf("flush trie nodes: %v", err) + } + + // Run the generator + runTestBintrieGenerator(t, db, root, nil) + + // Verify account header offsets are present. + stem := bintrie.GetBinaryTreeKeyBasicData(addr)[:bintrie.StemSize] + blob := rawdb.ReadBinTrieStem(db, stem) + if len(blob) == 0 { + t.Fatal("stem blob missing for contract account") + } + basic, _ := extractStemOffset(blob, bintrie.BasicDataLeafKey) + if len(basic) != 32 { + t.Errorf("BasicData: got len %d, want 32", len(basic)) + } + codeHashLeaf, _ := extractStemOffset(blob, bintrie.CodeHashLeafKey) + if len(codeHashLeaf) != 32 { + t.Errorf("CodeHash: got len %d, want 32", len(codeHashLeaf)) + } + + // Verify at least one code chunk offset (128) is present. + // 100 bytes of code = ceil(100/31) = 4 chunks, at offsets 128..131. + codeChunk0, _ := extractStemOffset(blob, 128) + if len(codeChunk0) != 32 { + t.Errorf("Code chunk at offset 128: got len %d, want 32 (code chunk missing from stem blob)", len(codeChunk0)) + } +}