Skip to content

[Storehouse] 001 - Part 2 Test cases#8609

Merged
zhangchiqing merged 6 commits into
masterfrom
leo/payloadless-tests-base
Jul 16, 2026
Merged

[Storehouse] 001 - Part 2 Test cases#8609
zhangchiqing merged 6 commits into
masterfrom
leo/payloadless-tests-base

Conversation

@zhangchiqing

@zhangchiqing zhangchiqing commented Jul 11, 2026

Copy link
Copy Markdown
Member

Accidentally merged #8572.

Now since #8569 has been merged, I'm opening this PR again to add test cases for payload node and trie implementation.

Summary by CodeRabbit

  • Tests
    • Expanded payloadless trie test coverage, including deterministic root-hash expectations, allocation counting, read/leaf-hash correctness, and unallocation behavior.
    • Added equivalence checks against the regular trie, covering pruning and unsafe inclusion/proof equivalence.
    • Strengthened randomized allocation/unallocation validation to ensure consistent roots, counts, and per-path results.
    • Added JSON dump verification for empty, default/unallocated, and multi-leaf cases.
    • Added interim-node and leaf enumeration hashing assertions, plus payloadless proof/proof-batch behavior checks (construction, equality, and string output).

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds extensive tests for payloadless trie roots, updates, pruning, reads, full-trie equivalence, JSON output, interim-node hashing, and single and batch proof behavior.

Changes

Payloadless trie validation

Layer / File(s) Summary
Trie behavior and pruning tests
ledger/complete/payloadless/trie_test.go
Adds deterministic root, update, unallocation, splitting, pruning, allocation-count, and leaf-read coverage.
Full and payloadless trie equivalence
ledger/complete/payloadless/equivalence_test.go
Compares roots, allocation counts, reads, unsafe reads, proofs, unallocation, pruning modes, and randomized updates.
Node hashes and JSON output
ledger/complete/payloadless/node_test.go, ledger/complete/payloadless/json_test.go
Pins interim-node hashes, validates leaf-hash enumeration, and checks empty, populated, and default-leaf JSON dumps.
Payloadless proof contracts
ledger/payloadless_proof_test.go
Tests proof construction, equality, string output, batch operations, path ordering, and leaf-hash extraction.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • onflow/flow-go#8569: Introduces the payloadless trie, node, and proof APIs exercised by these tests.

Suggested labels: Preserve

Suggested reviewers: janezpodhostnik

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title indicates test cases, but it's too generic to describe the main payloadless trie/payload node changes. Use a concise, specific title such as "Add payloadless trie and proof tests".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch leo/payloadless-tests-base

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@zhangchiqing
zhangchiqing marked this pull request as ready for review July 11, 2026 01:39
@zhangchiqing
zhangchiqing requested a review from a team as a code owner July 11, 2026 01:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
ledger/complete/payloadless/trie_test.go (1)

610-621: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Off-by-one in removal sampling loop.

i > samplesNeeded allows one extra iteration beyond the intended count. When samplesNeeded is 3, the loop executes 4 removals (i = 0, 1, 2, 3) before breaking. This is a long-running smoke test so the impact is low, but the logic should use >= for correctness.

♻️ Proposed fix
-			if i > samplesNeeded {
+			if i >= samplesNeeded {
 				break
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/complete/payloadless/trie_test.go` around lines 610 - 621, Fix the
off-by-one error in the removal sampling loop by changing the break condition
after incrementing i from i > samplesNeeded to i >= samplesNeeded, ensuring
exactly samplesNeeded paths are removed. Update the loop in the test that builds
updatePaths and updateValues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ledger/complete/payloadless/trie_test.go`:
- Around line 82-85: Move require.NoError(t, err) immediately after the
NewTrieWithUpdatedRegisters call and before any assertions dereferencing
leftPopulatedTrie, including AllocatedRegCount(), so errors are reported without
a nil-pointer panic.
- Line 804: Update the assertion in the relevant no-pruning subtest to compare
rootHash with updatedTrieNoPruning.RootHash(), replacing the incorrect
updatedTrie reference so the test validates the trie being modified and
re-populated.
- Around line 363-369: Update the multiset equality assertion after sorting in
the affected test so it compares every element of paths with sortedPaths,
including indices 0 through index-1; replace the loop starting at index with a
full-range comparison while preserving the existing length assumptions and
assert.Equal usage.

---

Nitpick comments:
In `@ledger/complete/payloadless/trie_test.go`:
- Around line 610-621: Fix the off-by-one error in the removal sampling loop by
changing the break condition after incrementing i from i > samplesNeeded to i >=
samplesNeeded, ensuring exactly samplesNeeded paths are removed. Update the loop
in the test that builds updatePaths and updateValues.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3478c3f8-6b69-4e79-a89a-b1abd5ca5028

📥 Commits

Reviewing files that changed from the base of the PR and between 2e2df5e and df59e78.

📒 Files selected for processing (5)
  • ledger/complete/payloadless/equivalence_test.go
  • ledger/complete/payloadless/json_test.go
  • ledger/complete/payloadless/node_test.go
  • ledger/complete/payloadless/trie_test.go
  • ledger/payloadless_proof_test.go

Comment thread ledger/complete/payloadless/trie_test.go Outdated
Comment thread ledger/complete/payloadless/trie_test.go
Comment thread ledger/complete/payloadless/trie_test.go Outdated

@AlexHentschel AlexHentschel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

  • suggested the addition of clarifying Ascii diagrams in a few places
  • AI identified some untested side-cases and proposed extension of tests (these were mostly untested already in the prior version of mTrie). The extension should be straight forward for an AI to implement 🤞 .

Overall, nice work consolidating the tests.

// (old value empty and new value present)
updatedTrieNoPruning, maxDepthTouched, err = payloadless.NewTrieWithUpdatedRegisters(updatedTrieNoPruning, paths, values, false)
require.NoError(t, err)
require.Equal(t, rootHash, updatedTrie.RootHash())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion re-checks updatedTrie, which is unchanged since line 739 — so it is trivially true, and the actual property (re-allocating the removed registers restores the original root hash on the no-pruning trie) is unverified.

Suggested change
require.Equal(t, rootHash, updatedTrie.RootHash())
require.Equal(t, rootHash, updatedTrieNoPruning.RootHash())

I verified locally that the test passes with the corrected assertion. Note the mtrie original has the identical bug (ledger/complete/mtrie/trie/trie_test.go line 1044) — candidate for a small follow-up fix there.

sort.Slice(paths, func(i, j int) bool {
return bytes.Compare(paths[i][:], paths[j][:]) < 0
})
for i := index; i < len(paths); i++ {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop starts at index, so the first partition paths[0:index] is never compared against sortedPaths — corruption of the zero-bit partition would go undetected. Both slices are fully sorted at this point, so the check can simply cover the whole range:

Suggested change
for i := index; i < len(paths); i++ {
for i := 0; i < len(paths); i++ {

Same issue in the mtrie original (ledger/complete/mtrie/trie/trie_test.go line 413).

const pathsNumber = 100
const redundantPaths = 10
const pathsSize = 32
randomIndex := rand.Intn(pathsSize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathsSize = 32 is the path length in bytes, but it bounds the bit index passed to SplitPaths. The test therefore only ever splits on bit indices 0–31 (the first 4 bytes); the remaining 224 bit positions — including the byte-boundary cases inside bitutils.ReadBit — are never exercised.

Suggested change
randomIndex := rand.Intn(pathsSize)
randomIndex := rand.Intn(pathsSize * 8)

(Inherited from the mtrie original.)

leftPopulatedTrie, maxDepthTouched, err := payloadless.NewTrieWithUpdatedRegisters(emptyTrie, []ledger.Path{path}, [][]byte{value}, true)
require.Equal(t, uint16(0), maxDepthTouched)
require.Equal(t, uint64(1), leftPopulatedTrie.AllocatedRegCount())
require.NoError(t, err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: require.NoError(t, err) should precede the assertions on the other return values — if the update errors, the maxDepthTouched / reg-count failures mask the root cause. Same ordering at lines 220–222, 227–229, and 435–438.

require.NoError(t, err)

// and now we override the first 99 registers with default values, i.e. unallocate them
emptyValues0 := make([][]byte, len(values1))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All unallocation tests use nil values. The spec's boundary condition is len(value) == 0 (see NewLeaf), which the empty non-nil slice []byte{} also satisfies — but that representation is never tested anywhere in the suite. Suggest setting a few of these entries to []byte{} so both representations of "unallocated" are pinned as equivalent.

n3 := NewLeaf(path, v, 1)
n4 := NewInterimNode(1, n1, n2)
n5 := NewInterimNode(2, n4, n3)
require.Equal(t, 3, len(n5.AllLeafHashes()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two weaknesses here:

  1. Only the count is asserted — an implementation returning the wrong hashes (or the same hash three times) would pass. Suggest asserting the returned set equals the three expected leaf hashes.
  2. n1 and n2 are the same register (identical path and value) attached as both children of n4, which is not a well-formed trie shape. Distinct registers keep the fixture representative and make the content assertion from (1) meaningful.

Adding a default leaf (unallocated register) into the subtree would also pin the "empty leaves are skipped" clause of the contract, which is currently untested at the node level.

paths := []ledger.Path{path1, path2, path3}
values := [][]byte{value1, value2, value3}

tr, _, err := payloadless.NewTrieWithUpdatedRegisters(payloadless.NewEmptyMTrie(), paths, values, true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both dump tests use pruned tries, so DumpAsJSON on an explicit default leaf (as occurs in unpruned tries) is un-tested: it currently emits {"path": ..., "leafHash": null}. If that is intended, worth a test case (build with prune=false, unallocate one register); if unallocated registers should rather be skipped (as AllLeafHashes does), that's a production-code question — either way a test would force the decision to be explicit.

Comment on lines +517 to +518
// Test_InterimNodeWithOneChild verifies that the hash value of an interim node with
// only one child (left or right) is computed correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small diagram would make this hash-pinning test self-explanatory and, more importantly, document why the two constructions hash differently (the nil sibling contributes DefaultHashForHeight(h-1)):

Suggested change
// Test_InterimNodeWithOneChild verifies that the hash value of an interim node with
// only one child (left or right) is computed correctly.
// Test_InterimNodeWithOneChild verifies that the hash value of an interim node with
// only one child (left or right) is computed correctly. The nil sibling contributes
// DefaultHashForHeight(h-1), which is why the left-only and right-only nodes below hash
// differently:
//
// NewInterimNode(1, c, nil) NewInterimNode(1, nil, c)
// (h=1) (h=1)
// / \ / \
// c=Leaf(56809) nil=D(0) nil=D(0) c=Leaf(56809)
// (h=0) (h=0)

Comment on lines +533 to +534
// Test_InterimNodeWithBothChildren verifies that the hash value of an interim node with
// both children (left and right) is computed correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A diagram clarifies the shape under test. Worth also noting the construction is synthetic, so a reader doesn't puzzle over why 56809 (MSB=1) sits on the left:

Suggested change
// Test_InterimNodeWithBothChildren verifies that the hash value of an interim node with
// both children (left and right) is computed correctly.
// Test_InterimNodeWithBothChildren verifies that the hash value of an interim node with
// both children (left and right) is computed correctly:
//
// NewInterimNode(1, leftChild, rightChild)
// (h=1)
// / \
// leftChild=Leaf rightChild=Leaf
// (56809, h=0) (2, h=0)
//
// Note: the construction is synthetic — the child paths are not consistent with their branch
// positions (NewInterimNode does not validate this); only the resulting node hash (a function
// of the two child hashes) is under test.

Comment on lines +549 to +550
// Test_AllLeafHashes verifies that AllLeafHashes collects the leaf hash of every allocated register in
// the subtree (here: three leaves across a small two-level interim structure).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-level shape is the one non-obvious structure among these tests — a diagram helps. This suggestion only touches the doc comment; it composes with my other comment on the fixture (using three distinct registers), which should be applied to the body:

Suggested change
// Test_AllLeafHashes verifies that AllLeafHashes collects the leaf hash of every allocated register in
// the subtree (here: three leaves across a small two-level interim structure).
// Test_AllLeafHashes verifies that AllLeafHashes collects the leaf hash of every allocated register in
// the subtree (here: three leaves across a small two-level interim structure). Structure under test
// (apply the fixture fix from my other comment first, so n1/n2/n3 are three DISTINCT registers):
//
// n5 (h=2)
// / \
// n4 (h=1) n3 (h=1)
// / \
// n1 (h=0) n2 (h=0)

@janezpodhostnik janezpodhostnik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Alex already covered everything I had

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ledger/complete/payloadless/equivalence_test.go`:
- Around line 374-396: Update the equivalence assertions around mByPath and
plByPath to derive the expected proof category from each path’s allocation
state, asserting Inclusion=true for allocated and explicitly-unallocated paths
and Inclusion=false for never-allocated paths before comparing structural
fields. Do not use mp.Inclusion as the oracle; retain the existing payloadless
LeafHash checks for the allocated versus explicitly-unallocated cases.
- Around line 480-484: In the live-path loop over live, add an assertion that
mByPath[p].Value() equals expectedValue before validating the payloadless hash.
Keep the existing non-empty, payload presence, and expectedLeafHash assertions
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77a343be-c347-4e02-b2aa-6307671b7239

📥 Commits

Reviewing files that changed from the base of the PR and between df59e78 and e74e43d.

📒 Files selected for processing (4)
  • ledger/complete/payloadless/equivalence_test.go
  • ledger/complete/payloadless/json_test.go
  • ledger/complete/payloadless/node_test.go
  • ledger/complete/payloadless/trie_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • ledger/complete/payloadless/node_test.go
  • ledger/complete/payloadless/trie_test.go

Comment on lines +374 to +396
for _, p := range queryPaths {
mp := mByPath[p]
plp := plByPath[p]

require.Equalf(t, mp.Inclusion, plp.Inclusion, "Inclusion mismatch for path %x", p[:])
require.Equalf(t, mp.Steps, plp.Steps, "Steps mismatch for path %x", p[:])
require.Equalf(t, mp.Flags, plp.Flags, "Flags mismatch for path %x", p[:])
require.Equalf(t, mp.Interims, plp.Interims, "Interims mismatch for path %x", p[:])
require.Equal(t, mp.Path, plp.Path)

if mp.Inclusion {
if mp.Payload.IsEmpty() {
// Explicitly-unallocated register (default leaf kept by prune=false):
// payloadless reports Inclusion=true with LeafHash=nil.
require.Nilf(t, plp.LeafHash,
"payloadless should report nil LeafHash for explicitly-unallocated path %x", p[:])
} else {
require.NotNilf(t, plp.LeafHash,
"payloadless should report non-nil LeafHash for allocated path %x", p[:])
expected := hash.HashLeaf(hash.Hash(mp.Path), []byte(mp.Payload.Value()))
require.Equalf(t, expected, *plp.LeafHash, "leaf hash mismatch for path %x", p[:])
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert each query category’s expected proof semantics independently.

Using mp.Inclusion as the oracle lets both implementations incorrectly return non-inclusion for explicitly unallocated leaves without failing. Assert allocated and explicitly unallocated paths have Inclusion=true, while never-allocated paths have Inclusion=false; then compare structural fields.

This is the unresolved case identified in the earlier review.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/complete/payloadless/equivalence_test.go` around lines 374 - 396,
Update the equivalence assertions around mByPath and plByPath to derive the
expected proof category from each path’s allocation state, asserting
Inclusion=true for allocated and explicitly-unallocated paths and
Inclusion=false for never-allocated paths before comparing structural fields. Do
not use mp.Inclusion as the oracle; retain the existing payloadless LeafHash
checks for the allocated versus explicitly-unallocated cases.

Comment on lines +480 to +484
for p, expectedValue := range live {
require.Falsef(t, mByPath[p].IsEmpty(), "mtrie should return non-empty payload for live path at step %d", step)
require.NotNilf(t, plByPath[p], "payloadless missing live path at step %d", step)
expectedLeafHash := hash.HashLeaf(hash.Hash(p), expectedValue)
require.Equalf(t, expectedLeafHash, *plByPath[p], "leaf hash mismatch for live path at step %d", step)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify the full-trie payload value, not only its presence.

A wrong non-empty value from m.UnsafeRead currently passes. Compare mByPath[p].Value() with expectedValue before validating the payloadless hash.

Proposed assertion
 for p, expectedValue := range live {
     require.Falsef(t, mByPath[p].IsEmpty(), "mtrie should return non-empty payload for live path at step %d", step)
+    require.Equalf(t, expectedValue, []byte(mByPath[p].Value()), "mtrie value mismatch for live path at step %d", step)
     require.NotNilf(t, plByPath[p], "payloadless missing live path at step %d", step)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ledger/complete/payloadless/equivalence_test.go` around lines 480 - 484, In
the live-path loop over live, add an assertion that mByPath[p].Value() equals
expectedValue before validating the payloadless hash. Keep the existing
non-empty, payload presence, and expectedLeafHash assertions unchanged.

@blacksmith-sh

This comment has been minimized.

@zhangchiqing
zhangchiqing enabled auto-merge July 16, 2026 21:20
@zhangchiqing
zhangchiqing added this pull request to the merge queue Jul 16, 2026
Merged via the queue into master with commit 163553d Jul 16, 2026
173 of 175 checks passed
@zhangchiqing
zhangchiqing deleted the leo/payloadless-tests-base branch July 16, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants