From 95bd95cf82817d366e27dba85f7214aa75cbbc30 Mon Sep 17 00:00:00 2001 From: i-norden Date: Tue, 23 May 2023 14:31:34 -0500 Subject: [PATCH 1/3] node.forEach methods that allow us to track node positions and extract all nodes IPLD objects (including internal nodes) --- node.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/node.go b/node.go index 2d85fdf..8fe5b41 100644 --- a/node.go +++ b/node.go @@ -348,6 +348,145 @@ func (n *node) forEachAt(ctx context.Context, bs cbor.IpldStore, bitWidth uint, return nil } +// Recursive implementation backing ForEach and ForEachAt. Performs a +// depth-first walk of the tree, beginning at the 'start' index. The 'offset' +// argument helps us locate the lateral position of the current node so we can +// figure out the appropriate 'index', since indexes are not stored with values +// and can only be determined by knowing how far a leaf node is removed from +// the left-most leaf node. +// This method also provides the trail of indices at each height/level in the way to the current node, which can be used to formulate a selector suffixes +func (n *node) forEachAtTracked(ctx context.Context, bs cbor.IpldStore, trail []int, bitWidth uint, height int, start, offset uint64, cb func(uint64, *cbg.Deferred, []int) error) error { + if height == 0 { + // height=0 means we're at leaf nodes and get to use our callback + for i, v := range n.values { + if v != nil { + ix := offset + uint64(i) + if ix < start { + // if we're here, 'start' is probably somewhere in the + // middle of this node's elements + continue + } + + // use 'offset' to determine the actual index for this element, it + // tells us how distant we are from the left-most leaf node + if err := cb(ix, v, append(trail, i)); err != nil { + return err + } + } + } + + return nil + } + + subCount := nodesForHeight(bitWidth, height) + for i, ln := range n.links { + if ln == nil { + continue + } + + // 'offs' tells us the index of the left-most element of the subtree defined + // by 'sub' + offs := offset + (uint64(i) * subCount) + nextOffs := offs + subCount + // nextOffs > offs checks for overflow at MaxIndex (where the next offset wraps back + // to 0). + if nextOffs >= offs && start >= nextOffs { + // if we're here, 'start' lets us skip this entire sub-tree + continue + } + + subn, err := ln.load(ctx, bs, bitWidth, height-1) + if err != nil { + return err + } + + // recurse into the child node, providing 'offs' to tell it where it's + // located in the tree + if err := subn.forEachAtTracked(ctx, bs, append(trail, i), bitWidth, height-1, start, offs, cb); err != nil { + return err + } + } + return nil +} + +// b *bytes.Buffer, sink func(node ipld.Node) error +// Recursive implementation backing ForEach and ForEachAt. Performs a +// depth-first walk of the tree, beginning at the 'start' index. The 'offset' +// argument helps us locate the lateral position of the current node so we can +// figure out the appropriate 'index', since indexes are not stored with values +// and can only be determined by knowing how far a leaf node is removed from +// the left-most leaf node. +// This method also provides the trail of indices at each height/level in the way to the current node, which can be used to formulate a selector suffixes +func (n *node) forEachAtTrackedWithNodeSink(ctx context.Context, bs cbor.IpldStore, trail []int, bitWidth uint, height int, start, offset uint64, b *bytes.Buffer, sink cbg.CBORUnmarshaler, cb func(uint64, *cbg.Deferred, []int) error) error { + if sink != nil { + if b == nil { + b = bytes.NewBuffer(nil) + } + b.Reset() + internalNode, err := n.compact(ctx, bitWidth, height) + if err != nil { + return err + } + if err := internalNode.MarshalCBOR(b); err != nil { + return err + } + if err := sink.UnmarshalCBOR(b); err != nil { + return err + } + } + if height == 0 { + // height=0 means we're at leaf nodes and get to use our callback + for i, v := range n.values { + if v != nil { + ix := offset + uint64(i) + if ix < start { + // if we're here, 'start' is probably somewhere in the + // middle of this node's elements + continue + } + + // use 'offset' to determine the actual index for this element, it + // tells us how distant we are from the left-most leaf node + if err := cb(ix, v, append(trail, i)); err != nil { + return err + } + } + } + + return nil + } + + subCount := nodesForHeight(bitWidth, height) + for i, ln := range n.links { + if ln == nil { + continue + } + + // 'offs' tells us the index of the left-most element of the subtree defined + // by 'sub' + offs := offset + (uint64(i) * subCount) + nextOffs := offs + subCount + // nextOffs > offs checks for overflow at MaxIndex (where the next offset wraps back + // to 0). + if nextOffs >= offs && start >= nextOffs { + // if we're here, 'start' lets us skip this entire sub-tree + continue + } + + subn, err := ln.load(ctx, bs, bitWidth, height-1) + if err != nil { + return err + } + + // recurse into the child node, providing 'offs' to tell it where it's + // located in the tree + if err := subn.forEachAtTracked(ctx, bs, append(trail, i), bitWidth, height-1, start, offs, cb); err != nil { + return err + } + } + return nil +} + var errNoVals = fmt.Errorf("no values") // Recursive implementation of FirstSetIndex that's performed on the left-most @@ -494,6 +633,37 @@ func (n *node) flush(ctx context.Context, bs cbor.IpldStore, bitWidth uint, heig return nd, nil } +// compact converts a node into its internal.Node representation +func (n *node) compact(ctx context.Context, bitWidth uint, height int) (*internal.Node, error) { + nd := new(internal.Node) + nd.Bmap = make([]byte, bmapBytes(bitWidth)) + + if height == 0 { + // leaf node, we're storing values in this node + for i, val := range n.values { + if val == nil { + continue + } + nd.Values = append(nd.Values, val) + // set the bit in the bitmap for this position to indicate its presence + nd.Bmap[i/8] |= 1 << (uint(i) % 8) + } + return nd, nil + } + + // non-leaf node, we're only storing Links in this node + for i, ln := range n.links { + if ln == nil { + continue + } + nd.Links = append(nd.Links, ln.cid) + // set the bit in the bitmap for this position to indicate its presence + nd.Bmap[i/8] |= 1 << (uint(i) % 8) + } + + return nd, nil +} + func (n *node) setLink(bitWidth uint, i uint64, l *link) { if n.links == nil { if l == nil { From dc372fdaaa7eeb3889264c37124018071d970d1b Mon Sep 17 00:00:00 2001 From: i-norden Date: Tue, 23 May 2023 14:34:57 -0500 Subject: [PATCH 2/3] Root.ForEach methods that use the node.forEachTrackedWithNodeSink methods --- amt.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/amt.go b/amt.go index 586a088..6038e77 100644 --- a/amt.go +++ b/amt.go @@ -321,6 +321,19 @@ func (r *Root) ForEachAt(ctx context.Context, start uint64, cb func(uint64, *cbg return r.node.forEachAt(ctx, r.store, r.bitWidth, r.height, start, 0, cb) } +// ForEachTrackedWithNodeSink iterates over the entire AMT and calls the cb function for each +// entry found in the leaf nodes. The callback will receive the index and the +// value of each element. +func (r *Root) ForEachTrackedWithNodeSink(ctx context.Context, b *bytes.Buffer, sink cbg.CBORUnmarshaler, cb func(uint64, *cbg.Deferred, []int) error) error { + return r.node.forEachAtTrackedWithNodeSink(ctx, r.store, []int{}, r.bitWidth, r.height, 0, 0, b, sink, cb) +} + +// ForEachAtTrackedWithNodeSink iterates over the AMT beginning from the given start index. See +// ForEach for more details. +func (r *Root) ForEachAtTrackedWithNodeSink(ctx context.Context, start uint64, b *bytes.Buffer, sink cbg.CBORUnmarshaler, cb func(uint64, *cbg.Deferred, []int) error) error { + return r.node.forEachAtTrackedWithNodeSink(ctx, r.store, []int{}, r.bitWidth, r.height, start, 0, b, sink, cb) +} + // FirstSetIndex finds the lowest index in this AMT that has a value set for // it. If this operation is called on an empty AMT, an ErrNoValues will be // returned. From 008f534ed4a03fccfebe1dcb9e18e6ae02ab465a Mon Sep 17 00:00:00 2001 From: i-norden Date: Tue, 23 May 2023 14:37:02 -0500 Subject: [PATCH 3/3] Diff methods using node.forEachTracked/node.forEachTrackedWithNodeSink methods --- diff.go | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- node.go | 26 +++- 2 files changed, 396 insertions(+), 8 deletions(-) diff --git a/diff.go b/diff.go index c9bfa90..dcce288 100644 --- a/diff.go +++ b/diff.go @@ -27,10 +27,11 @@ const ( // Change represents a change to a DAG and contains a reference to the old and // new CIDs. type Change struct { - Type ChangeType - Key uint64 - Before *cbg.Deferred - After *cbg.Deferred + Type ChangeType + Key uint64 + Before *cbg.Deferred + After *cbg.Deferred + SelectorSuffix []int } func (ch Change) String() string { @@ -77,6 +78,46 @@ func Diff(ctx context.Context, prevBs, curBs cbor.IpldStore, prev, cur cid.Cid, return diffNode(ctx, prevCtx, curCtx, prevAmt.node, curAmt.node, 0) } +// DiffTrackedWithNodeSink returns a set of changes that transform node 'a' into node 'b'. opts are applied to both prev and cur. +// it associates selector suffixes with the emitted Change set and sinks all unique nodes encountered under the current CID to the provided CBORUnmarshaler +func DiffTrackedWithNodeSink(ctx context.Context, prevBs, curBs cbor.IpldStore, prev, cur cid.Cid, b *bytes.Buffer, sink cbg.CBORUnmarshaler, trail []int, opts ...Option) ([]*Change, error) { + prevAmt, err := LoadAMT(ctx, prevBs, prev, opts...) + if err != nil { + return nil, xerrors.Errorf("loading previous root: %w", err) + } + + prevCtx := &nodeContext{ + bs: prevBs, + bitWidth: prevAmt.bitWidth, + height: prevAmt.height, + } + + curAmt, err := LoadAMT(ctx, curBs, cur, opts...) + if err != nil { + return nil, xerrors.Errorf("loading current root: %w", err) + } + + // TODO: remove when https://github.com/filecoin-project/go-amt-ipld/issues/54 is closed. + if curAmt.bitWidth != prevAmt.bitWidth { + return nil, xerrors.Errorf("diffing AMTs with differing bitWidths not supported (prev=%d, cur=%d)", prevAmt.bitWidth, curAmt.bitWidth) + } + + curCtx := &nodeContext{ + bs: curBs, + bitWidth: curAmt.bitWidth, + height: curAmt.height, + } + + // edge case of diffing an empty AMT against non-empty + if prevAmt.count == 0 && curAmt.count != 0 { + return addAllTrackWithNodeSink(ctx, curCtx, curAmt.node, 0, b, sink, trail) + } + if prevAmt.count != 0 && curAmt.count == 0 { + return removeAllTracked(ctx, prevCtx, prevAmt.node, 0, trail) + } + return diffNodeTrackedWithNodeSink(ctx, prevCtx, curCtx, prevAmt.node, curAmt.node, 0, b, sink, trail) +} + type nodeContext struct { bs cbor.IpldStore // store containining AMT data bitWidth uint // bit width of AMT @@ -302,6 +343,220 @@ func diffNode(ctx context.Context, prevCtx, curCtx *nodeContext, prev, cur *node return changes, nil } +func diffNodeTrackedWithNodeSink(ctx context.Context, prevCtx, curCtx *nodeContext, prev, cur *node, offset uint64, b *bytes.Buffer, sink cbg.CBORUnmarshaler, trail []int) ([]*Change, error) { + if prev == nil && cur == nil { + return nil, nil + } + + if prev == nil { + return addAllTrackWithNodeSink(ctx, curCtx, cur, offset, b, sink, trail) + } + + if cur == nil { + return removeAllTracked(ctx, prevCtx, prev, offset, trail) + } + + if prevCtx.height == 0 && curCtx.height == 0 { + return diffLeavesTrackedWithNodeSink(ctx, curCtx.bitWidth, prev, cur, offset, b, sink, trail) + } + + var changes []*Change + + if curCtx.height > prevCtx.height { + subCount := curCtx.nodesAtHeight() + for i, ln := range cur.links { + if ln == nil || ln.cid == cid.Undef { + continue + } + + subCtx := &nodeContext{ + bs: curCtx.bs, + bitWidth: curCtx.bitWidth, + height: curCtx.height - 1, + } + + subn, err := ln.load(ctx, subCtx.bs, subCtx.bitWidth, subCtx.height) + if err != nil { + return nil, err + } + + offs := offset + (uint64(i) * subCount) + if i == 0 { + cs, err := diffNodeTrackedWithNodeSink(ctx, prevCtx, subCtx, prev, subn, offs, b, sink, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + } else { + cs, err := addAllTrackWithNodeSink(ctx, subCtx, subn, offs, b, sink, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + } + } + + return changes, nil + } + + if prevCtx.height > curCtx.height { + subCount := prevCtx.nodesAtHeight() + for i, ln := range prev.links { + if ln == nil || ln.cid == cid.Undef { + continue + } + + subCtx := &nodeContext{ + bs: prevCtx.bs, + bitWidth: prevCtx.bitWidth, + height: prevCtx.height - 1, + } + + subn, err := ln.load(ctx, subCtx.bs, subCtx.bitWidth, subCtx.height) + if err != nil { + return nil, err + } + + offs := offset + (uint64(i) * subCount) + + if i == 0 { + cs, err := diffNodeTrackedWithNodeSink(ctx, subCtx, curCtx, subn, cur, offs, b, sink, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + } else { + cs, err := removeAllTracked(ctx, subCtx, subn, offs, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + } + } + + return changes, nil + } + + // sanity check + if prevCtx.height != curCtx.height { + return nil, fmt.Errorf("comparing non-leaf nodes of unequal heights (%d, %d)", prevCtx.height, curCtx.height) + } + + if len(prev.links) != len(cur.links) { + return nil, fmt.Errorf("nodes have different numbers of links (prev=%d, cur=%d)", len(prev.links), len(cur.links)) + } + + if prev.links == nil || cur.links == nil { + return nil, fmt.Errorf("nodes have no links") + } + + subCount := prevCtx.nodesAtHeight() + for i := range prev.links { + // Neither previous or current links are in use + if prev.links[i] == nil && cur.links[i] == nil { + continue + } + + // Previous had link, current did not + if prev.links[i] != nil && cur.links[i] == nil { + if prev.links[i].cid == cid.Undef { + continue + } + + subCtx := &nodeContext{ + bs: prevCtx.bs, + bitWidth: prevCtx.bitWidth, + height: prevCtx.height - 1, + } + + subn, err := prev.links[i].load(ctx, subCtx.bs, subCtx.bitWidth, subCtx.height) + if err != nil { + return nil, err + } + + offs := offset + (uint64(i) * subCount) + cs, err := removeAllTracked(ctx, subCtx, subn, offs, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + + continue + } + + // Current has link, previous did not + if prev.links[i] == nil && cur.links[i] != nil { + if cur.links[i].cid == cid.Undef { + continue + } + + subCtx := &nodeContext{ + bs: curCtx.bs, + bitWidth: curCtx.bitWidth, + height: curCtx.height - 1, + } + + subn, err := cur.links[i].load(ctx, subCtx.bs, subCtx.bitWidth, subCtx.height) + if err != nil { + return nil, err + } + + offs := offset + (uint64(i) * subCount) + cs, err := addAllTrackWithNodeSink(ctx, subCtx, subn, offs, b, sink, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + + continue + } + + // Both previous and current have links to diff + if prev.links[i].cid == cur.links[i].cid { + continue + } + + prevSubCtx := &nodeContext{ + bs: prevCtx.bs, + bitWidth: prevCtx.bitWidth, + height: prevCtx.height - 1, + } + + prevSubn, err := prev.links[i].load(ctx, prevSubCtx.bs, prevSubCtx.bitWidth, prevSubCtx.height) + if err != nil { + return nil, err + } + + curSubCtx := &nodeContext{ + bs: curCtx.bs, + bitWidth: curCtx.bitWidth, + height: curCtx.height - 1, + } + + curSubn, err := cur.links[i].load(ctx, curSubCtx.bs, curSubCtx.bitWidth, curSubCtx.height) + if err != nil { + return nil, err + } + + offs := offset + (uint64(i) * subCount) + + cs, err := diffNodeTrackedWithNodeSink(ctx, prevSubCtx, curSubCtx, prevSubn, curSubn, offs, b, sink, trail) + if err != nil { + return nil, err + } + + changes = append(changes, cs...) + } + + return changes, nil +} + func addAll(ctx context.Context, nc *nodeContext, node *node, offset uint64) ([]*Change, error) { var changes []*Change err := node.forEachAt(ctx, nc.bs, nc.bitWidth, nc.height, 0, offset, func(index uint64, deferred *cbg.Deferred) error { @@ -321,6 +576,26 @@ func addAll(ctx context.Context, nc *nodeContext, node *node, offset uint64) ([] return changes, nil } +func addAllTrackWithNodeSink(ctx context.Context, nc *nodeContext, node *node, offset uint64, b *bytes.Buffer, sink cbg.CBORUnmarshaler, trail []int) ([]*Change, error) { + var changes []*Change + err := node.forEachAtTrackedWithNodeSink(ctx, nc.bs, trail, nc.bitWidth, nc.height, 0, offset, b, sink, func(index uint64, deferred *cbg.Deferred, selectorSuffix []int) error { + changes = append(changes, &Change{ + Type: Add, + Key: index, + Before: nil, + After: deferred, + SelectorSuffix: selectorSuffix, + }) + + return nil + }) + if err != nil { + return nil, err + } + + return changes, nil +} + func removeAll(ctx context.Context, nc *nodeContext, node *node, offset uint64) ([]*Change, error) { var changes []*Change @@ -341,6 +616,27 @@ func removeAll(ctx context.Context, nc *nodeContext, node *node, offset uint64) return changes, nil } +func removeAllTracked(ctx context.Context, nc *nodeContext, node *node, offset uint64, trail []int) ([]*Change, error) { + var changes []*Change + + err := node.forEachAtTracked(ctx, nc.bs, trail, nc.bitWidth, nc.height, 0, offset, func(index uint64, deferred *cbg.Deferred, selectorSuffix []int) error { + changes = append(changes, &Change{ + Type: Remove, + Key: index, + Before: deferred, + After: nil, + SelectorSuffix: selectorSuffix, + }) + + return nil + }) + if err != nil { + return nil, err + } + + return changes, nil +} + func diffLeaves(prev, cur *node, offset uint64) ([]*Change, error) { if len(prev.values) != len(cur.values) { return nil, fmt.Errorf("node leaves have different numbers of values (prev=%d, cur=%d)", len(prev.values), len(cur.values)) @@ -390,3 +686,77 @@ func diffLeaves(prev, cur *node, offset uint64) ([]*Change, error) { return changes, nil } + +func diffLeavesTrackedWithNodeSink(ctx context.Context, bitWidth uint, prev, cur *node, offset uint64, b *bytes.Buffer, sink cbg.CBORUnmarshaler, trail []int) ([]*Change, error) { + if len(prev.values) != len(cur.values) { + return nil, fmt.Errorf("node leaves have different numbers of values (prev=%d, cur=%d)", len(prev.values), len(cur.values)) + } + + if sink != nil { + if b == nil { + b = bytes.NewBuffer(nil) + } + b.Reset() + internalNode, err := cur.compact(ctx, bitWidth, 0) + if err != nil { + return nil, err + } + if err := internalNode.MarshalCBOR(b); err != nil { + return nil, err + } + if err := sink.UnmarshalCBOR(b); err != nil { + return nil, err + } + } + + var changes []*Change + l := len(trail) + for i, prevVal := range prev.values { + subTrail := make([]int, l, l+1) + copy(subTrail, trail) + subTrail = append(subTrail, i) + index := offset + uint64(i) + + curVal := cur.values[i] + if prevVal == nil && curVal == nil { + continue + } + + if prevVal == nil && curVal != nil { + changes = append(changes, &Change{ + Type: Add, + Key: index, + Before: nil, + After: curVal, + SelectorSuffix: subTrail, + }) + + continue + } + + if prevVal != nil && curVal == nil { + changes = append(changes, &Change{ + Type: Remove, + Key: index, + Before: prevVal, + After: nil, + SelectorSuffix: subTrail, + }) + + continue + } + + if !bytes.Equal(prevVal.Raw, curVal.Raw) { + changes = append(changes, &Change{ + Type: Modify, + Key: index, + Before: prevVal, + After: curVal, + SelectorSuffix: subTrail, + }) + } + + } + + return changes, nil +} diff --git a/node.go b/node.go index 8fe5b41..5d6e513 100644 --- a/node.go +++ b/node.go @@ -356,10 +356,15 @@ func (n *node) forEachAt(ctx context.Context, bs cbor.IpldStore, bitWidth uint, // the left-most leaf node. // This method also provides the trail of indices at each height/level in the way to the current node, which can be used to formulate a selector suffixes func (n *node) forEachAtTracked(ctx context.Context, bs cbor.IpldStore, trail []int, bitWidth uint, height int, start, offset uint64, cb func(uint64, *cbg.Deferred, []int) error) error { + l := len(trail) if height == 0 { // height=0 means we're at leaf nodes and get to use our callback for i, v := range n.values { if v != nil { + subTrail := make([]int, l, l+1) + copy(subTrail, trail) + subTrail = append(subTrail, i) + ix := offset + uint64(i) if ix < start { // if we're here, 'start' is probably somewhere in the @@ -369,7 +374,7 @@ func (n *node) forEachAtTracked(ctx context.Context, bs cbor.IpldStore, trail [] // use 'offset' to determine the actual index for this element, it // tells us how distant we are from the left-most leaf node - if err := cb(ix, v, append(trail, i)); err != nil { + if err := cb(ix, v, subTrail); err != nil { return err } } @@ -384,6 +389,10 @@ func (n *node) forEachAtTracked(ctx context.Context, bs cbor.IpldStore, trail [] continue } + subTrail := make([]int, l, l+1) + copy(subTrail, trail) + subTrail = append(subTrail, i) + // 'offs' tells us the index of the left-most element of the subtree defined // by 'sub' offs := offset + (uint64(i) * subCount) @@ -402,7 +411,7 @@ func (n *node) forEachAtTracked(ctx context.Context, bs cbor.IpldStore, trail [] // recurse into the child node, providing 'offs' to tell it where it's // located in the tree - if err := subn.forEachAtTracked(ctx, bs, append(trail, i), bitWidth, height-1, start, offs, cb); err != nil { + if err := subn.forEachAtTracked(ctx, bs, subTrail, bitWidth, height-1, start, offs, cb); err != nil { return err } } @@ -434,10 +443,15 @@ func (n *node) forEachAtTrackedWithNodeSink(ctx context.Context, bs cbor.IpldSto return err } } + l := len(trail) if height == 0 { // height=0 means we're at leaf nodes and get to use our callback for i, v := range n.values { if v != nil { + subTrail := make([]int, l, l+1) + copy(subTrail, trail) + subTrail = append(subTrail, i) + ix := offset + uint64(i) if ix < start { // if we're here, 'start' is probably somewhere in the @@ -447,7 +461,7 @@ func (n *node) forEachAtTrackedWithNodeSink(ctx context.Context, bs cbor.IpldSto // use 'offset' to determine the actual index for this element, it // tells us how distant we are from the left-most leaf node - if err := cb(ix, v, append(trail, i)); err != nil { + if err := cb(ix, v, subTrail); err != nil { return err } } @@ -462,6 +476,10 @@ func (n *node) forEachAtTrackedWithNodeSink(ctx context.Context, bs cbor.IpldSto continue } + subTrail := make([]int, l, l+1) + copy(subTrail, trail) + subTrail = append(subTrail, i) + // 'offs' tells us the index of the left-most element of the subtree defined // by 'sub' offs := offset + (uint64(i) * subCount) @@ -480,7 +498,7 @@ func (n *node) forEachAtTrackedWithNodeSink(ctx context.Context, bs cbor.IpldSto // recurse into the child node, providing 'offs' to tell it where it's // located in the tree - if err := subn.forEachAtTracked(ctx, bs, append(trail, i), bitWidth, height-1, start, offs, cb); err != nil { + if err := subn.forEachAtTracked(ctx, bs, subTrail, bitWidth, height-1, start, offs, cb); err != nil { return err } }