Skip to content

Support unilateral exit with VHTLC ancestor#228

Merged
luckysori merged 11 commits into
masterfrom
fix/exit-vhtlc
May 25, 2026
Merged

Support unilateral exit with VHTLC ancestor#228
luckysori merged 11 commits into
masterfrom
fix/exit-vhtlc

Conversation

@luckysori
Copy link
Copy Markdown
Collaborator

@luckysori luckysori commented May 21, 2026

Fix #225.

This only expands the number of ancestor scripts that are supported, but I wonder if a better solution would be for the server to return ancestor virtual transactions that are already finalised. After all, the transaction has already been accepted into the virtual mempool, so the client here is only repeating what the original sender + Arkade had to do to publish it in the first place. It's possible that this feature already exists and I'm not aware of it.

Summary by CodeRabbit

  • Improvements

    • Updated batch-tree handling and signing flows for batch transactions.
    • Refactored unilateral-exit processing to finalize exit transaction branches.
  • Documentation

    • Standardized “batch-tree” terminology and clarified VTXO pre-confirmation details.
  • Tests

    • Added an end-to-end regtest for reverse-swap unilateral exit scenarios.

Review Change Stack

@luckysori luckysori requested a review from bonomat May 21, 2026 05:22
@luckysori luckysori self-assigned this May 21, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Warning

Review limit reached

@luckysori, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 9 minutes and 25 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, 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 trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1ad6871-cd74-487e-8a5c-529ee03f36b0

📥 Commits

Reviewing files that changed from the base of the PR and between 774497c and 6d2df3e.

📒 Files selected for processing (9)
  • ark-client/src/batch.rs
  • ark-client/src/unilateral_exit.rs
  • ark-core/src/batch.rs
  • ark-core/src/intent.rs
  • ark-core/src/send.rs
  • ark-core/src/server.rs
  • ark-core/src/tree_tx_output_script.rs
  • ark-core/src/unilateral_exit.rs
  • e2e-tests/tests/boltz_reverse_vhtlc_unilateral_exit.rs

Walkthrough

Client batch flows now use a VTXO batch-tree TxGraph for collecting/building/signing transactions; unilateral-exit handling was refactored to finalize taproot witnesses from PSBT data (supporting VHTLC descendants); docs, error messages, and an ignored e2e test were added/updated.

Changes

Batch-tree graph and unilateral exit finalization refactoring

Layer / File(s) Summary
Batch client VTXO batch-tree graph tracking
ark-client/src/batch.rs
settle_delegate and join_next_batch replace VTXO graph state with vtxo_batch_tree_graph_chunks/vtxo_batch_tree_graph. Tree event handlers (TreeTx, TreeSignature, TreeSigningStarted, TreeNonces) now collect, build, generate nonces from, sign, and submit using the VTXO batch-tree TxGraph.
Core unilateral exit finalization APIs
ark-core/src/unilateral_exit.rs
Added finalize_virtual_tx_input, finalize_taproot_script_spend_witness, and finalize_unilateral_exit_tree to inject witness_utxo and build key- or script-spend witnesses by extracting CHECKSIG/CHECKSIGVERIFY pubkeys and assembling ordered signatures. sign_unilateral_exit_tree is deprecated and delegates to the new finalizer.
Client integration, core docs, and small core edits
ark-client/src/unilateral_exit.rs, ark-core/src/batch.rs, ark-core/src/server.rs, ark-core/src/send.rs, ark-core/src/tree_tx_output_script.rs, ark-core/src/intent.rs
Client code switched to finalize_unilateral_exit_tree. Multiple doc comments and error strings standardized to use “batch-tree”; encode_witness visibility changed to pub(crate); VTXO pre-confirmation wording clarified.
E2E test for VHTLC unilateral exit recovery
e2e-tests/tests/boltz_reverse_vhtlc_unilateral_exit.rs
Adds an ignored async regtest test that orchestrates funding, reverse-swap invoice/payment coordination, unilateral-exit finalization, iterative branch broadcasting with mining, and on-chain spend verification using bitcoinconsensus.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • bonomat
  • vincenzopalazzo
🚥 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 PR title clearly and concisely summarizes the main change: supporting unilateral exit with VHTLC ancestor transactions.
Linked Issues check ✅ Passed All objectives from issue #225 are addressed: broader script support beyond 2-of-2 patterns, exponential path enumeration fix via topological sort, and condition witness handling for VHTLC inputs.
Out of Scope Changes check ✅ Passed All changes align with the linked issue scope; documentation updates for terminology consistency and test coverage for unilateral exit behavior are appropriate supporting changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/exit-vhtlc

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

@coderabbitai coderabbitai Bot left a comment

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 `@ark-core/src/unilateral_exit.rs`:
- Around line 588-603: The code currently panics when accessing
psbt.unsigned_tx.input[VTXO_INPUT_INDEX], indexing outputs by vout, and with
.expect on witness_utxo; replace these panic paths in the branch finalization
logic so the function returns a typed Error instead. Validate that
VTXO_INPUT_INDEX is within psbt.unsigned_tx.input.len() (use get on
psbt.unsigned_tx.input and return Err if missing), check that
virtual_tx_previous_output.vout as usize < other_psbt.output.len() before
cloning the output, and change the .expect("witness UTXO in path") to ok_or(...)
to return a descriptive Error; propagate errors through the surrounding function
(e.g., via Result<..., Error>) and use the existing error enum or create an
appropriate variant for malformed branch data. Ensure references to
virtual_tx_previous_output, psbt, unilateral_exit_branch, commitment_txs, and
compute_txid are used when constructing the error messages for diagnostics.
🪄 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: 7bebd5d5-d8a6-4ea9-aed1-4e246949d149

📥 Commits

Reviewing files that changed from the base of the PR and between 0444708 and a910750.

📒 Files selected for processing (8)
  • ark-client/src/batch.rs
  • ark-client/src/unilateral_exit.rs
  • ark-core/src/batch.rs
  • ark-core/src/send.rs
  • ark-core/src/server.rs
  • ark-core/src/tree_tx_output_script.rs
  • ark-core/src/unilateral_exit.rs
  • e2e-tests/tests/boltz_reverse_vhtlc_unilateral_exit.rs

Comment thread ark-core/src/unilateral_exit.rs
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.

Arkana Code Review — PR #228: Support unilateral exit with VHTLC ancestor

Verdict: Request changes (protocol-critical — requires human sign-off)

This PR generalizes the unilateral exit witness construction from a hardcoded 2-of-2 multisig script to a generic taproot script-spend finalizer that handles any combination of CHECKSIG/CHECKSIGVERIFY pubkeys plus condition witness elements (e.g. VHTLC preimages). The approach is sound and the witness ordering is correct for all current script formats in this codebase.


✅ What looks good

  1. Witness ordering is correct. I verified against the VHTLC claim script in ark-core/src/vhtlc.rs:171-184 (OP_HASH160 <hash> OP_EQUAL OP_VERIFY <receiver> CHECKSIGVERIFY <server> CHECKSIG). The new code pushes reversed signatures then condition elements, which places condition data at the top of the stack — consumed first by hash-check opcodes before CHECKSIG executes. This matches the old hardcoded behavior for 2-of-2 scripts and extends correctly to VHTLC scripts.

  2. extract_checksig_pubkeys is more robust than the old extract_pubkeys_from_2of2_script. It uses instruction-level parsing instead of raw byte offsets, handles arbitrary script lengths, and has existing test coverage.

  3. Deprecated wrapper for sign_unilateral_exit_tree preserves backward compatibility.

  4. Condition element decoding in condition_witness_elements has proper bounds checking (element count vs remaining buffer, per-element length validation, overflow guards).

  5. E2E test (boltz_reverse_vhtlc_unilateral_exit.rs) covers the full happy path including bitcoinconsensus::verify.

  6. No cross-repo breakage. sign_unilateral_exit_tree and extract_pubkeys_from_2of2_script are only consumed within rust-sdk itself. Other SDKs (ts-sdk, go-sdk, dotnet-sdk) use different architectures.


⚠️ Issues to address

1. Implicit script layout assumption — needs documentation

ark-core/src/unilateral_exit.rsfinalize_taproot_script_spend_witness

The function assumes condition witness elements always go at the top of the stack (pushed after signatures in the witness). This is correct for ALL current scripts (VHTLC claim, unilateral claim) because they put hash-check opcodes before CHECKSIG. But this is an implicit invariant that will silently produce invalid witnesses if anyone creates a script with CHECKSIG before the condition check.

Request: Add a doc comment on finalize_taproot_script_spend_witness explicitly stating this contract: "Condition witness elements are placed at the top of the initial stack. This requires that condition-checking opcodes (OP_HASH160, OP_SIZE, etc.) appear before any CHECKSIG/CHECKSIGVERIFY in the tapleaf script."

2. .expect("witness UTXO in path") is a panic in protocol-critical code

ark-core/src/unilateral_exit.rsfinalize_unilateral_exit_tree, near line 596 (in the diff)

.expect("witness UTXO in path");

If a VTXO's previous output doesn't match any transaction in the branch or commitment set, this panics instead of returning an error. In a unilateral exit scenario — which is already an emergency path — a panic here could prevent the user from recovering funds.

Request: Replace with .ok_or_else(|| Error::transaction("witness UTXO not found in unilateral exit path"))?

(This is pre-existing, but you're touching this code path and it's protocol-critical.)


📝 Nits (non-blocking)

  1. tap_scripts iteration order: finalize_taproot_script_spend_witness picks the first satisfiable leaf from BTreeMap<ControlBlock, ...> iteration. This works because the caller only populates signatures for the intended leaf, but it's worth a brief comment noting that exactly one leaf should have complete signatures.

  2. No unit test for condition_witness_elements decoder in isolation. The encoding lives in intent.rs::encode_witness and boltz.rs::inject_preimage_into_psbt. A round-trip unit test encode_witness → condition_witness_elements would catch format drift between encoder and decoder without needing the full E2E environment.

  3. Boltz.rs encoder uses vec![1] raw byte (lines 1460, 1712, 2132, 3574) while intent.rs::encode_witness uses write_compact_size. Both produce identical output for small counts, but the inconsistency is a maintenance footprint. Consider having boltz.rs call encode_witness instead of rolling its own encoding. (Out of scope for this PR, but worth a follow-up.)


🔒 Protocol-critical flag

This PR modifies unilateral exit witness construction — the code path users depend on to recover funds when the Ark server is uncooperative. Even though the logic is correct, a human must review and approve before merge. Incorrect witness construction here = funds permanently locked.

🤖 Reviewed by Arkana

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.

Arkana Code Review — PR #228: Support unilateral exit with VHTLC ancestor

Verdict: Request changes (protocol-critical — requires human sign-off)

This PR generalizes the unilateral exit witness construction from a hardcoded 2-of-2 multisig script to a generic taproot script-spend finalizer that handles any combination of CHECKSIG/CHECKSIGVERIFY pubkeys plus condition witness elements (e.g. VHTLC preimages). The approach is sound and the witness ordering is correct for all current script formats in this codebase.


✅ What looks good

  1. Witness ordering is correct. I verified against the VHTLC claim script in ark-core/src/vhtlc.rs:171-184 (OP_HASH160 <hash> OP_EQUAL OP_VERIFY <receiver> CHECKSIGVERIFY <server> CHECKSIG). The new code pushes reversed signatures then condition elements, which places condition data at the top of the stack — consumed first by hash-check opcodes before CHECKSIG executes. This matches the old hardcoded behavior for 2-of-2 scripts and extends correctly to VHTLC scripts.

  2. extract_checksig_pubkeys is more robust than the old extract_pubkeys_from_2of2_script. It uses instruction-level parsing instead of raw byte offsets, handles arbitrary script lengths, and has existing test coverage.

  3. Deprecated wrapper for sign_unilateral_exit_tree preserves backward compatibility.

  4. Condition element decoding in condition_witness_elements has proper bounds checking (element count vs remaining buffer, per-element length validation, overflow guards).

  5. E2E test (boltz_reverse_vhtlc_unilateral_exit.rs) covers the full happy path including bitcoinconsensus::verify.

  6. No cross-repo breakage. sign_unilateral_exit_tree and extract_pubkeys_from_2of2_script are only consumed within rust-sdk itself. Other SDKs (ts-sdk, go-sdk, dotnet-sdk) use different architectures.


⚠️ Issues to address

1. Implicit script layout assumption — needs documentation

ark-core/src/unilateral_exit.rsfinalize_taproot_script_spend_witness

The function assumes condition witness elements always go at the top of the stack (pushed after signatures in the witness). This is correct for ALL current scripts (VHTLC claim, unilateral claim) because they put hash-check opcodes before CHECKSIG. But this is an implicit invariant that will silently produce invalid witnesses if anyone creates a script with CHECKSIG before the condition check.

Request: Add a doc comment on finalize_taproot_script_spend_witness explicitly stating this contract: "Condition witness elements are placed at the top of the initial stack. This requires that condition-checking opcodes (OP_HASH160, OP_SIZE, etc.) appear before any CHECKSIG/CHECKSIGVERIFY in the tapleaf script."

2. .expect("witness UTXO in path") is a panic in protocol-critical code

ark-core/src/unilateral_exit.rsfinalize_unilateral_exit_tree, near the witness_utxo lookup

.expect("witness UTXO in path");

If a VTXO's previous output doesn't match any transaction in the branch or commitment set, this panics instead of returning an error. In a unilateral exit scenario — which is already an emergency path — a panic here could prevent the user from recovering funds.

Request: Replace with .ok_or_else(|| Error::transaction("witness UTXO not found in unilateral exit path"))?

(This is pre-existing, but you're touching this code path and it's protocol-critical.)


📝 Nits (non-blocking)

  1. tap_scripts iteration order: finalize_taproot_script_spend_witness picks the first satisfiable leaf from BTreeMap<ControlBlock, ...> iteration. This works because the caller only populates signatures for the intended leaf, but it's worth a brief comment noting that exactly one leaf should have complete signatures.

  2. No unit test for condition_witness_elements decoder in isolation. The encoding lives in intent.rs::encode_witness and boltz.rs::inject_preimage_into_psbt. A round-trip unit test encode_witness → condition_witness_elements would catch format drift between encoder and decoder without needing the full E2E environment.

  3. Boltz.rs encoder uses vec![1] raw byte (lines ~1460, ~1712, ~2132, ~3574) while intent.rs::encode_witness uses write_compact_size. Both produce identical output for small counts, but the inconsistency is a maintenance footprint. Consider having boltz.rs call encode_witness instead of rolling its own encoding. (Out of scope for this PR, but worth a follow-up.)


🔒 Protocol-critical flag

This PR modifies unilateral exit witness construction — the code path users depend on to recover funds when the Ark server is uncooperative. Even though the logic is correct, a human must review and approve before merge. Incorrect witness construction here = funds permanently locked.

🤖 Reviewed by Arkana

@luckysori luckysori changed the title Support unilateral exit with VHTLC ancestor. Support unilateral exit with VHTLC ancestor May 22, 2026
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.

Arkana Code Review — PR #228 (follow-up)

Verdict: Approve

All three issues from my previous review are resolved in the latest commits:

  1. f4258d2.expect() replaced with proper error handling in finalize_unilateral_exit_tree. The panic on missing witness_utxo is now a descriptive Error::ad_hoc with the outpoint included. Correct fix for protocol-critical code.

  2. 1167fc5 — Doc comment added on finalize_taproot_script_spend_witness documenting the condition-first script layout invariant. Matches exactly what I requested.

  3. 774497c — Round-trip unit test for condition_witness_elements decoder covering edge cases (empty element, 253-byte varint boundary, regular data). encode_witness visibility minimally bumped to pub(crate) — no leakage.


🔒 Protocol-critical reminder

This PR modifies unilateral exit witness construction — the emergency fund recovery path. The code is correct, but a human must still review and approve before merge.

🤖 Reviewed by Arkana

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.

Arkana Code Review — PR #228 (follow-up)

Verdict: Approve

All three issues from my previous review are resolved in the latest commits:

  1. f4258d2.expect() replaced with proper error handling in finalize_unilateral_exit_tree. The panic on missing witness_utxo is now a descriptive Error::ad_hoc with the outpoint included. Correct fix for protocol-critical code.

  2. 1167fc5 — Doc comment added on finalize_taproot_script_spend_witness documenting the condition-first script layout invariant. Matches exactly what I requested.

  3. 774497c — Round-trip unit test for condition_witness_elements decoder covering edge cases (empty element, 253-byte varint boundary, regular data). encode_witness visibility minimally bumped to pub(crate) — no leakage.


🔒 Protocol-critical reminder

This PR modifies unilateral exit witness construction — the emergency fund recovery path. The code is correct, but a human must still review and approve before merge.

🤖 Reviewed by Arkana

@luckysori luckysori merged commit 241e229 into master May 25, 2026
9 checks passed
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.

sign_unilateral_exit_tree rejects VHTLC inputs — VTXOs descended from Lightning swaps cannot be exit-prepared

1 participant