Skip to content

Support multiple payment outputs on claim and refund transactions - #162

Open
BullishNode wants to merge 4 commits into
trunkfrom
feat/multi-output-claims
Open

Support multiple payment outputs on claim and refund transactions#162
BullishNode wants to merge 4 commits into
trunkfrom
feat/multi-output-claims

Conversation

@BullishNode

@BullishNode BullishNode commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Adds the ability to pay multiple outputs from a swap claim or refund transaction, on both Liquid and Bitcoin.

  • LBtcSwapTx / BtcSwapTx gain additional_outputs: Vec<(Address, u64)> and a with_additional_outputs(...) setter.
  • The primary output_address receives the remainder (input − fee − Σ additional) and remains the first payment output (index 0 on claims; after the fee output on Liquid refunds, matching the existing refund output order). Additional outputs pay fixed amounts in the given order.
  • TransactionOptions::with_additional_outputs(Vec<(String, u64)>) threads the destinations through construct_claim / construct_refund on both chains without breaking existing callers (new private field with Default).

Validation policy

Construction enforces balance, not policy:

  • Additional amounts are summed with checked_add (a wrapping sum cannot bypass the insufficient-funds guard) and must fit in input − fee.
  • Bitcoin accepts zero-valued outputs and a zero primary remainder: zero outputs are consensus-valid, and dust/relay policy belongs to the broadcaster, not the constructor.
  • Liquid rejects zero-valued outputs — including a zero primary — up front with a clear protocol error: every payment output is confidential and the rangeproof cannot prove less than 1 satoshi (TxOut::RANGEPROOF_MIN_VALUE).
  • The wrapper conversions validate address strings before any signing: network match on Bitcoin (also fixes a pre-existing bug where new_claim_with_utxo computed but discarded its network check), address params + confidentiality on Liquid.

Liquid CT details

create_claim/create_refund share a create_payment_outputs helper generalized to N blinded outputs:

  • each output gets its own asset blinding factor, surjection proof and rangeproof;
  • the last payment output balances the blinding equation via ValueBlindingFactor::last against the input and the explicit fee output;
  • for a single output the arguments are identical to the previous code, so existing behavior is unchanged.

The cooperative (MuSig key-path) flows are untouched: they already sign whatever transaction was built and send the full serialized tx to Boltz for the partial signature, which commits to all outputs.

Tests

  • Unit tests fabricate a blinded funding utxo locally (no chain backend): multi-output Liquid claim and refund pass verify_tx_amt_proofs and every output unblinds to the expected value/asset/script_pubkey; boundary cases cover zero, one-satoshi, [u64::MAX, 1] overflow, zero-primary, insufficient funds and unblinded addresses on the appropriate chain.
  • Node-level broadcast tests (tests/txs.rs): multi-output claims and refunds on both chains are broadcast to elementsd/bitcoind and accepted, including a Bitcoin multi-output claim with Fee::Relative proving the added output weight is paid for.
  • Boltz regtest suite exercises the wrapper path end to end: reverse claims run a non-cooperative multi-output variant; submarine refunds and chain-swap claims pass additional outputs cooperatively, so Boltz partial-signs complete multi-output transactions on both chains. A shared helper asserts remainder math, output ordering, and unblinds Liquid outputs via the node's blinding keys.
  • Mainnet battle tests against production Boltz (harness in examples/multiout_battletest.rs), all claimed cooperatively:
    • reverse swap ZqxQ58hMEIxY → Liquid claim 0e845207… (2 blinded outputs + explicit fee, confirmed)
    • chain swap L-BTC→BTC FkNCgU6FJzfA → Bitcoin claim 7266da85… (24,343 + 5,000 sat outputs)
    • chain swap BTC→L-BTC 5RHJIphzQrkN → Liquid claim 24a88229…

All of cargo fmt --check, make cargo-clippy, make wasm-clippy, make cargo-test (73 passed), make wasm-test, make cargo-regtest-test (13/13 flows) and make wasm-regtest-test (7/7) pass.

Compatibility notes

  • Must ship as 0.5.0 (changelog section included; version bump left to the release PR). Adding the struct field is source-breaking for code constructing LBtcSwapTx/BtcSwapTx with literal syntax; constructors initialize it to an empty vector and the wrapper API is backward compatible.
  • Bitcoin single-output behavior with input == fee (zero primary) is unchanged; Liquid previously failed the same case deep inside blinding and now fails it with a clear protocol error.
  • The insufficient-funds error strings changed slightly (they now mention additional outputs).
  • FFI bindings (UniFFI/Python/Dart) intentionally do not expose additional_outputs yet — follow-up work.

Adds `additional_outputs: Vec<(Address, u64)>` to LBtcSwapTx and BtcSwapTx
with a `with_additional_outputs` setter. The primary `output_address`
receives the remainder (input - fee - sum of additional outputs) and
remains the first payment output. Additional outputs pay fixed amounts;
amounts are summed with overflow checks and validated non-zero when the
transaction is built.

Liquid: claim and refund construction share a `create_payment_outputs`
helper generalized to N blinded outputs. Each output gets its own asset
blinding factor, surjection proof and rangeproof; the last payment output
balances the blinding equation (ValueBlindingFactor::last) against the
input and the explicit fee output. All destination addresses must be
confidential. Output ordering is unchanged: [payments..., fee] for claims,
[fee, payments...] for refunds.

Bitcoin: claim and refund transactions build one output per destination,
primary remainder at index 0.

Wrappers: TransactionOptions::with_additional_outputs threads the extra
destinations through construct_claim and construct_refund on both chains
without breaking existing callers.

The cooperative (MuSig key-path) flows are unchanged: they already sign
whatever transaction create_claim/create_refund built, and the full
serialized transaction is sent to Boltz for the partial signature.

Tests: unit tests fabricate a blinded funding utxo locally and assert
that multi-output Liquid claim and refund transactions pass
verify_tx_amt_proofs (pedersen balance, rangeproofs, surjection proofs)
and that every output unblinds to the expected value, asset and
script_pubkey; Bitcoin tests cover claim and refund output composition
and the insufficient-funds and zero-amount errors. Regtest integration
tests broadcast a multi-output confidential claim (relative fee) and a
multi-output Bitcoin claim and confirm node acceptance.
@BullishNode
BullishNode force-pushed the feat/multi-output-claims branch from d7abd83 to 68cd501 Compare July 12, 2026 05:08
@BullishNode
BullishNode marked this pull request as draft July 12, 2026 16:30
Bitcoin construction no longer rejects zero-valued outputs: it enforces
balance (overflow, overspend) but not dust or relay policy, which belongs
to the broadcaster. Liquid now rejects a zero-valued primary output
unconditionally with a clear protocol error instead of failing deep
inside blinding (rangeproofs cannot prove less than 1 satoshi).

Fix BtcSwapTx::new_claim_with_utxo silently discarding the result of its
claim-address network check, and validate additional-output address
strings in the wrappers: network on Bitcoin, confidentiality and address
params on Liquid. Includes boundary unit tests (zero, one-satoshi,
overflow, zero-primary), builder documentation, and a 0.5.0 changelog
section for the source-breaking public field.
Extend the regtest suites through the wrapper path: reverse claims run a
non-cooperative multi-output variant, submarine refunds and chain-swap
claims pass additional outputs through TransactionOptions cooperatively,
so Boltz partial-signs complete multi-output transactions on both chains.
A shared assert_multi_output_tx helper verifies remainder math, output
ordering, and unblinds Liquid outputs with the node's blinding keys.

Add node-level broadcast coverage in txs.rs: multi-output Bitcoin and
Liquid refunds, and a Bitcoin multi-output claim with a relative fee to
show the added output weight is paid for.
@BullishNode
BullishNode force-pushed the feat/multi-output-claims branch from 79aba87 to f33f680 Compare July 12, 2026 19:39
Reverse and chain swap subcommands against production Boltz with a
recovery state file written before any payment information is shown, a
refund escape hatch for chain swaps, and a NONCOOP env toggle for
script-path claims. Used to verify multi-output claims with real funds:
reverse swap ZqxQ58hMEIxY (Liquid claim, 2 blinded outputs + fee) and
chain swaps FkNCgU6FJzfA / 5RHJIphzQrkN (Bitcoin and Liquid claims),
all claimed cooperatively.
@BullishNode
BullishNode force-pushed the feat/multi-output-claims branch from f33f680 to d3b8520 Compare July 12, 2026 19:46
@BullishNode
BullishNode marked this pull request as ready for review July 12, 2026 20:14
@BullishNode
BullishNode requested a review from i5hi July 12, 2026 20:14
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