// 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