Skip to content

[Storehouse] 001 - Add payloadless node and trie implementation#8569

Merged
zhangchiqing merged 11 commits into
masterfrom
leo/payloadless
Jul 10, 2026
Merged

[Storehouse] 001 - Add payloadless node and trie implementation#8569
zhangchiqing merged 11 commits into
masterfrom
leo/payloadless

Conversation

@zhangchiqing

@zhangchiqing zhangchiqing commented May 30, 2026

Copy link
Copy Markdown
Member

This PR adds the payloadless node and trie implementation. The base branch already contains a copy of the mtrie code in the payloadless package, so this PR's diff shows only the payloadless-specific changes, making review easier.

Test cases are created in #8572
Find Spec

Summary by CodeRabbit

  • New Features
    • Added payloadless Merkle trie support with immutable updates, register-count querying, traversal, validation, JSON export, and root-hash equality checks.
    • Added payloadless inclusion/non-inclusion proof types, batch proof handling, and proof inspection/comparison.
    • Added utilities to derive compact-trie values from an existing leaf hash.
  • Documentation
    • Improved Memory-Trie (MTrie) storage model and update-algorithm wording and pseudocode formatting.
  • Bug Fixes
    • Corrected a small SHA3 comment typo and confirmed matching hashing behavior.
  • Tests
    • Added coverage for payloadless trie/node behavior and for SHA3 hashing path equivalence.

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34d26f6b-7439-40f2-89fb-7757907479b8

📥 Commits

Reviewing files that changed from the base of the PR and between 917b5db and 9b0aa2a.

📒 Files selected for processing (2)
  • ledger/complete/payloadless/node.go
  • ledger/complete/payloadless/node_test.go
💤 Files with no reviewable changes (1)
  • ledger/complete/payloadless/node.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • ledger/complete/payloadless/node_test.go

📝 Walkthrough

Walkthrough

Adds a payloadless Merkle trie with cached nodes, copy-on-write updates, register accounting, proof generation, hashing helpers, extensive tests, and revised MTrie algorithm documentation.

Changes

Payloadless trie foundation

Layer / File(s) Summary
Hashing and proof contracts
ledger/common/hash/*, ledger/trie.go, ledger/payloadless_proof.go
Adds compact-hash computation from a leaf hash, payloadless proof and batch-proof types, and SHA3 path-equivalence coverage.
Payloadless node model
ledger/complete/payloadless/node.go, ledger/complete/payloadless/node_test.go
Adds cached-hash nodes, leaf/interim constructors, compactification, predicates, accessors, verification, and comprehensive node tests.
Trie operations and proofs
ledger/complete/payloadless/trie.go, ledger/complete/payloadless/trie_test.go
Adds immutable trie construction, reads, copy-on-write updates, register accounting, proofs, traversal, serialization, and validation.
Algorithm documentation and references
ledger/complete/mtrie/README.md, ledger/complete/mtrie/trie/trie_test.go
Revises MTrie update pseudocode and updates root-hash reference comments.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: holyfuchs, fxamacker, janezpodhostnik

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MTrie
  participant Node
  participant BatchProof
  Client->>MTrie: Update registers
  MTrie->>Node: Rebuild affected paths
  Node-->>MTrie: Return cached hashes
  Client->>MTrie: Request proofs
  MTrie->>BatchProof: Populate inclusion and sibling hashes
  BatchProof-->>Client: Return proof batch
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding payloadless node and trie implementation.
Docstring Coverage ✅ Passed Docstring coverage is 97.92% which is sufficient. The required threshold is 80.00%.
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

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.

Comment thread ledger/complete/payloadless/trie.go Outdated
hasValue := len(values[i]) > 0
var newLeafHash hash.Hash
if hasValue {
newLeafHash = hash.HashLeaf(hash.Hash(paths[i]), values[i])

@zhangchiqing zhangchiqing May 30, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

leaf hash is computed here to be shared in the next if and else branches

Comment thread ledger/complete/payloadless/trie.go Outdated
root *node.Node
root *Node
regCount uint64 // number of registers allocated in the trie
regSize uint64 // size of registers allocated in the trie

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

regSize is removed from the trie, because we won't be able to know the actual size. When updating an existing register, we know the new size, but we don't know the old size, so we can't update the regSize with correct value. Given that, this field is removed. Currently the regSize is used by metrics reporting only.

// allocatedRegCountDelta and allocatedRegSizeDelta should both be 0
return n, 0, 0, nodeHeight
// no compact leaf node from its ancester, it means we are storing a value on a new path,
n = NewLeaf(paths[0], values[0], nodeHeight)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why DeepCopy is not needed here?

The original mtrie needs DeepCopy to protect the payload value slice from being mutated. But for the payloadless trie, the leaf hash is computed from the payload value (HashLeaf), so the leaf no longer reference any shared byte slices with the caller. A DeepCopy would be wasteful.

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.

Isn't it a problem that LeafHash() returns a *hash.Hash which then gets publically surfaced via MTrie.ReadSingleLeafHash and MTrie.UnsafeRead. should we coppy the hash when returning from LeafHash().

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.

Good flag, beyond discussing it in the PR a comment in the code is useful. I have added respective documentation to the NewLeaf constructor:

// It is safe for `path` or `value` to be mutated after the call to `NewLeaf` returns.
//
// UNCHECKED requirement: height must be non-negative.
func NewLeaf(path ledger.Path, value []byte, height int) *Node {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

https://github.com/onflow/flow-go/pull/8573/changes#diff-3677adfcc780fd8b4d5e84d5f0f83345558194173cd3049f852e8f4ffc733809R113

The defensive copy (copyLeafHash) is added to the forest.

This is better than adding the copy in trie's ReadSingleLeafHash. Because 1) the execution node interact with trie through the forest, which has the defensive copy. 2) the trie's API is lower level, and other methods like "UnsafeRead" is also under a "do no modify" contract".

But it's a good flag. I will add comments to the ReadSingleLeafHash as well as the AllLeafHashes (which isn't being used anywhere) to highlight they should not be modified.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

updated d621150

// for retrieving the original value separately if needed.
type PayloadlessTrieProof struct {
Path Path // path
LeafHash *hash.Hash // leaf hash HashLeaf(path, value); nil for empty leaves and non-inclusion proofs

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The PayloadlessTrieProof is copied from ledger/trie.go#TrieProof.

The difference is the Payload field is changed to LeafHash field

@zhangchiqing zhangchiqing marked this pull request as ready for review June 3, 2026 18:23
@zhangchiqing zhangchiqing requested a review from a team as a code owner June 3, 2026 18:23
@zhangchiqing zhangchiqing requested review from AlexHentschel and janezpodhostnik and removed request for a team June 3, 2026 22:41
@zhangchiqing zhangchiqing changed the title [Storehouse] Add payloadless node and trie implementation [Storehouse] Spec 001 - Add payloadless node and trie implementation Jun 8, 2026
@zhangchiqing zhangchiqing changed the title [Storehouse] Spec 001 - Add payloadless node and trie implementation [Storehouse] 001 - Add payloadless node and trie implementation Jun 9, 2026
Comment thread ledger/trie.go Outdated
// is moved to a higher position in the trie.
// UNCHECKED requirement: fromHeight < toHeight
// UNCHECKED requirement: fromHeight >= 0
func ExtendHashToHeight(path hash.Hash, baseHash hash.Hash, fromHeight, toHeight int) hash.Hash {

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.

fromHeight looks like an unnecessary parameter


// NewMTrie returns a Mtrie given the root
func NewMTrie(root *node.Node, regCount uint64, regSize uint64) (*MTrie, error) {
func NewMTrie(root *Node, regCount uint64) (*MTrie, error) {

This comment was marked as resolved.

Comment thread ledger/complete/payloadless/trie.go Outdated
Comment thread ledger/complete/payloadless/node.go Outdated
@zhangchiqing zhangchiqing force-pushed the leo/payloadless-base branch from 5cbb377 to c51de07 Compare July 2, 2026 17:58
…ne docs, add node/trie tests

Encapsulate the "move a compact leaf to a new height" operation inside node.go
(NewRelevelledLeaf) so trie.go business logic no longer branches on the allocated-
vs-unallocated-register distinction; refine node/trie documentation and the mtrie
README Update case taxonomy; add exhaustive node_test.go / trie_test.go coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
AlexHentschel and others added 3 commits July 8, 2026 22:55
Co-authored-by: Leo Zhang <zhangchiqing@gmail.com>
Co-authored-by: Leo Zhang <zhangchiqing@gmail.com>
Suggested amendments for PR #8569: `[Storehouse] 001 - Add payloadless node and trie implementation`

@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.

Thanks. Very happy with the result.

Disclaimer: some portion of changes in this PR has been introduced by my suggested changes (see my merged changed #8599 targeting the branch of this PR). For other reviewers, I have provided a variety of comments motivating and explaining my changes; hope that simplifies further reviews.

@janez if you were happy with Leo's PR before, I pretty much didn't change any behaviour, just documentation and encapsulation of logic.

Only a single minor nit-pick for Leo to address:

👉 my comment #8569 (comment)

All other comments I left are purely informational to explain my changes.


Happy for this to be merged. Thanks for your patience Leo 🙇

// allocatedRegCountDelta and allocatedRegSizeDelta should both be 0
return n, 0, 0, nodeHeight
// no compact leaf node from its ancester, it means we are storing a value on a new path,
n = NewLeaf(paths[0], values[0], nodeHeight)

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.

Good flag, beyond discussing it in the PR a comment in the code is useful. I have added respective documentation to the NewLeaf constructor:

// It is safe for `path` or `value` to be mutated after the call to `NewLeaf` returns.
//
// UNCHECKED requirement: height must be non-negative.
func NewLeaf(path ledger.Path, value []byte, height int) *Node {

Comment thread ledger/complete/payloadless/trie.go Outdated
Comment on lines -426 to -439
if len(paths) == 1 {
// check if the only path to update has the same payload.
// if payload is the same, we could skip the update to avoid creating duplicated node
if !currentNode.Payload().ValueEquals(&payloads[i]) {
n = node.NewLeaf(paths[i], payloads[i].DeepCopy(), nodeHeight)

allocatedRegCountDelta, allocatedRegSizeDelta =
computeAllocatedRegDeltas(currentNode.Payload(), &payloads[i])

return n, allocatedRegCountDelta, allocatedRegSizeDelta, nodeHeight
}
// avoid creating a new node when the same payload is written
return currentNode, 0, 0, nodeHeight
}

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 is one of the few algorithmic changes I made in my PR #8599 (already applied here).

Essentially I have moved this code block outside of the loop, for logical clarity. Less checks of the unchanging condition len(paths) == 1 is a nice side effect but was not the goal of the change. Moving the code block is purely aesthetically (hopefully meaningfully improving clarity in conjunction with the extended documentation) - the algorithmic behaviour is entirely unchanged in regards to to the conditions handled by this code block.

The if len(paths) == 1 condition is now paired with requiring paths[0] == currentPath which is true if and only if the code block lines 426 - 439 was reached in the old version 👇 [new version]

// [Recursion Base Case] README case 2.a.i: the single updated path coincides with `currentNode`'s path,
// so we overwrite the register represented by the existing leaf in place.
if len(paths) == 1 && (paths[0] == currentPath) {
// In most cases, the new register value will be different from the old value, in which case we need to instantiate a new leaf
// anyway. So we optimistically create the new leaf first. But if a posterior check reveals that the new leaf's hash is identical
// to the old `currentNode`, we just return `currentNode` to avoid duplication (optimistically created leaf is garbage collected).
n = NewLeaf(paths[0], values[0], nodeHeight)
if n.hashValue == currentNode.hashValue {
return currentNode, 0, nodeHeight
}
allocatedRegCountDelta = computeAllocatedRegCountDelta(currentNode.IsAllocatedRegisterLeaf(), n.IsAllocatedRegisterLeaf())
return n, allocatedRegCountDelta, nodeHeight
}

Comment thread ledger/complete/payloadless/trie.go Outdated
Comment on lines -441 to -446
found = true

allocatedRegCountDelta, allocatedRegSizeDelta =
computeAllocatedRegDeltasFromHigherHeight(currentNode.Payload())

break

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 iteration over paths and the check whether p is found in paths is contained has now been consolidated into

// [Recursive Case] README case 2.a.ii or 2.b, both FALL THROUGH to the SPLIT-AND-RECURSE section below
if slices.Contains(paths, currentPath) { // `currentNode.path ∈ paths` and `len(paths) > 1`: README case 2.a.ii
// The register at `currentNode`'s path is among the updated `paths`, so its value will be overwritten. Here we
// only account for removing `currentNode`'s own contribution to the count; the new value is counted separately,
// deeper in the recursion, when its leaf is (re)created. Dropping `currentNode` yields -1 if it held an
// allocated register and 0 if it was a default (unallocated) leaf.
allocatedRegCountDelta = computeAllocatedRegCountDelta(currentNode.IsAllocatedRegisterLeaf(), false) // drop `currentNode`
} else { // `currentNode.path ∉ paths`: README case 2.b
// `currentNode` carries a path that is not among the updated `paths`. Hence it represents a compact leaf
// that must be carried down the recursion.
compactLeaf = currentNode
// INSIGHT [not implemented]: formally `currentNode` could be set to nil here. This proof (presumably relatively short)
// should be written up in the readme, to keep the complexity out of the code.
}

Previously, those logic conceptually belonging together was more separated in the old code version.

Comment thread ledger/complete/payloadless/trie.go Outdated
Comment on lines -540 to -575
n = node.NewInterimNode(nodeHeight, newLeftChild, newRightChild)
return n, allocatedRegCountDelta, allocatedRegSizeDelta, lowestHeightTouched
}

// computeAllocatedRegDeltasFromHigherHeight returns the deltas
// needed to compute the allocated reg count and reg size when
// a payload is updated or unallocated at a lower height.
func computeAllocatedRegDeltasFromHigherHeight(oldPayload *ledger.Payload) (allocatedRegCountDelta, allocatedRegSizeDelta int64) {
if !oldPayload.IsEmpty() {
// Allocated register will be updated or unallocated at lower height.
allocatedRegCountDelta--
}
oldPayloadSize := oldPayload.Size()
allocatedRegSizeDelta -= int64(oldPayloadSize)
return
n = NewInterimNode(nodeHeight, newLeftChild, newRightChild)
return n, allocatedRegCountDelta, lowestHeightTouched
}

// computeAllocatedRegDeltas returns the allocated reg count
// and reg size deltas computed from old payload and new payload.
// PRECONDITION: !oldPayload.Equals(newPayload)
func computeAllocatedRegDeltas(oldPayload, newPayload *ledger.Payload) (allocatedRegCountDelta, allocatedRegSizeDelta int64) {
allocatedRegCountDelta = 0
if newPayload.IsEmpty() {
// Old payload is not empty while new payload is empty.
// Allocated register will be unallocated.
allocatedRegCountDelta = -1
} else if oldPayload.IsEmpty() {
// Old payload is empty while new payload is not empty.
// Unallocated register will be allocated.
allocatedRegCountDelta = 1
}

oldPayloadSize := oldPayload.Size()
newPayloadSize := newPayload.Size()
allocatedRegSizeDelta = int64(newPayloadSize - oldPayloadSize)
return

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.

consolidated into computeAllocatedRegCountDelta method

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 AI applied this clarification also to this file, increasing change surface. If that results in conflicts we could drop the changes here and re-apply them in the end. @zhangchiqing

Comment thread ledger/trie.go
//
// The function extends the leaf hash from height 0 to nodeHeight by hashing upward
// through the trie structure, combining with default hashes at each level.
func ComputeCompactValueFromLeafHash(path hash.Hash, leafHash hash.Hash, nodeHeight int) hash.Hash {

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.

Suggested change
func ComputeCompactValueFromLeafHash(path hash.Hash, leafHash hash.Hash, nodeHeight int) hash.Hash {
// TODO: tentative method; should eventually be replaced by node-provided methods, as the node as
// the node technically defined how to compute hashes, and we should encapsulate the logic there.
func ComputeCompactValueFromLeafHash(path hash.Hash, leafHash hash.Hash, nodeHeight int) hash.Hash {

@@ -354,120 +253,146 @@ func NewTrieWithUpdatedRegisters(
// updateResult is a wrapper of return values from update().
// It's used to communicate values from goroutine.
type updateResult struct {

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.

my PR #8599 extended the documentation of this method. When reviewing, you'll see some changes, but none of them change the higher-level behaviour of the update method (at least the changes should not 😅 ).

My main goal was to increase encapsulation, by moving everything hash-computation related into node, which already is the authority on how the leaf hashes are computed in detail.

Comment thread ledger/complete/payloadless/trie.go Outdated
return i
}

func minInt(a, b int) int {

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.

now provided natively by go

@zhangchiqing zhangchiqing changed the base branch from leo/payloadless-base to master July 10, 2026 15:06
@zhangchiqing zhangchiqing enabled auto-merge July 10, 2026 15:07
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

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

Scanned Files

None

@codecov-commenter

codecov-commenter commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 32.86290% with 333 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ledger/complete/payloadless/trie.go 27.27% 192 Missing and 8 partials ⚠️
ledger/payloadless_proof.go 0.00% 88 Missing ⚠️
ledger/complete/payloadless/node.go 68.75% 39 Missing and 1 partial ⚠️
ledger/trie.go 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@blacksmith-sh

This comment has been minimized.

@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: 1

🤖 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/node.go`:
- Around line 384-391: Update the Node.Path method to guard against a nil
receiver before accessing n.path; return nil when n is nil or not a leaf, while
preserving the existing pointer return for leaf nodes.
🪄 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: 0c785d32-b9fa-43a7-976f-aa6139361cc6

📥 Commits

Reviewing files that changed from the base of the PR and between a983fda and 917b5db.

📒 Files selected for processing (10)
  • ledger/common/hash/sha3.go
  • ledger/common/hash/sha3_internal_test.go
  • ledger/complete/mtrie/README.md
  • ledger/complete/mtrie/trie/trie_test.go
  • ledger/complete/payloadless/node.go
  • ledger/complete/payloadless/node_test.go
  • ledger/complete/payloadless/trie.go
  • ledger/complete/payloadless/trie_test.go
  • ledger/payloadless_proof.go
  • ledger/trie.go

Comment on lines +384 to +391
// Path returns a pointer to the Node's register storage path.
// If the node is not a leaf, the function returns `nil`.
func (n *Node) Path() *ledger.Path {
if n.IsLeaf() {
return &n.path
}
return nil
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Path() panics on nil receiver despite IsLeaf() being nil-safe.

IsLeaf() returns true for a nil node, but Path() then dereferences n.path, causing a nil pointer panic. All current callers check for nil before calling Path(), but the nil-safety of IsLeaf() creates a trap for future callers.

🛡️ Proposed fix: add nil guard to `Path()`
 func (n *Node) Path() *ledger.Path {
-	if n.IsLeaf() {
+	if n == nil || !n.IsLeaf() {
+		return nil
+	}
+	if n.IsLeaf() {
 		return &n.path
 	}
 	return nil
 }

Or more concisely:

 func (n *Node) Path() *ledger.Path {
-	if n.IsLeaf() {
+	if n == nil || n.lChild != nil || n.rChild != nil {
+		return nil
+	}
+	if n.IsLeaf() {
 		return &n.path
 	}
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Path returns a pointer to the Node's register storage path.
// If the node is not a leaf, the function returns `nil`.
func (n *Node) Path() *ledger.Path {
if n.IsLeaf() {
return &n.path
}
return nil
}
// Path returns a pointer to the Node's register storage path.
// If the node is not a leaf, the function returns `nil`.
func (n *Node) Path() *ledger.Path {
if n == nil || !n.IsLeaf() {
return nil
}
if n.IsLeaf() {
return &n.path
}
return nil
}
🤖 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/node.go` around lines 384 - 391, Update the
Node.Path method to guard against a nil receiver before accessing n.path; return
nil when n is nil or not a leaf, while preserving the existing pointer return
for leaf nodes.

@zhangchiqing zhangchiqing added this pull request to the merge queue Jul 10, 2026
Merged via the queue into master with commit 2e2df5e Jul 10, 2026
61 checks passed
@zhangchiqing zhangchiqing deleted the leo/payloadless branch July 10, 2026 17:36
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