mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-10-12 05:20:53 -04:00
Alice to validate Bob's PSBT for correctness
In order for the re-construction of TxLock to be meaningful, we limit `Message2` to the PSBT instead of the full struct. This is a breaking change in the network layer. The PSBT is valid if: - It has at most two outputs (we allow a change output) - One of the outputs pays the agreed upon amount to a shared output script Resolves #260.
This commit is contained in:
parent
8576894c10
commit
52b9a78de2
10 changed files with 485 additions and 73 deletions
|
@ -4,7 +4,8 @@ use crate::bitcoin::{
|
|||
};
|
||||
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use ::bitcoin::{OutPoint, TxIn, TxOut, Txid};
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use bdk::database::BatchDatabase;
|
||||
use bitcoin::Script;
|
||||
use ecdsa_fun::fun::Point;
|
||||
use miniscript::{Descriptor, DescriptorTrait};
|
||||
|
@ -18,7 +19,15 @@ pub struct TxLock {
|
|||
}
|
||||
|
||||
impl TxLock {
|
||||
pub async fn new(wallet: &Wallet, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self> {
|
||||
pub async fn new<B, D, C>(
|
||||
wallet: &Wallet<B, D, C>,
|
||||
amount: Amount,
|
||||
A: PublicKey,
|
||||
B: PublicKey,
|
||||
) -> Result<Self>
|
||||
where
|
||||
D: BatchDatabase,
|
||||
{
|
||||
let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||
let address = lock_output_descriptor
|
||||
.address(wallet.get_network())
|
||||
|
@ -32,6 +41,56 @@ impl TxLock {
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates an instance of `TxLock` from a PSBT, the public keys of the
|
||||
/// parties and the specified amount.
|
||||
///
|
||||
/// This function validates that the given PSBT does indeed pay that
|
||||
/// specified amount to a shared output.
|
||||
pub fn from_psbt(
|
||||
psbt: PartiallySignedTransaction,
|
||||
A: PublicKey,
|
||||
B: PublicKey,
|
||||
btc: Amount,
|
||||
) -> Result<Self> {
|
||||
let shared_output_candidate = match psbt.global.unsigned_tx.output.as_slice() {
|
||||
[shared_output_candidate, _] if shared_output_candidate.value == btc.as_sat() => {
|
||||
shared_output_candidate
|
||||
}
|
||||
[_, shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => {
|
||||
shared_output_candidate
|
||||
}
|
||||
// A single output is possible if Bob funds without any change necessary
|
||||
[shared_output_candidate] if shared_output_candidate.value == btc.as_sat() => {
|
||||
shared_output_candidate
|
||||
}
|
||||
[_, _] => {
|
||||
bail!("Neither of the two provided outputs pays the right amount!");
|
||||
}
|
||||
[_] => {
|
||||
bail!("The provided output does not pay the right amount!");
|
||||
}
|
||||
other => {
|
||||
let num_outputs = other.len();
|
||||
bail!(
|
||||
"PSBT has {} outputs, expected one or two. Something is fishy!",
|
||||
num_outputs
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let descriptor = build_shared_output_descriptor(A.0, B.0);
|
||||
let legit_shared_output_script = descriptor.script_pubkey();
|
||||
|
||||
if shared_output_candidate.script_pubkey != legit_shared_output_script {
|
||||
bail!("Output script is not a shared output")
|
||||
}
|
||||
|
||||
Ok(TxLock {
|
||||
inner: psbt,
|
||||
output_descriptor: descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lock_amount(&self) -> Amount {
|
||||
Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value)
|
||||
}
|
||||
|
@ -116,3 +175,84 @@ impl Watchable for TxLock {
|
|||
self.output_descriptor.script_pubkey()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_sends_good_psbt_when_reconstructing_then_succeeeds() {
|
||||
let (A, B) = alice_and_bob();
|
||||
let wallet = Wallet::new_funded(50000);
|
||||
let agreed_amount = Amount::from_sat(10000);
|
||||
|
||||
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
|
||||
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
|
||||
|
||||
result.expect("PSBT to be valid");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bob_can_fund_without_a_change_output() {
|
||||
let (A, B) = alice_and_bob();
|
||||
let fees = 610;
|
||||
let agreed_amount = Amount::from_sat(10000);
|
||||
let wallet = Wallet::new_funded(agreed_amount.as_sat() + fees);
|
||||
|
||||
let psbt = bob_make_psbt(A, B, &wallet, agreed_amount).await;
|
||||
assert_eq!(
|
||||
psbt.global.unsigned_tx.output.len(),
|
||||
1,
|
||||
"psbt should only have a single output"
|
||||
);
|
||||
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
|
||||
|
||||
result.expect("PSBT to be valid");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_is_sending_less_than_agreed_when_reconstructing_txlock_then_fails() {
|
||||
let (A, B) = alice_and_bob();
|
||||
let wallet = Wallet::new_funded(50000);
|
||||
let agreed_amount = Amount::from_sat(10000);
|
||||
|
||||
let bad_amount = Amount::from_sat(5000);
|
||||
let psbt = bob_make_psbt(A, B, &wallet, bad_amount).await;
|
||||
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
|
||||
|
||||
result.expect_err("PSBT to be invalid");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() {
|
||||
let (A, B) = alice_and_bob();
|
||||
let wallet = Wallet::new_funded(50000);
|
||||
let agreed_amount = Amount::from_sat(10000);
|
||||
|
||||
let E = eve();
|
||||
let psbt = bob_make_psbt(E, B, &wallet, agreed_amount).await;
|
||||
let result = TxLock::from_psbt(psbt, A, B, agreed_amount);
|
||||
|
||||
result.expect_err("PSBT to be invalid");
|
||||
}
|
||||
|
||||
/// Helper function that represents Bob's action of constructing the PSBT.
|
||||
///
|
||||
/// Extracting this allows us to keep the tests concise.
|
||||
async fn bob_make_psbt(
|
||||
A: PublicKey,
|
||||
B: PublicKey,
|
||||
wallet: &Wallet<(), bdk::database::MemoryDatabase, ()>,
|
||||
amount: Amount,
|
||||
) -> PartiallySignedTransaction {
|
||||
TxLock::new(&wallet, amount, A, B).await.unwrap().into()
|
||||
}
|
||||
|
||||
fn alice_and_bob() -> (PublicKey, PublicKey) {
|
||||
(PublicKey::random(), PublicKey::random())
|
||||
}
|
||||
|
||||
fn eve() -> PublicKey {
|
||||
PublicKey::random()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -348,6 +348,39 @@ impl<B, D, C> Wallet<B, D, C> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Wallet<(), bdk::database::MemoryDatabase, ()> {
|
||||
/// Creates a new, funded wallet to be used within tests.
|
||||
pub fn new_funded(amount: u64) -> Self {
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::{LocalUtxo, TransactionDetails};
|
||||
use bitcoin::OutPoint;
|
||||
use std::str::FromStr;
|
||||
use testutils::testutils;
|
||||
|
||||
let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"));
|
||||
|
||||
let mut database = MemoryDatabase::new();
|
||||
bdk::populate_test_db!(
|
||||
&mut database,
|
||||
testutils! {
|
||||
@tx ( (@external descriptors, 0) => amount ) (@confirmations 1)
|
||||
},
|
||||
Some(100)
|
||||
);
|
||||
|
||||
let wallet =
|
||||
bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap();
|
||||
|
||||
Self {
|
||||
client: Arc::new(Mutex::new(())),
|
||||
wallet: Arc::new(Mutex::new(wallet)),
|
||||
finality_confirmations: 1,
|
||||
network: Network::Regtest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a watchable transaction.
|
||||
///
|
||||
/// For a transaction to be watchable, we need to know two things: Its
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue