Skip to content

Commit 4fffb7b

Browse files
authored
Merge pull request #144 from stejbac/add-custom-payouts-for-mediation-part-2
Custom payouts for mediation part 2: add tx builder & RPC endpoints
2 parents 8ef044d + f3c7f8e commit 4fffb7b

21 files changed

Lines changed: 769 additions & 405 deletions

File tree

Cargo.lock

Lines changed: 208 additions & 257 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
[workspace]
22
resolver = "3"
33
members = ["protocol", "rpc", "wallet", "testenv", "bmp_tracing", "mem"]
4-
exclude = ["poc"]
54

65
[workspace.dependencies]
7-
anyhow = "1.0.101"
6+
anyhow = "1.0.102"
87
argon2 = { version = "0.5.3", default-features = false }
98
base64 = "0.22.1"
109
bdk_bitcoind_rpc = "0.22.0"
@@ -20,13 +19,14 @@ rand = "0.9.2"
2019
rand_chacha = "0.9.0"
2120
rusqlite = { version = "0.31.0", features = ["bundled-sqlcipher"] }
2221
secp = "0.6.0"
23-
simple-semaphore = "0.2.0"
24-
tempfile = "3.25.0"
22+
simple-semaphore = "1.0.0"
23+
tempfile = "3.27.0"
2524
thiserror = "2.0.18"
26-
tokio = "1.49.0"
25+
tokio = "1.50.0"
26+
tokio-stream = "0.1.18"
2727
tracing = "0.1.44"
28-
tracing-core = { version = "0.1.35", default-features = false }
29-
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
28+
tracing-core = { version = "0.1.36", default-features = false }
29+
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
3030
zeroize = "1.8.2"
3131

3232
[workspace.lints.rust]

bmp_tracing/src/lib.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
pub use tracing;
2-
pub use tracing_subscriber;
3-
41
use std::error::Error as _;
52
use std::fs::File;
63
use std::io;
74
use std::path::PathBuf;
85

96
use tracing_subscriber::filter::EnvFilter;
10-
use tracing_subscriber::{
11-
Layer, fmt, layer::SubscriberExt as _, registry::LookupSpan, util::SubscriberInitExt as _,
12-
};
7+
use tracing_subscriber::layer::SubscriberExt as _;
8+
use tracing_subscriber::registry::LookupSpan;
9+
use tracing_subscriber::util::SubscriberInitExt as _;
10+
use tracing_subscriber::{Layer, fmt};
11+
pub use {tracing, tracing_subscriber};
1312

1413
#[derive(Debug, Clone)]
1514
#[expect(clippy::exhaustive_enums)]

mem/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ edition = "2024"
55

66
[dependencies]
77
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] }
8-
zeromq = { version = "0.4", default-features = false, features = ["tokio-runtime", "tcp-transport"] }
8+
zeromq = { version = "0.5", default-features = false, features = ["tokio-runtime", "tcp-transport"] }
99
anyhow = { workspace = true }
1010
bdk_wallet = { workspace = true }
1111
hex = { workspace = true }
12-
tokio-stream = "0.1.18"
12+
tokio-stream = { workspace = true }
1313

1414
[dev-dependencies]
1515
testenv = { path = "../testenv" }

mem/src/lib.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use zeromq::{Socket as _, SocketRecv as _, SubSocket};
88
pub async fn stream_unconfirmed_tx(zmq_connect: &str) -> ReceiverStream<Transaction> {
99
// Subscribe to raw transactions via ZMQ
1010
let mut sub = SubSocket::new();
11-
sub.connect(zmq_connect)
12-
.await
13-
.expect("zmq connect");
11+
sub.connect(zmq_connect).await.expect("zmq connect");
1412
sub.subscribe("rawtx").await.expect("zmq subscribe");
1513

1614
// Small delay to let the subscription propagate
@@ -21,13 +19,12 @@ pub async fn stream_unconfirmed_tx(zmq_connect: &str) -> ReceiverStream<Transact
2119

2220
// Spawn a task to receive messages from the ZMQ socket and send them through the channel
2321
tokio::spawn(async move {
24-
2522
while let Ok(zmq_msg) = sub.recv().await {
2623
let frames = zmq_msg.into_vec();
2724
assert_eq!(frames[0].as_ref(), b"rawtx", "first frame should be the topic");
2825

2926
let transaction: Transaction =
30-
consensus::deserialize(&frames[1]).expect("deserialize zmq tx");
27+
consensus::deserialize(&frames[1]).expect("deserialize zmq tx");
3128
tx.send(transaction).await.unwrap();
3229
}
3330
});

mem/tests/zmq.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::time::Duration;
22

33
use anyhow::Result;
4-
use bdk_wallet::bitcoin::{consensus, Amount, Transaction};
4+
use bdk_wallet::bitcoin::{Amount, Transaction, consensus};
55
use mem::stream_unconfirmed_tx;
66
use testenv::TestEnv;
77
use tokio_stream::StreamExt as _;
@@ -17,7 +17,7 @@ async fn test_stream_zmq_async_receives_broadcast_tx() -> Result<()> {
1717
let address = env.new_address()?;
1818
let amount = Amount::from_sat(50_000);
1919
let txid = env.fund_address(&address, amount)?;
20-
env.debug_tx(txid); // output link to inspect the transaction iff esplorer is running.
20+
env.debug_tx(txid); // output link to inspect the transaction iff esplora is running.
2121

2222
let zmq_tx = tokio::time::timeout(
2323
Duration::from_secs(5),

protocol/src/protocol_musig_adaptor.rs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use bdk_wallet::bitcoin::{
66
};
77
use musig2::secp::{MaybeScalar, Point};
88
use musig2::{PartialSignature, PubNonce};
9-
109
use wallet::protocol_wallet_api::MemWallet;
10+
1111
use crate::multisig::{KeyCtx, SigCtx};
1212
use crate::receiver::{Receiver, ReceiverList};
1313
use crate::transaction::{
@@ -67,10 +67,10 @@ pub struct Round2Parameter {
6767
#[derive(Debug)]
6868
pub struct Round3Parameter {
6969
// DepositTx --------
70-
pub deposit_txid: Txid, // only for verification / fast fail
70+
// only for verification / fast fail
71+
pub deposit_txid: Txid,
7172
// SwapTx --------------
7273
// aggregated adaptive signature for SwapTx,
73-
7474
pub swap_part_sig: PartialSignature,
7575
pub p_part_peer: PartialSignature,
7676
pub q_part_peer: PartialSignature,
@@ -314,7 +314,8 @@ impl BMPProtocol {
314314
/// can raise the fees with CPFP to get it mined before `ClaimTx` can be broadcast.
315315
///
316316
/// `RedirectTx` Bob spends from `WarningTx` Alice, that's important.
317-
/// Sending funds to the DAO is done by having a list of addresses (from contributors) and percentages. (must add up to 100%)
317+
/// Sending funds to the DAO is done by having a list of addresses (from contributors) and
318+
/// percentages. (must add up to 100%)
318319
#[derive(Default)]
319320
pub struct RedirectTx {
320321
pub sig: SigCtx,
@@ -544,8 +545,8 @@ impl SwapTx {
544545
self.swap_spend.clone()
545546
}
546547

547-
/// even though only the seller gets a `SwapTx` transaction, both parties are constructing the transaction
548-
/// and only the buyer will send the seller the signature.
548+
/// even though only the seller gets a `SwapTx` transaction, both parties are constructing the
549+
/// transaction and only the buyer will send the seller the signature.
549550
fn new(role: ProtocolRole) -> Self {
550551
Self {
551552
role,
@@ -655,11 +656,11 @@ impl DepositTx {
655656

656657
let psbt = if ctx.am_buyer() {
657658
self.builder
658-
.init_buyers_half_psbt(&mut ctx.funds, &mut rand::rng())?
659+
.init_buyers_half_psbt(&mut ctx.funds, &mut rand::rng())?
659660
.buyers_half_psbt()?
660661
} else {
661662
self.builder
662-
.init_sellers_half_psbt(&mut ctx.funds, &mut rand::rng())?
663+
.init_sellers_half_psbt(&mut ctx.funds, &mut rand::rng())?
663664
.sellers_half_psbt()?
664665
};
665666
Ok(psbt.clone())
@@ -687,8 +688,10 @@ impl DepositTx {
687688
Ok(())
688689
}
689690

690-
fn transfer_sig_and_broadcast(&mut self, ctx: &mut BMPContext,
691-
psbt_bob: Psbt, // bobs psbt should be same as mine but have bob's sig
691+
fn transfer_sig_and_broadcast(
692+
&mut self,
693+
ctx: &mut BMPContext,
694+
psbt_bob: Psbt, // bobs psbt should be same as mine but have bob's sig
692695
) -> anyhow::Result<Txid> {
693696
self.builder.combine_psbts(psbt_bob)?;
694697

protocol/src/psbt.rs

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::{BTreeMap, BTreeSet};
2+
use std::mem;
23
use std::sync::LazyLock;
34

45
use bdk_wallet::bitcoin::amount::CheckedSum as _;
@@ -10,6 +11,7 @@ use bdk_wallet::bitcoin::{
1011
Address, Amount, FeeRate, Network, OutPoint, Psbt, ScriptBuf, Sequence, TapSighashType,
1112
Transaction, TxIn, TxOut, Weight, Witness, XOnlyPublicKey, absolute, psbt, script, secp256k1,
1213
};
14+
use bdk_wallet::miniscript::psbt::PsbtExt as _;
1315
use bdk_wallet::miniscript::{Descriptor, ToPublicKey as _};
1416
use bdk_wallet::{KeychainKind, SignOptions, TxOrdering, Wallet};
1517
use rand::{RngCore, SeedableRng as _};
@@ -54,11 +56,10 @@ struct MockTradeWallet<Cs: Iterator<Item=TxOutput>, As: Iterator<Item=Address>>
5456
new_addresses: As,
5557
signature_map: BTreeMap<OutPoint, Signature>,
5658
internal_key: Option<XOnlyPublicKey>,
59+
script_sigs: BTreeMap<XOnlyPublicKey, Vec<Signature>>,
5760
}
5861

59-
impl<Cs: Iterator<Item = TxOutput>, As: Iterator<Item = Address>> TradeWallet
60-
for MockTradeWallet<Cs, As>
61-
{
62+
impl<Cs: Iterator<Item=TxOutput>, As: Iterator<Item=Address>> TradeWallet for MockTradeWallet<Cs, As> {
6263
fn network(&self) -> Network { Network::Regtest }
6364

6465
fn new_address(&mut self) -> Result<Address> {
@@ -140,12 +141,38 @@ impl<Cs: Iterator<Item = TxOutput>, As: Iterator<Item = Address>> TradeWallet
140141
}
141142

142143
fn sign_selected_inputs(&self, psbt: &mut Psbt, is_selected: &dyn Fn(&OutPoint) -> bool) -> Result<()> {
144+
let mut script_sigs = self.script_sigs.clone();
145+
143146
for (input, TxIn { previous_output, .. })
144147
in psbt.inputs.iter_mut().zip(&psbt.unsigned_tx.input) {
145148
if is_selected(previous_output) {
146-
let signature = self.signature_map.get(previous_output)
147-
.ok_or(TransactionErrorKind::MissingSignature)?;
148-
input.final_script_witness = Some(Witness::p2tr_key_spend(signature));
149+
for (key, (leaf_hashes, _)) in &input.tap_key_origins {
150+
if let Some(sig) = script_sigs.get_mut(key).and_then(Vec::pop) {
151+
for leaf_hash in leaf_hashes {
152+
input.tap_script_sigs.insert((*key, *leaf_hash), sig);
153+
}
154+
}
155+
}
156+
if let Some(signature) = self.signature_map.get(previous_output) {
157+
// Mock keyspend:
158+
input.final_script_witness = Some(Witness::p2tr_key_spend(signature));
159+
input.redact_sensitive_fields();
160+
} else if input.tap_key_origins.len() == input.tap_script_sigs.len() + 1 {
161+
// Mock script spend (assumes only one path):
162+
if let Some((control_block, (script, _))) = input.tap_scripts.first_key_value() {
163+
let mut wit = Witness::new();
164+
// For the purpose of the mock, assume that (pubkey, leaf-hash) -> signature
165+
// mappings occur in the opposite order that the signatures need to be added
166+
// to the witness. This won't be true in general.
167+
for sig in input.tap_script_sigs.values().rev() {
168+
wit.push(sig.serialize());
169+
}
170+
wit.push(script.as_bytes());
171+
wit.push(control_block.serialize());
172+
input.final_script_witness = Some(wit);
173+
input.redact_sensitive_fields();
174+
}
175+
}
149176
}
150177
}
151178
Ok(())
@@ -169,9 +196,15 @@ pub fn mock_buyer_trade_wallet() -> impl TradeWallet {
169196
].map(|a| a.parse::<Address<_>>()
170197
.expect("hardcoded addresses should be valid").assume_checked()).into_iter();
171198
let internal_key =
172-
"0000000000000000000000000000000000000000000000000000000000000001".parse().ok();
199+
"51494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295".parse().ok();
200+
let script_sigs = script_sigs(internal_key.as_slice(), &[
201+
"5564448d3c5f024eaf2c65024a0c6e7a9066eb0390f8ffaeee2feacde310fabf\
202+
87f3a8d8ad7fb125d7a6f68a282cfab8cd3178262a1fd0c2d06a598c8c454af8",
203+
"652d0abaa3b4f8c7dd85ac9d523d44f768c8e1541aded79165c3cdfb3ba35d62\
204+
eef114e89becb490a80cfdab946d2d91748ccea501ceb4f08655dcc2868c0463",
205+
]);
173206

174-
MockTradeWallet { funding_coins, new_addresses, signature_map, internal_key }
207+
MockTradeWallet { funding_coins, new_addresses, signature_map, internal_key, script_sigs }
175208
}
176209

177210
//noinspection SpellCheckingInspection
@@ -195,17 +228,30 @@ pub fn mock_seller_trade_wallet() -> impl TradeWallet {
195228
].map(|a| a.parse::<Address<_>>()
196229
.expect("hardcoded addresses should be valid").assume_checked()).into_iter();
197230
let internal_key =
198-
"0000000000000000000000000000000000000000000000000000000000000002".parse().ok();
231+
"fcba7ecf41bc7e1be4ee122d9d22e3333671eb0a3a87b5cdf099d59874e1940f".parse().ok();
232+
let script_sigs = script_sigs(internal_key.as_slice(), &[
233+
"87790f7eb3e98eb1b4dadc55ff5762275c4e3c02c6491abb26c8eabfada55b4b\
234+
3f2627f627919d667be8f191a1b275b01549ab24e5eeda0019f83c658840500e",
235+
"52fe2e44a4789a0f9bc406da144dcacca2621d2c1286e2d8f9913425c9927288\
236+
13d658a3334a4070ce585cb907a67604fc74578e84e714c38c6547377fac133e",
237+
]);
199238

200-
MockTradeWallet { funding_coins, new_addresses, signature_map, internal_key }
239+
MockTradeWallet { funding_coins, new_addresses, signature_map, internal_key, script_sigs }
201240
}
202241

203-
fn signature_map(funding_coins: &[TxOutput], signatures: &[&'static str]) -> BTreeMap<OutPoint, Signature> {
204-
let signatures = signatures.iter().map(|s| Signature {
242+
fn signature_vec(signatures: &[&'static str]) -> Vec<Signature> {
243+
signatures.iter().map(|s| Signature {
205244
signature: s.parse().expect("hardcoded signatures should be valid"),
206245
sighash_type: TapSighashType::Default,
207-
});
208-
funding_coins.iter().map(|o| o.outpoint).zip(signatures).collect()
246+
}).collect()
247+
}
248+
249+
fn signature_map(funding_coins: &[TxOutput], signatures: &[&'static str]) -> BTreeMap<OutPoint, Signature> {
250+
funding_coins.iter().map(|o| o.outpoint).zip(signature_vec(signatures)).collect()
251+
}
252+
253+
fn script_sigs(iks: &[XOnlyPublicKey], signatures: &[&'static str]) -> BTreeMap<XOnlyPublicKey, Vec<Signature>> {
254+
iks.iter().map(|k| (*k, signature_vec(signatures))).collect()
209255
}
210256

211257
impl TradeWallet for Wallet {
@@ -238,19 +284,19 @@ impl TradeWallet for Wallet {
238284
) -> Result<Psbt> {
239285
let mut builder = self.build_tx();
240286
builder
241-
.ordering(TxOrdering::Untouched)
242-
.nlocktime(absolute::LockTime::ZERO)
243-
.fee_rate(fee_rate)
244-
.add_recipient(half_deposit_placeholder_spk(rng), deposit_amount);
287+
.ordering(TxOrdering::Untouched)
288+
.nlocktime(absolute::LockTime::ZERO)
289+
.fee_rate(fee_rate)
290+
.add_recipient(half_deposit_placeholder_spk(rng), deposit_amount);
245291
for receiver in trade_fee_receivers {
246292
builder.add_recipient(receiver.address.script_pubkey(), receiver.amount);
247293
}
248294
let mut psbt = builder.finish()?;
249295
// Calculate tx fee overpay unconditionally, as this performs additional checks on the PSBT:
250296
let overpay_msat: u64 = half_psbt_fee_overpay_msat(&psbt, fee_rate)
251-
.ok_or(TransactionErrorKind::Overflow)?
252-
.try_into()
253-
.map_err(|_| TransactionErrorKind::InvalidPsbt)?;
297+
.ok_or(TransactionErrorKind::Overflow)?
298+
.try_into()
299+
.map_err(|_| TransactionErrorKind::InvalidPsbt)?;
254300
let change_output_index = 1 + trade_fee_receivers.len();
255301
if psbt.unsigned_tx.output.len() > change_output_index {
256302
// Correct any tx fee overpay due to overly conservative input witness size estimation
@@ -274,6 +320,13 @@ impl TradeWallet for Wallet {
274320
if is_selected(&psbt.unsigned_tx.input[i].previous_output) {
275321
psbt.inputs[i].final_script_sig = psbt_copy.inputs[i].final_script_sig.take();
276322
psbt.inputs[i].final_script_witness = psbt_copy.inputs[i].final_script_witness.take();
323+
psbt.inputs[i].tap_script_sigs = mem::take(&mut psbt_copy.inputs[i].tap_script_sigs);
324+
325+
if !psbt.inputs[i].tap_script_sigs.is_empty() {
326+
// BDK couldn't finalize the selected input. Try to finalize it ourselves using
327+
// the `miniscript` lib, ignoring any errors that might occur.
328+
let _ = psbt.finalize_inp_mut(&*LIBSECP256K1_CTX, i);
329+
}
277330
}
278331
}
279332
Ok(())
@@ -301,9 +354,9 @@ impl TradeWallet for MemWallet {
301354
rng: &mut dyn RngCore,
302355
) -> Result<Psbt> {
303356
let mut res: Vec<(ScriptBuf, Amount)> = trade_fee_receivers
304-
.iter()
305-
.map(|receiver| (receiver.address.script_pubkey(), deposit_amount))
306-
.collect();
357+
.iter()
358+
.map(|receiver| (receiver.address.script_pubkey(), deposit_amount))
359+
.collect();
307360
res.push((half_deposit_placeholder_spk(rng), deposit_amount));
308361
let mut psbt = self.create_psbt(res, fee_rate)?;
309362
// Calculate tx fee overpay unconditionally, as this performs additional checks on the PSBT:
@@ -348,6 +401,7 @@ impl Redact for psbt::Input {
348401
fn redact_sensitive_fields(&mut self) {
349402
self.tap_key_origins.clear();
350403
self.tap_scripts.clear();
404+
self.tap_script_sigs.clear();
351405
self.tap_internal_key = None;
352406
self.tap_merkle_root = None;
353407
}
@@ -463,7 +517,7 @@ pub(crate) fn merge_psbt_halves(
463517
fn re<T: Clone>(dest: &mut Vec<T>, src: &[T]) -> Vec<T> {
464518
let mut cloned_src = Vec::with_capacity(src.len() + dest.len());
465519
cloned_src.extend(src.iter().cloned());
466-
std::mem::replace(dest, cloned_src)
520+
mem::replace(dest, cloned_src)
467521
}
468522
use std::convert::identity as id;
469523

@@ -544,6 +598,17 @@ pub(crate) fn set_payouts_and_shuffle(psbt: &mut Psbt, buyer_payout: &mut TxOutp
544598
[buyer_payout.outpoint.txid, seller_payout.outpoint.txid] = [txid; 2];
545599
}
546600

601+
pub(crate) fn extract_signed_tx(psbt: &Psbt) -> Result<Transaction> {
602+
if !is_well_formed(psbt) || psbt.inputs.iter().any(|input| input.final_script_sig.is_some()) {
603+
return Err(TransactionErrorKind::InvalidPsbt);
604+
}
605+
if psbt.inputs.iter().any(|input| input.final_script_witness.is_none()) {
606+
return Err(TransactionErrorKind::MissingSignature);
607+
}
608+
// TODO: Report undocumented panics in `Psbt::extract_tx` & `Psbt::fee` if the PSBT is malformed.
609+
Ok(psbt.clone().extract_tx()?)
610+
}
611+
547612
#[cfg(test)]
548613
mod tests {
549614
use bdk_wallet::psbt::PsbtUtils as _;

0 commit comments

Comments
 (0)