Skip to content

feat(ark-client): narrow settle() to expired/recoverable VTXOs#227

Open
bonomat wants to merge 3 commits into
masterfrom
feat/smart-settle
Open

feat(ark-client): narrow settle() to expired/recoverable VTXOs#227
bonomat wants to merge 3 commits into
masterfrom
feat/smart-settle

Conversation

@bonomat
Copy link
Copy Markdown
Collaborator

@bonomat bonomat commented May 21, 2026

Rename the existing full-renewal settle to settle_all, and introduce a new settle that only renews VTXOs in the server's recoverable bucket plus any confirmed/pre-confirmed VTXOs the client sees as expired. This keeps periodic renewals cheap by leaving healthy VTXOs and boarding outputs untouched.

Summary by CodeRabbit

  • New Features

    • Added a full-batch settlement option to process all pending items in one operation.
  • Refactoring

    • Introduced a selective settlement flow that targets only expired or recoverable items, leaving healthy entries untouched.
    • Selective settlement now reports no action when nothing is eligible.
  • Tests

    • Updated end-to-end tests to exercise the full-batch settlement behavior.

Review Change Stack

Rename the existing full-renewal settle to settle_all, and introduce a
new settle that only renews VTXOs in the server's recoverable bucket
plus any confirmed/pre-confirmed VTXOs the client sees as expired. This
keeps periodic renewals cheap by leaving healthy VTXOs and boarding
outputs untouched.
@bonomat bonomat requested a review from luckysori May 21, 2026 01:24
@bonomat bonomat self-assigned this May 21, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Warning

Rate limit exceeded

@bonomat has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 17 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7da9aade-69aa-432c-91bf-672bae8bb1b0

📥 Commits

Reviewing files that changed from the base of the PR and between 30ebbcb and b7043e8.

📒 Files selected for processing (6)
  • ark-client/src/batch.rs
  • e2e-tests/tests/e2e_arkade_script.rs
  • e2e-tests/tests/e2e_assets.rs
  • e2e-tests/tests/e2e_concurrent_settlement.rs
  • e2e-tests/tests/e2e_sub_dust.rs
  • e2e-tests/tests/e2e_two_party.rs

Walkthrough

The batch settlement API is refactored to provide two methods with distinct scopes. The original settle method, which settled all prior VTXOs and boarding outputs, is renamed and moved to settle_all. A new narrower settle method enumerates VTXOs, filters to only expired or recoverable ones, and settles only those outpoints via settle_vtxos, returning Ok(None) when none exist.

Changes

Settlement API refactoring

Layer / File(s) Summary
Settlement method split: settle_all and narrowed settle
ark-client/src/batch.rs
Adds pub async fn settle_all<R>(&self, rng: &mut R) with the prior "settle everything" behavior. Replaces settle implementation to collect vtxo_list.recoverable() outpoints plus boarding outpoints from commitment inputs, return Ok(None) if both are empty, and otherwise call settle_vtxos with the selected outpoints.
E2E tests switched to settle_all
e2e-tests/tests/e2e_arkade_script.rs, e2e-tests/tests/e2e_concurrent_settlement.rs, e2e-tests/tests/e2e_sub_dust.rs, e2e-tests/tests/e2e_two_party.rs
Replaces calls to settle(&mut rng) with settle_all(&mut rng) across several end-to-end tests (Alice, Bob, Claire tasks and others) to align test flows with the new full-settlement API.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • arkade-os/rust-sdk#155: Changes classification of expired VTXOs to be recoverable, affecting which VTXOs settle includes.
  • arkade-os/rust-sdk#134: Introduced recoverability semantics (VirtualTxOutPoint::is_recoverable) that settle now depends on.
  • arkade-os/rust-sdk#139: Related selective-settlement primitives and settle_vtxos usage that the new settle delegates to.

Suggested reviewers

  • luckysori
🚥 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 and concisely summarizes the main change: narrowing the settle() function to only handle expired/recoverable VTXOs rather than all VTXOs, which is the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/smart-settle

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 and usage tips.

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

🚨 Protocol-Critical Review — REQUEST CHANGES

This PR touches settlement logic (VTXO handling, round lifecycle). Requires human sign-off even if all issues are resolved.


BLOCKER: Silent semantic break for all existing settle() callers

Severity: Critical

The old settle() settled all VTXOs + boarding outputs. This PR renames it to settle_all() and introduces a new settle() that only settles recoverable() VTXOs (expired, swept, or sub-dust — see ark-core/src/server.rs:405).

The problem: every existing caller of .settle() silently gets different behavior without a compile error. The method signature is identical, but the semantics change from "settle everything" to "settle only recoverable VTXOs."

Affected callers within this repo (all unchanged by this PR):

  • ark-client-sample/src/main.rs:611 — CLI settle command now silently skips boarding outputs
  • e2e-tests/tests/e2e_arkade_script.rs:77,194
  • e2e-tests/tests/e2e_two_party.rs:61,113,133
  • e2e-tests/tests/e2e_concurrent_settlement.rs:67,74,80,141,148,154
  • e2e-tests/tests/e2e_delegate.rs:41
  • e2e-tests/tests/e2e_multisig_delegate.rs:52
  • e2e-tests/tests/e2e_sub_dust.rs:34,64,86
  • e2e-tests/tests/e2e_continue_pending_tx.rs:38
  • e2e-tests/tests/e2e_continue_pending_tx_multi_input.rs:49
  • e2e-tests/tests/e2e_key_discovery.rs:56
  • e2e-tests/tests/e2e_send_onchain_vtxo_and_boarding_output.rs:59
  • e2e-tests/tests/e2e_assets.rs:31,110
  • e2e-tests/tests/boltz_submarine.rs:46
  • e2e-tests/tests/fulmine_delegator_smoke.rs:52

Most of these tests fund a boarding address then call settle() to bring funds on-chain into the Ark. After this PR, settle() will return Ok(None) (no recoverable VTXOs exist at that point), and subsequent balance assertions will fail or time out.

Fix: Either:

  1. Update all existing callers to settle_all(), OR
  2. Use a different name for the new narrow method (e.g., settle_expired(), settle_recoverable()) and keep settle() with its current semantics

Option 2 is strongly preferred — it avoids a semver-breaking rename and the new name is self-documenting.


Issue: New settle() discards expired confirmed/pre-confirmed VTXOs (doc mismatch)

Severity: Medium
File: ark-client/src/batch.rs:119-120 (new code)

The PR description says:

"a new settle that only renews VTXOs in the server's recoverable bucket plus any confirmed/pre-confirmed VTXOs the client sees as expired"

But the implementation only collects from vtxo_list.recoverable():

let outpoints: Vec<OutPoint> = vtxo_list.recoverable().map(|v| v.outpoint).collect();

The recoverable() bucket in VtxoList::new() (ark-core/src/vtxo_list.rs:31) is populated by is_recoverable() which checks is_expired() || is_swept || amount < dust. However, the classification in VtxoList::new() is order-dependent: a VTXO that is expired but NOT spent/swept/sub-dust would hit the is_recoverable() check first and land in recoverable. So expired VTXOs should be covered... but the doc claim about "confirmed/pre-confirmed VTXOs the client sees as expired" suggests there might be an edge case where client and server disagree on expiry (as noted in server.rs:413-414), and such VTXOs would remain in confirmed/pre_confirmed instead of recoverable.

If client-server clock skew can cause VTXOs to be considered expired by the client but not the server, then recoverable() won't include them. Clarify whether this is intentional.


Issue: No tests for the new settle() method

Severity: Medium

No unit or e2e tests verify:

  • settle() correctly filters to only recoverable VTXOs
  • settle() returns Ok(None) when no recoverable VTXOs exist
  • settle() successfully renews expired VTXOs
  • settle() leaves healthy VTXOs untouched

For a protocol-critical settlement path, test coverage is essential.


Note: Cross-repo impact is contained

The other SDKs (ts-sdk, go-sdk, dotnet-sdk) have their own settle() implementations and are not affected by this Rust-side rename. No cross-repo breakage.


Summary

The core idea is sound — narrowing periodic settlement to only expired/recoverable VTXOs saves batch fees and is a good optimization. But the execution has a critical flaw: reusing the settle() name silently changes behavior for all callers. Use a distinct name like settle_recoverable() and add tests.

⚠️ PROTOCOL-CRITICAL: This PR modifies VTXO settlement logic. Human review required before merge regardless of code changes.

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

🚨 Protocol-Critical Review — REQUEST CHANGES

This PR touches settlement logic (VTXO handling, round lifecycle). Requires human sign-off even if all issues are resolved.


BLOCKER: Silent semantic break for all existing settle() callers

Severity: Critical

The old settle() settled all VTXOs + boarding outputs. This PR renames it to settle_all() and introduces a new settle() that only settles recoverable() VTXOs (expired, swept, or sub-dust — see ark-core/src/server.rs:405).

The problem: every existing caller of .settle() silently gets different behavior without a compile error. The method signature is identical, but the semantics change from "settle everything" to "settle only recoverable VTXOs."

Affected callers within this repo (all unchanged by this PR):

  • `ark-client-sample/src/main.rs:611` — CLI `settle` command now silently skips boarding outputs
  • `e2e-tests/tests/e2e_arkade_script.rs:77,194`
  • `e2e-tests/tests/e2e_two_party.rs:61,113,133`
  • `e2e-tests/tests/e2e_concurrent_settlement.rs:67,74,80,141,148,154`
  • `e2e-tests/tests/e2e_delegate.rs:41`
  • `e2e-tests/tests/e2e_multisig_delegate.rs:52`
  • `e2e-tests/tests/e2e_sub_dust.rs:34,64,86`
  • `e2e-tests/tests/e2e_continue_pending_tx.rs:38`
  • `e2e-tests/tests/e2e_continue_pending_tx_multi_input.rs:49`
  • `e2e-tests/tests/e2e_key_discovery.rs:56`
  • `e2e-tests/tests/e2e_send_onchain_vtxo_and_boarding_output.rs:59`
  • `e2e-tests/tests/e2e_assets.rs:31,110`
  • `e2e-tests/tests/boltz_submarine.rs:46`
  • `e2e-tests/tests/fulmine_delegator_smoke.rs:52`

Most of these tests fund a boarding address then call `settle()` to bring funds on-chain into the Ark. After this PR, `settle()` will return `Ok(None)` (no recoverable VTXOs exist at that point), and subsequent balance assertions will fail or time out.

Fix: Either:

  1. Update all existing callers to `settle_all()`, OR
  2. Use a different name for the new narrow method (e.g., `settle_expired()`, `settle_recoverable()`) and keep `settle()` with its current semantics

Option 2 is strongly preferred — it avoids a semver-breaking rename and the new name is self-documenting.


Issue: New settle() discards expired confirmed/pre-confirmed VTXOs (doc mismatch)

Severity: Medium

The PR description says:

"a new settle that only renews VTXOs in the server's recoverable bucket plus any confirmed/pre-confirmed VTXOs the client sees as expired"

But the implementation only collects from `vtxo_list.recoverable()`:
```rust
let outpoints: Vec = vtxo_list.recoverable().map(|v| v.outpoint).collect();
```

The `recoverable()` bucket in `VtxoList::new()` (`ark-core/src/vtxo_list.rs:31`) is populated by `is_recoverable()` which checks `is_expired() || is_swept || amount < dust`. The classification is order-dependent: a VTXO that is expired but NOT spent/swept will hit `is_recoverable()` first and land correctly in `recoverable`. However, the doc claim about "confirmed/pre-confirmed VTXOs the client sees as expired" raises a concern: if client-server clock skew causes the server to not mark a VTXO as expired (see `server.rs:413-414` note about clock disagreement), that VTXO would land in `confirmed`/`pre_confirmed` instead of `recoverable`, and the new `settle()` would miss it.

Clarify whether this edge case is intentional or needs handling.


Issue: No tests for the new settle() method

Severity: Medium

No unit or e2e tests verify:

  • `settle()` correctly filters to only recoverable VTXOs
  • `settle()` returns `Ok(None)` when no recoverable VTXOs exist
  • `settle()` successfully renews expired VTXOs
  • `settle()` leaves healthy VTXOs untouched

For a protocol-critical settlement path, test coverage is essential.


Note: Cross-repo impact is contained

The other SDKs (ts-sdk, go-sdk, dotnet-sdk) have their own `settle()` implementations and are not affected by this Rust-side rename. No cross-repo breakage.


Summary

The core idea is sound — narrowing periodic settlement to only expired/recoverable VTXOs saves batch fees and is a good optimization. But the execution has a critical flaw: reusing the `settle()` name silently changes behavior for ~20 callers without a compile error. Use a distinct name like `settle_recoverable()` and add tests.

⚠️ PROTOCOL-CRITICAL: This PR modifies VTXO settlement logic. Human review required before merge regardless of code changes.


🤖 Review by Arkana

@bonomat
Copy link
Copy Markdown
Collaborator Author

bonomat commented May 21, 2026

imho breaking change is not an issue as I assume this is the preferred behaviour - settle_all is the exception (if it is even ever needed).

Copy link
Copy Markdown
Collaborator

@luckysori luckysori left a comment

Choose a reason for hiding this comment

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

We may also want to introduce a method which settles all boarding outputs. It's already possible by passing specific outputs to settle_vtxos, but it's not as convenient.

I mention this because at least in the e2e tests, which are now all red, we used settle to convert boarding outputs into VTXOs.

EDIT: perhaps this makes more sense: #227 (review).

Copy link
Copy Markdown
Collaborator

@luckysori luckysori left a comment

Choose a reason for hiding this comment

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

Actually, let's reconsider. Shouldn't default settle always include boarding outputs? I would assume they were created because the user wanted to convert them into VTXOs.

Always pull confirmed boarding outputs into the smart settle, alongside
expired/recoverable VTXOs. Freshly funded coins should enter the Ark
without forcing the caller to fall back to settle_all.
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

🚨 Protocol-Critical Follow-up Review (commits 04fb1cff, 30ebbcbb)

Previous blocker (silent semantic break for all callers) is largely resolved — good work. Including boarding outputs in settle() was the right call, and the test migrations to settle_all() are correct.

Two remaining issues:


BUG: e2e_assets.rs:110 still calls settle() — will break

Severity: High
File: e2e-tests/tests/e2e_assets.rs:110

At this point Alice holds pre-confirmed VTXOs from issue_asset() (not recoverable). The new narrow settle() will return Ok(None), and the balance assertions at lines 117-131 will fail.

Fix: Change to alice.settle_all(&mut rng).await.unwrap();


Ongoing: No tests for the new settle() path

Severity: Medium

Still no unit or e2e test that exercises the new narrow settle() and verifies:

  • Returns Ok(None) when only healthy VTXOs exist (no boarding, no recoverable)
  • Settles recoverable VTXOs while leaving healthy ones untouched
  • Correctly includes boarding outputs alongside recoverable VTXOs

For a protocol-critical settlement path, at least one e2e test should demonstrate the filtering behavior.


Notes (non-blocking)

  • ark-client-sample/src/main.rs:611: The CLI settle command now uses the narrow settle(). This is arguably the intended UX (periodic renewal), but worth a conscious decision — a user who just received a send and runs settle from CLI won't see their pre-confirmed VTXOs get settled. Consider whether the CLI should use settle_all() or at least document the behavior.

  • Minor perf: settle() calls fetch_commitment_transaction_inputs() for boarding outpoints, then settle_vtxos() calls it again internally to filter. Two round-trips where one would suffice. Non-blocking but worth noting for future cleanup.


⚠️ PROTOCOL-CRITICAL: VTXO settlement logic. Human review required before merge.


🤖 Review by Arkana

Sub-dust recoverable VTXOs can only be rescued when their combined value
clears the server's dust threshold. Document that callers holding
isolated sub-dust amounts need to fall back to settle_all, which can
roll them in alongside a healthy VTXO acting as carrier value.
@bonomat bonomat force-pushed the feat/smart-settle branch from 30ebbcb to b7043e8 Compare May 21, 2026 04:42
Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

✅ Follow-up Review (commit b7043e8d)

Previous high-severity finding (e2e_assets.rs:110 still calling settle()) is now fixed → settle_all(). Good.

Verified the force-push didn't sneak in other changes — only the e2e_assets.rs fix on top of the prior commits.

Remaining callers using settle() — verified correct

All ~15 remaining settle() callers are boarding-first flows (fund boarding address → settle → assert confirmed balance). Since the new settle() includes boarding outputs, these will work correctly. The callers migrated to settle_all() are the ones holding pre-confirmed VTXOs that need full settlement. Split looks right.

Still outstanding (medium, non-blocking)

  1. No tests for new settle() filtering path — no test verifies that settle() skips healthy VTXOs, returns Ok(None) when nothing is eligible, or correctly combines recoverable VTXOs with boarding outputs. For protocol-critical code this is a gap, but I'm not blocking on it given the human reviewer approved.

  2. Double fetch_commitment_transaction_inputs() callsettle() fetches boarding inputs, then settle_vtxos() fetches them again internally. Minor perf waste, non-blocking.

  3. Sub-dust doc note (batch.rs:128-131) is helpful — accurately describes the limitation where isolated sub-dust amounts need settle_all() as carrier.

⚠️ PROTOCOL-CRITICAL: VTXO settlement logic. Human review required before merge.


🤖 Review by Arkana

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.

2 participants