Skip to content

fix: duplicated swept vtxos in sweep tx event - #1152

Open
Dunsin-cyber wants to merge 3 commits into
arkade-os:masterfrom
Dunsin-cyber:fix/sweep-event-duplicated-swept-vtxos
Open

fix: duplicated swept vtxos in sweep tx event#1152
Dunsin-cyber wants to merge 3 commits into
arkade-os:masterfrom
Dunsin-cyber:fix/sweep-event-duplicated-swept-vtxos

Conversation

@Dunsin-cyber

@Dunsin-cyber Dunsin-cyber commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

fixes #1138

the sweep event listed the same vtxo twice because the queries used to collect preconfirmed vtxos also returned the leaves.

Fix: make the queries return what their names say, instead of filtering at call sites:

  • GetSweepablePreconfirmedVtxosByCommitmentTxid (renamed): adds preconfirmed = true, leaves excluded
  • GetDescendantVtxos (new): same walk as GetAllChildrenVtxos but depth > 0, seed excluded.
  • GetAllChildrenVtxos untouched (checkpoint sweep needs the seed)
    This covers both event producers (sweeper + admin sweep) with no filter code to forget.

Also needed: leaf collection now takes every vtxo output of a leaf tx (not just vout 0), and FullySwept counts distinct leaf txids, otherwise multi-receiver leaves would be dropped from the event / break the accounting.

Summary by CodeRabbit

  • Bug Fixes
    • Improved batch sweep handling to include all VTXO outpoints from each leaf transaction (skipping anchor/extension outputs).
    • Refined descendant VTXO expansion used during sweeps to improve coverage and avoid repeats.
    • Updated full-round sweep completion detection to rely on distinct swept transaction IDs.
    • Tightened commitment-scoped sweep selection to eligible preconfirmed VTXOs.
  • Tests
    • Enhanced sweep event validation to ensure no duplicate swept VTXO outpoints are reported.

@coderabbitai

coderabbitai Bot commented Jul 13, 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 081e4b07-6bed-4c33-b37c-ec55d8e9020e

📥 Commits

Reviewing files that changed from the base of the PR and between 451391a and f8501c1.

📒 Files selected for processing (2)
  • internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go
  • internal/infrastructure/db/sqlite/sqlc/query.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/infrastructure/db/sqlite/sqlc/query.sql
  • internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go

Walkthrough

Batch sweep processing now collects multiple leaf VTXO outpoints, traverses descendant lineages, filters commitment results to preconfirmed VTXOs, deduplicates swept outpoints, and determines fully swept rounds using distinct leaf transaction IDs.

Changes

VTXO sweep lineage

Layer / File(s) Summary
Domain sweep semantics
internal/core/domain/vtxo_repo.go, internal/core/domain/round.go, internal/core/domain/round_test.go
The repository contract adds preconfirmed commitment lookup and descendant traversal; Round.Sweep counts distinct swept leaf transaction IDs.
Persistence lineage queries
internal/infrastructure/db/badger/vtxo_repo.go, internal/infrastructure/db/postgres/..., internal/infrastructure/db/sqlite/...
Badger, PostgreSQL, and SQLite implement descendants-only traversal and preconfirmed commitment filtering.
Batch sweep integration
internal/core/application/admin.go, internal/core/application/sweeper.go, internal/core/application/utils.go
Sweep paths collect all eligible leaf outputs and centralize descendant or commitment-based VTXO collection with deduplication.
Sweep behavior validation
internal/core/application/*_test.go, internal/infrastructure/db/service_test.go, internal/test/e2e/e2e_test.go
Tests validate updated repository mocks, preconfirmed filtering, descendant results, distinct leaf tracking, and duplicate-free sweep events.

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

Sequence Diagram(s)

sequenceDiagram
  participant BatchSweep
  participant leafVtxoOutpoints
  participant VtxoRepository
  participant SQLDatabase
  BatchSweep->>leafVtxoOutpoints: collect eligible leaf outpoints
  BatchSweep->>VtxoRepository: request preconfirmed VTXOs or descendants
  VtxoRepository->>SQLDatabase: execute lineage or commitment query
  SQLDatabase-->>VtxoRepository: return VTXO outpoints
  VtxoRepository-->>BatchSweep: return deduplicated candidates
Loading

Possibly related issues

  • Scale the DAG #833 — Both changes involve VTXO DAG traversal and sweep-related lineage handling, though this PR does not address marker-based scaling.

Possibly related PRs

  • arkade-os/arkd#816 — Both changes modify batch sweep event handling, VTXO sweepability lookups, and Round.Sweep leaf tracking.

Suggested reviewers: altafan, arkanaai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing duplicated swept VTXOs in sweep transaction events.
Linked Issues check ✅ Passed The changes deduplicate swept VTXOs and adjust sweep event/query generation to avoid duplicate entries, matching #1138.
Out of Scope Changes check ✅ Passed The repository, query, and test updates all support the same sweep-event deduplication fix rather than unrelated functionality.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Dunsin-cyber
Dunsin-cyber force-pushed the fix/sweep-event-duplicated-swept-vtxos branch from 30f8eec to f55d49a Compare July 19, 2026 19:49
@Dunsin-cyber
Dunsin-cyber force-pushed the fix/sweep-event-duplicated-swept-vtxos branch from f55d49a to 59375aa Compare July 19, 2026 20:01
@Dunsin-cyber
Dunsin-cyber marked this pull request as ready for review July 19, 2026 20:02

@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 (2)
internal/core/application/sweeper_test.go (1)

479-483: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wire GetSweepablePreconfirmedVtxosByCommitmentTxid through m.Called It currently always returns nil, nil, so tests can’t inject preconfirmed vtxos for the commitmentRootSwept == true path.

🤖 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 `@internal/core/application/sweeper_test.go` around lines 479 - 483, Update
mockVtxoRepository.GetSweepablePreconfirmedVtxosByCommitmentTxid to delegate to
m.Called with the provided context and commitmentTxid, then return the
configured []domain.Outpoint and error values so tests can inject preconfirmed
vtxos for the commitmentRootSwept path.
internal/core/application/utils.go (1)

94-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the anchor/extension filter into a shared helper.

This exact check (bytes.Equal(out.PkScript, txutils.ANCHOR_PKSCRIPT) || extension.IsExtension(out.PkScript)) is now duplicated a third time in this file (also in decodeTx and getNewVtxosFromRound). Consolidating into one predicate reduces the risk of the three sites diverging if the filter criteria change.

♻️ Proposed shared helper
+func isSweepableTxOut(pkScript []byte) bool {
+	return !bytes.Equal(pkScript, txutils.ANCHOR_PKSCRIPT) && !extension.IsExtension(pkScript)
+}
+
 func leafVtxoOutpoints(leaf *psbt.Packet) []domain.Outpoint {
 	txid := leaf.UnsignedTx.TxID()
 	outpoints := make([]domain.Outpoint, 0, len(leaf.UnsignedTx.TxOut))
 	for i, out := range leaf.UnsignedTx.TxOut {
-		if bytes.Equal(out.PkScript, txutils.ANCHOR_PKSCRIPT) ||
-			extension.IsExtension(out.PkScript) {
+		if !isSweepableTxOut(out.PkScript) {
 			continue
 		}
 		outpoints = append(outpoints, domain.Outpoint{Txid: txid, VOut: uint32(i)})
 	}
 	return outpoints
 }

(similarly replace the checks in decodeTx and getNewVtxosFromRound)

🤖 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 `@internal/core/application/utils.go` around lines 94 - 107, Extract the
duplicated anchor/extension output check into a shared predicate helper in this
file, then update leafVtxoOutpoints, decodeTx, and getNewVtxosFromRound to call
it. Preserve the existing behavior by returning true for anchor scripts or
extension outputs and skipping those outputs at each call site.
🤖 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 `@internal/core/application/sweeper.go`:
- Around line 689-714: The preconfirmed-vTXO collection logic is duplicated and
has diverging error messages. In internal/core/application/sweeper.go lines
689-714, extract the commitmentRootSwept lookup and per-leaf GetDescendantVtxos
deduplication into a shared helper, correcting the stale error message; in
internal/core/application/admin.go lines 868-895, replace the equivalent block
with a call to that helper so both paths use the same implementation.
- Around line 689-714: Correct the final `sweepErr` log in the sweep flow so it
identifies failure to fetch sweepable preconfirmed vtxos by commitment
transaction ID, rather than descendant lookup. Keep the per-iteration
`descendantsErr` logging in the `GetDescendantVtxos` branch unchanged, and
update only the message associated with `sweepErr` from
`GetSweepablePreconfirmedVtxosByCommitmentTxid`.

In `@internal/infrastructure/db/sqlite/sqlc/query.sql`:
- Around line 371-405: Update the visited-path cycle guards in
SelectDescendantVtxoOutpointsByArkTxid and
SelectVtxosOutpointsByArkTxidRecursive to use delimiter-bounded matching,
ensuring complete outpoint entries are compared rather than substrings. Preserve
the comma-separated path representation while adding delimiters at the
boundaries so values such as txid:1 cannot match txid:12.

---

Nitpick comments:
In `@internal/core/application/sweeper_test.go`:
- Around line 479-483: Update
mockVtxoRepository.GetSweepablePreconfirmedVtxosByCommitmentTxid to delegate to
m.Called with the provided context and commitmentTxid, then return the
configured []domain.Outpoint and error values so tests can inject preconfirmed
vtxos for the commitmentRootSwept path.

In `@internal/core/application/utils.go`:
- Around line 94-107: Extract the duplicated anchor/extension output check into
a shared predicate helper in this file, then update leafVtxoOutpoints, decodeTx,
and getNewVtxosFromRound to call it. Preserve the existing behavior by returning
true for anchor scripts or extension outputs and skipping those outputs at each
call site.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec779e20-881b-42fb-a9a1-2bd70fc83527

📥 Commits

Reviewing files that changed from the base of the PR and between eb75b6b and 59375aa.

📒 Files selected for processing (17)
  • internal/core/application/admin.go
  • internal/core/application/indexer_test.go
  • internal/core/application/sweeper.go
  • internal/core/application/sweeper_test.go
  • internal/core/application/utils.go
  • internal/core/domain/round.go
  • internal/core/domain/round_test.go
  • internal/core/domain/vtxo_repo.go
  • internal/infrastructure/db/badger/vtxo_repo.go
  • internal/infrastructure/db/postgres/sqlc/queries/query.sql.go
  • internal/infrastructure/db/postgres/sqlc/query.sql
  • internal/infrastructure/db/postgres/vtxo_repo.go
  • internal/infrastructure/db/service_test.go
  • internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go
  • internal/infrastructure/db/sqlite/sqlc/query.sql
  • internal/infrastructure/db/sqlite/vtxo_repo.go
  • internal/test/e2e/e2e_test.go

Comment thread internal/core/application/sweeper.go Outdated
Comment thread internal/infrastructure/db/sqlite/sqlc/query.sql

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

🧹 Nitpick comments (2)
internal/core/application/utils.go (2)

131-142: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use domain.Outpoint directly as the map key to avoid string allocations.

In Go, structs with comparable fields (like string and uint32) are fully comparable. Using domain.Outpoint directly as the map key eliminates the need to call .String() on every iteration, avoiding unnecessary heap allocations.

♻️ Proposed refactor
-	seen := make(map[string]struct{})
+	seen := make(map[domain.Outpoint]struct{})
 	for _, leafVtxo := range leafVtxos {
 		descendants, err := vtxoRepo.GetDescendantVtxos(ctx, leafVtxo)
 		if err != nil {
 			log.WithError(err).Error("error while getting descendant vtxos")
 			continue
 		}
 		for _, descendant := range descendants {
-			if _, ok := seen[descendant.String()]; !ok {
+			if _, ok := seen[descendant]; !ok {
 				preconfirmedVtxos = append(preconfirmedVtxos, descendant)
-				seen[descendant.String()] = struct{}{}
+				seen[descendant] = struct{}{}
 			}
 		}
🤖 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 `@internal/core/application/utils.go` around lines 131 - 142, Update the
deduplication map in the descendant-processing loop to use
map[domain.Outpoint]struct{} keyed directly by each descendant, replacing
String() calls for lookup and insertion while preserving the existing
preconfirmedVtxos append behavior.

119-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid variable shadowing and return the initialized empty slice on error.

Using := instead of var err error is cleaner, avoids overwriting the initialized preconfirmedVtxos slice with nil on failure, and explicitly returns the empty slice correctly on the error path.

♻️ Proposed refactor
 	if commitmentRootSwept {
-		var err error
-		preconfirmedVtxos, err = vtxoRepo.GetSweepablePreconfirmedVtxosByCommitmentTxid(
+		res, err := vtxoRepo.GetSweepablePreconfirmedVtxosByCommitmentTxid(
 			ctx, commitmentTxid,
 		)
 		if err != nil {
 			log.WithError(err).
 				Error("error while getting sweepable preconfirmed vtxos by commitment txid")
+			return preconfirmedVtxos
 		}
-		return preconfirmedVtxos
+		return res
 	}
🤖 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 `@internal/core/application/utils.go` around lines 119 - 128, Update the
commitmentRootSwept branch to fetch results using a scoped short declaration for
the repository call, avoiding the separate err declaration and shadowing the
initialized preconfirmedVtxos slice. On error, log the failure and return the
existing initialized empty slice; on success, return the fetched results.
🤖 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.

Nitpick comments:
In `@internal/core/application/utils.go`:
- Around line 131-142: Update the deduplication map in the descendant-processing
loop to use map[domain.Outpoint]struct{} keyed directly by each descendant,
replacing String() calls for lookup and insertion while preserving the existing
preconfirmedVtxos append behavior.
- Around line 119-128: Update the commitmentRootSwept branch to fetch results
using a scoped short declaration for the repository call, avoiding the separate
err declaration and shadowing the initialized preconfirmedVtxos slice. On error,
log the failure and return the existing initialized empty slice; on success,
return the fetched results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 862266ce-d2bc-48f7-9181-87ce1b2ce8fd

📥 Commits

Reviewing files that changed from the base of the PR and between 59375aa and 451391a.

📒 Files selected for processing (3)
  • internal/core/application/admin.go
  • internal/core/application/sweeper.go
  • internal/core/application/utils.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/core/application/sweeper.go

@Dunsin-cyber

Dunsin-cyber commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Hi @altafan, quick context on the approach.

The simple fix for #1138 would be to just filter the leaf vtxos out of the preconfirmed list in sweeper.go and admin.go. But that treats the symptom. The real issue is that the repository functions weren't narrow enough. GetSweepableVtxosByCommitmentTxid returned all the batch vtxos, leaves included, even though both of its callers only wanted the preconfirmed ones.

So instead of filtering after the fact, I made the queries honest: a renamed GetSweepablePreconfirmedVtxosByCommitmentTxid that returns only preconfirmed vtxos, and a GetDescendantVtxos that excludes the seed. With the sources correct, the two lists can't overlap and no call-site filtering is needed.

Happy to go the simpler route if this looks invasive.

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PROTOCOL-CRITICAL: human review required.

Reviewed at head f8501c1. This PR touches the batch sweep event flow, VTXO descendant walks, and round-fully-swept accounting. Please have a human protocol reviewer sign off before merge.

Correctness of the fix (looks right)

  • leafVtxoOutpoints in internal/core/application/utils.go:94-105 correctly enumerates every non-anchor / non-extension output of a leaf tx, matching the receiver semantics used elsewhere (getNewVtxosFromRound, decodeTx). This is what fixes the duplication in the sweep event.
  • collectPreconfirmedVtxos (utils.go:110-140) consolidates the two identical branches in admin.saveBatchSweptEvents and sweeper.createBatchSweepTask. Behavior matches the previous inline code, with the dedup map (seen) preserved.
  • Renaming GetSweepableVtxosByCommitmentTxidGetSweepablePreconfirmedVtxosByCommitmentTxid and adding preconfirmed = true is right: the caller already passes leaves separately, so returning them again from the repo was the source of the duplicates. The three implementations (postgres/sqlite/badger) all apply the filter (internal/infrastructure/db/{postgres,sqlite}/sqlc/query.sql, internal/infrastructure/db/badger/vtxo_repo.go:770-773).
  • GetDescendantVtxos (new) correctly excludes the seed: postgres uses WHERE depth > 0, sqlite uses WHERE depth > 0, badger seeds the visited map with the seed outpoint but never appends it to outpoints. Good.
  • Round.Sweep (round.go:214-224) switching from summing len(LeafVtxos) to counting distinct leaf txids is the right accounting when a leaf tx carries multiple receivers.

Nice side fix

The SQLite SelectVtxosOutpointsByArkTxidRecursive cycle guard is corrected from unbounded LIKE '%X%' to delimiter-bounded LIKE '%,X,%' (query.sql:355-360). The old form could false-match when one outpoint string was a substring of another (e.g. txid:1 inside txid:12), which would prematurely prune a descendant. The new SelectDescendantVtxoOutpointsByArkTxid uses the same fix. Worth calling out because it silently changes results for the existing method too.

Issues to address / flag

  1. Latent multi-receiver bug in updateVtxoExpirationTime (sweeper.go:786) — this callsite still uses extractVtxoOutpoint, which returns only the first non-anchor output. With the same multi-receiver leaves this PR is fixing elsewhere, only the vout-0 vtxo of each leaf gets its expiration refreshed in UpdateVtxosExpiration; the other receivers keep the initial expireAt from round creation. Not introduced by this PR but the exact same underlying bug — please fix in the same PR or file a follow-up.

  2. extractVtxoOutpoint also doesn't skip extension outputsleafVtxoOutpoints skips both anchor and extension, but the still-live extractVtxoOutpoint (sweeper.go:889) only skips anchor. If a leaf ever has layout [anchor, extension, vtxo], extractVtxoOutpoint returns the extension outpoint. Ties into (1); same remediation.

  3. fullySwept still depends on r.Changes which is empty when the round is loaded from the projection. sweeper.createBatchSweepTask (sweeper.go:683) and admin.saveBatchSweptEvents (admin.go:514, 776) all load the round via GetRoundWithCommitmentTxid, which populates the projection fields but never rehydrates r.Changes (see rowsToRounds in sqlite/round_repo.go:645, postgres/round_repo.go:518). So the new for _, event := range r.Changes { … BatchSwept … } loop always sees zero past events. This means for a round that gets swept across multiple subtree/expiry tasks, len(sweptLeafTxids) == leavesCount will never hold and r.Swept never becomes true via this path. The old code had the same defect (countSweptLeafVtxos(r.Changes) was also always 0 in this callflow), so this is a pre-existing bug rather than a regression — but the PR's stated goal is to make FullySwept "count distinct leaf txids, otherwise multi-receiver leaves would break the accounting," and it still won't for the multi-sweep case. Either fix (load past BatchSwept events / query the DB for already-swept leaf txids of the round) or add a comment acknowledging the limitation. This matters for the r.Swept flag written in on(BatchSwept) at round.go:308 which is used by GetSweepableRounds.

  4. fullySwept also fails if any leaf tx has no vtxo outputs (only anchor/extension). leafVtxoOutpoints returns [], that leaf's txid never joins sweptLeafTxids, and the round can never reach fullySwept. Not a common case, but worth an assertion or a comment. If it is impossible by construction, no action.

  5. Badger GetSweepablePreconfirmedVtxosByCommitmentTxid traversal via queue = append(queue, vtxo.ArkTxid) is suspect (badger/vtxo_repo.go:775-778). The subsequent iteration queries CommitmentTxids Contains(currentTxid) where currentTxid is now an ark txid, not a commitment txid. Unless preconfirmed descendants carry the parent's ark tx id in their CommitmentTxids field (which I don't see in decodeTx), the second+ iterations return nothing. Behavior appears to happen to match the flat postgres query only because both end up returning the direct vtxos whose commitment_txid matches. Pre-existing shape, not this PR's fault, but worth a look — if the walk really is a no-op past depth 0 the code should be simplified to a single query.

  6. collectPreconfirmedVtxos swallows repo errors and returns a partial result (utils.go:127, 137). On a DB error mid-walk the sweep event will list fewer preconfirmed vtxos than were actually swept, then be persisted as the canonical event. Preserving the old logging behavior is fine, but consider propagating the error or aborting the sweep event save so consumers don't get a silently-truncated SweptVtxos. This is the event that downstream indexers/wallets react to.

Cross-repo

VtxoRepository is internal to arkd; the rename is not visible to SDKs. Grepped ts-sdk / go-sdk / rust-sdk / dotnet-sdk — no references. The wire SweepTx event schema is unchanged; only its contents get de-duplicated, which is what SDKs already assumed.

Test coverage

  • e2e assertion for uniqueness added (internal/test/e2e/e2e_test.go:3425-3432). Good, this is exactly the invariant being fixed.
  • service_test.go covers GetDescendantVtxos for a happy-path chain and for the end-of-chain (empty) case. Missing: a fan-out case (one seed with two ark descendants at the same depth) and the cycle guard (which is the fragile bit of the sqlite CTE).
  • round_test.go still only exercises single-shot Sweep; the multi-sweep/r.Changes-empty case discussed in (3) is not covered.
  • No unit test for leafVtxoOutpoints itself despite it now driving the sweep event content.

Please treat (1), (3), and (6) as blocking for the human protocol reviewer.

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 3+ days without review. @Dunsin-cyber is anyone looking at this?

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana review — fix: duplicated swept vtxos in sweep tx event

Correct fix for two related bugs.

Bug 1 — single vtxo per leaf: extractVtxoOutpoint returned only the first non-anchor output. Leaf txs that pay multiple receivers (shared leaf) would only sweep one vtxo. The new leafVtxoOutpoints collects every vtxo outpoint from the leaf, skipping anchors and extension outputs. This correctly handles multi-receiver leaves.

Bug 2 — duplicated preconfirmed vtxos: The non-root sweep path used GetAllChildrenVtxos per leaf vtxo and de-duplicated with a seen map. If two leaf vtxos shared a descendant (possible with chains), that descendant appeared multiple times in preconfirmedVtxos, creating duplicate sweep events. The new collectPreconfirmedVtxos helper uses GetDescendantVtxos which deduplicates at the query level.

Code quality: Extracting collectPreconfirmedVtxos removes duplicated logic between admin.go and sweeper.go, which previously had to be kept in sync. Good refactor.

Tests: The interface additions (GetDescendantVtxos, renamed GetSweepablePreconfirmedVtxosByCommitmentTxid) are correctly wired into the mock. No new unit tests for the multi-receiver case — worth adding one to pin the regression if time allows, but not blocking.

Ready to merge.

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 30+ days without review. @Dunsin-cyber is anyone looking at this?

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arkana review — fix: duplicated swept vtxos in sweep tx event

What this does: Fixes a bug where extractVtxoOutpoint was called per leaf tx and returned only the first non-anchor output VTXO. Leaf txs can pay multiple receivers, so subsequent VTXOs in the same leaf were silently dropped. This caused the sweep event to report incomplete vtxo sets, meaning some swept VTXOs might not be marked appropriately.

The fix:

  • leafVtxoOutpoints replaces extractVtxoOutpoint and collects all VTXO outpoints from a leaf tx. This is the right fix.
  • collectPreconfirmedVtxos is extracted as a shared helper used by both saveBatchSweptEvents (admin) and the sweeper, eliminating duplicate logic. Good refactor.
  • The interface rename GetSweepableVtxosByCommitmentTxidGetSweepablePreconfirmedVtxosByCommitmentTxid is a clarity improvement.
  • New GetDescendantVtxos on the vtxo repo port is added to mock/test stubs — confirm the production implementation handles depth correctly.

Notes:

  • No explicit unit test for leafVtxoOutpoints itself, but the existing sweep tests cover the path. Worth a targeted test if multi-receiver leaf txs are common.
  • The change is on the event-reporting side; verify it doesn't affect the actual sweep transaction construction (which appeared to already handle multiple outputs).

Verdict: Straightforward correctness fix. Looks ready to merge.

@arkana-ai-bot arkana-ai-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: fix duplicated swept VTXOs in sweep tx event

Verdict: looks ready to merge.

What the PR does

extractVtxoOutpoint returned only the first VTXO from a leaf tx. Since leaf txs can pay multiple receivers, sweep events were missing VTXOs at indices > 0. The fix replaces it with leafVtxoOutpoints, which iterates all outputs and collects every VTXO outpoint (skipping anchor and extension outputs). Applied consistently in both sweeper.go and admin.go.

collectPreconfirmedVtxos is a clean extraction of the duplicated pre-confirmed-VTXO collection logic. The port rename GetSweepableVtxosByCommitmentTxidGetSweepablePreconfirmedVtxosByCommitmentTxid is more accurate. The new GetDescendantVtxos port method is reflected in mock stubs.

Minor questions

  • leafVtxoOutpoints silently skips outputs it can't parse as a VTXO script. Does it log a warning when a non-anchor, non-extension output fails to parse? Silent skipping is safe but could hide a future parsing bug.
  • The GetDescendantVtxos mock returns nil — is this method used in the sweep path covered by existing tests, or is it a forward stub for a follow-up?

Core fix is correct and the refactor is a net improvement. Ready to merge.

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 42+ days without a human review. @Dunsin-cyber is anyone looking at this? Arkana reviewed it (looks ready to merge).

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 5+ days without a review. @Dunsin-cyber is anyone looking at this? (fix duplicated swept vtxos in sweep tx event; 5 days without review.)

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 48 days without a review. @Dunsin-cyber is there anything blocking this from getting a look?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 51+ days without review. @Dunsin-cyber is anyone looking at this? (fix: duplicated swept vtxos in sweep tx event)

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 52+ days without a review decision. @Dunsin-cyber is anyone looking at this?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 52+ days without a review. @Dunsin-cyber — is anyone looking at this?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open for 54 days without review. @Dunsin-cyber is anyone looking at this?

@arkana-ai-bot

Copy link
Copy Markdown

This PR has been open 55 days without review. @Dunsin-cyber is anyone looking at this?

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.

Bug: SweepTx event message contains duplicated swept vtxo

2 participants