// Copyright 2026 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" "encoding/binary" "fmt" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/trienode" ) // TestRootHashMatchesReadBackHash pins the round-trip invariant: the root // hash a Commit advertises must be exactly the value a fresh reader computes // from the on-disk blob. Before Option B the writer produced a natural-depth // hash while DeserializeAndHash produced an extended-depth hash, so the two // disagreed for any non-trivial subtree — this test failed. With the // per-entry depth byte, the reader rebuilds the natural-shape tree and the // hashes match for every groupDepth and every divergence bit. func TestRootHashMatchesReadBackHash(t *testing.T) { for groupDepth := 1; groupDepth <= MaxGroupDepth; groupDepth++ { // divergeBit ∈ [0, groupDepth-1] places the two stems at natural // depth (divergeBit+1) within the root group; we want to exercise // every depth-offset value the new format must handle. for divergeBit := 0; divergeBit < groupDepth; divergeBit++ { t.Run(fmt.Sprintf("gd=%d/diverge=%d", groupDepth, divergeBit), func(t *testing.T) { tr := &BinaryTrie{ store: newNodeStore(), tracer: trie.NewPrevalueTracer(), groupDepth: groupDepth, } stemL, stemR := stemsDivergingAt(divergeBit) if err := tr.store.Insert(stemL, oneKey[:], nil); err != nil { t.Fatalf("Insert stemL: %v", err) } if err := tr.store.Insert(stemR, twoKey[:], nil); err != nil { t.Fatalf("Insert stemR: %v", err) } natural := tr.Hash() _, ns := tr.Commit(false) rootNode, ok := ns.Nodes[""] if !ok { t.Fatalf("Commit produced no root blob (path \"\")") } readBack, err := DeserializeAndHash(rootNode.Blob, 0) if err != nil { t.Fatalf("DeserializeAndHash: %v", err) } if natural != readBack { t.Fatalf("round-trip hash mismatch:\n"+ " tr.Hash() = %x\n"+ " DeserializeAndHash(rootBlob) = %x\n"+ "the parent's stored root hash cannot be reproduced from its own blob", natural, readBack) } }) } } } // TestMultiStemMixedDepths inserts four stems that diverge at different // depths within a single groupDepth=5 group, then round-trips the trie // through Commit + fresh-read. Verifies that every stem is retrievable by // key after reload — exercises the new format with several depth-offset // values in the same blob (1, 2, 3, 4) and confirms attachInGroup builds // the natural-shape tree correctly. func TestMultiStemMixedDepths(t *testing.T) { const groupDepth = 5 // Each stem diverges from `0x00…00` at a different bit, so naturally: // - stem at bit-0 divergence → depth 1 // - stem at bit-1 divergence → depth 2 // - stem at bit-2 divergence → depth 3 // - stem at bit-3 divergence → depth 4 stems := [][]byte{ zeroKey[:], bitFlipStem(0), // diverge at bit 0 bitFlipStem(1), // diverge at bit 1 (prefix "0" matches stem 0) bitFlipStem(2), // diverge at bit 2 (prefix "00") bitFlipStem(3), // diverge at bit 3 (prefix "000") } values := []common.Hash{oneKey, twoKey, threeKey, fourKey, ffKey} tr := &BinaryTrie{ store: newNodeStore(), tracer: trie.NewPrevalueTracer(), groupDepth: groupDepth, } for i, stem := range stems { if err := tr.store.Insert(stem, values[i][:], nil); err != nil { t.Fatalf("Insert stem %d: %v", i, err) } } before := tr.Hash() _, ns := tr.Commit(false) rootBlob, ok := ns.Nodes[""] if !ok { t.Fatalf("no root blob in NodeSet") } readBack, err := DeserializeAndHash(rootBlob.Blob, 0) if err != nil { t.Fatalf("DeserializeAndHash: %v", err) } if before != readBack { t.Fatalf("hash mismatch: tr.Hash()=%x DeserializeAndHash(rootBlob)=%x", before, readBack) } // Reload the root blob into a fresh store and confirm structure. fresh := newNodeStore() ref, err := fresh.deserializeNodeWithHash(rootBlob.Blob, 0, before) if err != nil { t.Fatalf("deserializeNodeWithHash: %v", err) } if ref.Kind() != kindInternal { t.Fatalf("expected root to be Internal, got kind %d", ref.Kind()) } // Spot-check: the reload-tree's root hash equals the commit-time hash. if got := fresh.computeHash(ref); got != before { t.Fatalf("reload root hash mismatch: got %x, want %x", got, before) } } // TestDecodeRejectsNonCanonicalPosition hand-crafts a blob where the bitmap // position has nonzero trailing bits given its depth offset. Two // implementations must produce byte-identical blobs for the same logical // content, so a non-canonical position is unambiguously an invalid blob. func TestDecodeRejectsNonCanonicalPosition(t *testing.T) { // groupDepth=5, bitmap size = 4 bytes. Set bit at position 5 (binary // 00101) and declare depthOffset=2. Top 2 bits of 00101 are 00 (path // "00"), the trailing 3 bits should be zero — they're 101 here, so the // reader must reject. blob := []byte{nodeTypeInternal, 5} // bitmap[0] = bit at position 5 → 1 << (7-5) = 0x04 blob = append(blob, 0x04, 0x00, 0x00, 0x00) // depths[0] = 2, packed as (2-1)=1 in 3 bits MSB-first → 0b0010_0000 = 0x20 blob = append(blob, 0x20) // hashes[0] = 32 zero bytes blob = append(blob, make([]byte, HashSize)...) s := newNodeStore() _, err := s.deserializeNode(blob, 0) if err == nil { t.Fatal("expected non-canonical position error, got nil") } if err.Error() != "non-canonical bitmap position" { t.Errorf("expected 'non-canonical bitmap position', got %q", err.Error()) } } // TestDecodeRejectsInvalidDepthOffset covers depthOffset>groupDepth (the entry // would live below the group's bottom layer, impossible by construction). The // old depthOffset=0 and depthOffset>MaxGroupDepth cases are gone: the 3-bit // field stores (offset-1) ∈ [0,7], so offset 0 and offset 9 are unrepresentable // and can no longer be hand-crafted into a blob. Only offset>groupDepth with // groupDepth