Skip to content

Custom payouts for mediation part 2: add tx builder & RPC endpoints#144

Merged
ChrisSon15 merged 6 commits intobisq-network:mainfrom
stejbac:add-custom-payouts-for-mediation-part-2
Apr 6, 2026
Merged

Custom payouts for mediation part 2: add tx builder & RPC endpoints#144
ChrisSon15 merged 6 commits intobisq-network:mainfrom
stejbac:add-custom-payouts-for-mediation-part-2

Conversation

@stejbac
Copy link
Copy Markdown
Contributor

@stejbac stejbac commented Apr 5, 2026

Provide a new tx builder for signed Custom Payouts from mediated trades, together with a unit test to show that the construction & signing works without error, and that the final signed tx has the expected weight. As with the Deposit Tx builder, the (un)signed tx is held in a PSBT.

Since the trade wallet is responsible for the multisig signing, the PSBT needs to be augmented with extra data to allow the wallet to determine the script path and key to sign with, and thus we update the PSBT with (definite) descriptors for the buyer/seller inputs, when the unsigned tx is computed. The descriptor is supplied by the function script_paths::deposit_payout_descriptor and is of the form

tr({internal_key},and_v(v:pk({buyer_pub_key}),pk({seller_pub_key})))

When both signatures have been added to the Custom Payout PSBT, it is automatically finalised so that a signed tx can be extracted. This requires some slight changes to the MemWallet and BDK Wallet impls of TradeWallet. The mock trade wallet has also been updated (along with with new fake hardcoded signatures for the buyer/seller mock instances to use) to match the behaviour of the real wallets when partially signing the Custom Payout PSBT.

Provide the new Musig RPC endpoints: SignCustomPayoutTx to create and partially sign a Custom Payout PSBT from the agreed seller's payout amount and tx feerate, and CustomCloseTrade to merge the peer's PSBT and broadcast the resulting signed tx. (For now, the service uses a mock trade wallet and does not do a real broadcast of any of the protocol txs.) Provide new API calls to rpc::protocol to implement the construction, partial signing & merging of Custom Payout PSBTs, using the new tx builder.

Also provide a test case to TradeProtocolClient.java to show how to use the new RPC endpoints.

NOTE: Since only a single CustomPayoutTxBuider is held in the trade model and the builder's fields are write-once, the mediated payouts provided in this way are currently single-shot only and don't permit readjustment of the tx fee or payout amounts/addresses.

--

Also tidy the formatting of assorted Rust source files a little (mainly whitespace & import list order), as well as the Cargo manifests, and run cargo update to bump all dependencies to latest (excluding bdk_wallet, rand, rand_chacha, rusqlite, hmac & sha2, which are held back for compatibility reasons or to avoid bloating the build).

--

Since this PR is already quite big, I will probably provide integration tests for the custom payouts in a third, final PR. This may require further changes to the TradeWallet impls to make sure they work for wallets with an actual keychain from an xprv instead of a single private key.

--

TODO: Try to make script_paths::deposit_payout_descriptor a bit more robust and panic-free, instead of just parsing a formatted string as it does currently. Also make sure dust payout amounts are handled properly. (I will probably do both these things in the final PR for custom payouts.)

stejbac added 6 commits April 2, 2026 02:45
Also remove an unnecessary "anyhow::.." qualification that wasn't picked
up by rustc (even after enabling the lint) for some reason, and revert
the formatting of "container.args(..)" to its pre-rustfmt-normalised
version, adding '#[rustfmt::skip]', to improve readability. (Both in
'testenv/src/lib.rs'.)
Run "cargo update" to bump the minor/patch versions of all the project
dependencies to latest, making sure that the minimum versions specified
in the manifests match those in the lockfile. Also tidy them slightly.

Make major/breaking version updates of 'simple-semaphore' to 1.0.0 and
'zeromq' to 0.5, which require no code changes. But hold 'hmac' & 'sha2'
at 0.12 & 0.10 respectively, to avoid bloating the build, as 'musig2'
still specifies the old versions of those two dependencies.
Use a helper fn to ensure that all the Deposit Tx inputs are actually
signed, and all have empty ScriptSigs, when extracting the final signed
tx from the builder's PSBT, since 'Psbt::extract_tx' does not do this.
No attempt is made to validate the final witnesses, just ensure that no
more can be added.

The fact that 'extract_tx' consumes the PSBT (which requires us to clone
it) suggests that this method wasn't being used as intended, since the
supplied PSBT could have further witnesses added, leading to different
extracted txs. Thus, in cases where the supplied PSBT isn't consumed, it
should first be checked to make sure that all inputs are fully signed
and finalised, to ensure a robust API.

Also remove a redundant validation TODO, when merging the peer's PSBT,
as it looks from the code that 'Psbt::combine' should be perfectly safe
to use as-is, without further sanity checking.
Provide a new tx builder for signed Custom Payouts from mediated trades,
together with a unit test to show that the construction & signing works
without error, and that the final signed tx has the expected weight. As
with the Deposit Tx builder, the (un)signed tx is held in a PSBT.

Since the trade wallet is responsible for the multisig signing, the PSBT
needs to be augmented with extra data to allow the wallet to determine
the script path and key to sign with, and thus the 'witness_utxo' and
'tap_(internal_key|merkle_root|key_origins|scripts)' input fields must
be populated. To this end, provide '(buyer|seller)_input_descriptor' tx
builder params in addition to the regular '(buyer|seller)_input' params,
along with a new 'script_paths::deposit_payout_descriptor' fn to supply
the builder with (definite) input descriptors of the form,

  tr({internal_key},and_v(v:pk({buyer_pub_key}),pk({seller_pub_key})))

which the PSBT inputs are updated from when the unsigned tx is computed.

For now, use the fake wallet in 'bdk_wallet::test_utils' with a single
static private key, in place of the mock trade wallet, when unit testing
the Custom Payout signing.

TODO: Get Custom Payout signing working with the mock trade wallet.
TODO: Ensure signing still works with xprvs in place of single keys.
Provide 'MockTradeWallet' with support to script-sign a PSBT whose
inputs have a single tapscript path, in addition to the existing mock
keysigning of selected inputs. This should be suitable to sign Custom
Payout and dynamic (non-prepared) Claim Txs, provided their PSBT inputs
have been updated from a descriptor to include the 'tap_key_origins' &
'tap_scripts' fields needed to locate the control block, script and leaf
hash of the path, as well as the mapping from pubkeys to leaf hashes.

To this end, add a 'script_sigs' field to 'MockTradeWallet', defining a
map from pubkeys to a vector of signatures to use. Whenever a script-
spending PSBT input with matching pubkey is encountered, a signature is
popped from the vector to add to the 'tap_script_sigs' field of that
input, and the input is automatically mock-finalised once enough
signatures have been added.

Furnish 'psbt::mock_(buyer|seller)_trade_wallet' with the signatures and
internal keys needed to exactly match the signing behaviour of the fake
BDK wallets in 'transaction::tests::test_custom_payout_tx_builder', and
update the unit test to verify that.

This will allow 'rpc::protocol' to create and sign Custom Payout Txs
(with bogus hardcoded signatures) for the 'Musig' gRPC service, which
still uses mock trade wallets. (TODO: Implement that.)
Provide 'rpc::protocol' API calls to build and sign a Custom Payout Tx,
combining PSBTs exchanged with the peer, for mediated trade closures.
Add a 'custom_payout_tx' field to 'TradeModel', with associated wrapper
struct holding a single 'CustomPayoutTxBuilder', partially populated
during trade setup. Currently, this allows just a single-shot mediated
trade closure (since the builder fields are write-once), with the two
output addresses of the Custom Payout Tx set to the traders' respective
Claim payout addresses, which are decided at the trade start.

Add 'Musig' gRPC endpoints: 'SignCustomPayoutTx' to create and partially
sign a Custom Payout PSBT from the agreed seller's payout amount and tx
feerate, and 'CustomCloseTrade' to merge the peer's PSBT and broadcast
the resulting signed tx.

Also add a test case to 'TradeProtocolClient.java' to show how to make a
mediated trade closure with the updated API.
@ChrisSon15
Copy link
Copy Markdown
Collaborator

ACk

@ChrisSon15 ChrisSon15 merged commit 4fffb7b into bisq-network:main Apr 6, 2026
3 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.

2 participants