mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-31 15:20:15 +00:00
Ensure that output of lock script is at tx-output index 0
We subscribe to transactions upon broadcast, where we use output index `0` for the subscription. In order to ensure that this subscription is guaranteed to be for the locking script (and not a change output) we now ensure that the locking script output is always at index `0` of the outputs of the transaction. We chose this solution because otherwise we would have to add more information to broadcasting a transaction. This solution is less intrusive, because the order of transaction outputs should not have any side effects and ensuring index `0` makes the whole behaviour more deterministic.
This commit is contained in:
parent
bae38a712f
commit
6694e4f4e0
@ -16,6 +16,7 @@ use reqwest::Url;
|
|||||||
use rust_decimal::prelude::*;
|
use rust_decimal::prelude::*;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use rust_decimal_macros::dec;
|
use rust_decimal_macros::dec;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -108,18 +109,6 @@ impl Wallet {
|
|||||||
Ok((txid, subscription))
|
Ok((txid, subscription))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
|
||||||
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
|
|
||||||
|
|
||||||
if !finalized {
|
|
||||||
bail!("PSBT is not finalized")
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx = signed_psbt.extract_tx();
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||||
self.get_tx(txid)
|
self.get_tx(txid)
|
||||||
.await?
|
.await?
|
||||||
@ -257,6 +246,18 @@ where
|
|||||||
C: EstimateFeeRate,
|
C: EstimateFeeRate,
|
||||||
D: BatchDatabase,
|
D: BatchDatabase,
|
||||||
{
|
{
|
||||||
|
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
||||||
|
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
|
||||||
|
|
||||||
|
if !finalized {
|
||||||
|
bail!("PSBT is not finalized")
|
||||||
|
}
|
||||||
|
|
||||||
|
let tx = signed_psbt.extract_tx();
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn balance(&self) -> Result<Amount> {
|
pub async fn balance(&self) -> Result<Amount> {
|
||||||
let balance = self
|
let balance = self
|
||||||
.wallet
|
.wallet
|
||||||
@ -293,6 +294,10 @@ where
|
|||||||
Ok(Amount::from_sat(fees))
|
Ok(Amount::from_sat(fees))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a partially signed transaction
|
||||||
|
///
|
||||||
|
/// Ensures that the address script is at output index `0`
|
||||||
|
/// for the partially signed transaction.
|
||||||
pub async fn send_to_address(
|
pub async fn send_to_address(
|
||||||
&self,
|
&self,
|
||||||
address: Address,
|
address: Address,
|
||||||
@ -301,11 +306,30 @@ where
|
|||||||
let wallet = self.wallet.lock().await;
|
let wallet = self.wallet.lock().await;
|
||||||
let client = self.client.lock().await;
|
let client = self.client.lock().await;
|
||||||
let fee_rate = client.estimate_feerate(self.target_block)?;
|
let fee_rate = client.estimate_feerate(self.target_block)?;
|
||||||
|
let script = address.script_pubkey();
|
||||||
|
|
||||||
let mut tx_builder = wallet.build_tx();
|
let mut tx_builder = wallet.build_tx();
|
||||||
tx_builder.add_recipient(address.script_pubkey(), amount.as_sat());
|
tx_builder.add_recipient(script.clone(), amount.as_sat());
|
||||||
tx_builder.fee_rate(fee_rate);
|
tx_builder.fee_rate(fee_rate);
|
||||||
let (psbt, _details) = tx_builder.finish()?;
|
let (psbt, _details) = tx_builder.finish()?;
|
||||||
|
let mut psbt: PartiallySignedTransaction = psbt;
|
||||||
|
|
||||||
|
// When subscribing to transactions we depend on the relevant script being at
|
||||||
|
// output index 0, thus we ensure the relevant output to be at index `0`.
|
||||||
|
psbt.outputs.sort_by(|a, _| {
|
||||||
|
if a.witness_script.as_ref() == Some(&script) {
|
||||||
|
Ordering::Less
|
||||||
|
} else {
|
||||||
|
Ordering::Greater
|
||||||
|
}
|
||||||
|
});
|
||||||
|
psbt.global.unsigned_tx.output.sort_by(|a, _| {
|
||||||
|
if a.script_pubkey == script {
|
||||||
|
Ordering::Less
|
||||||
|
} else {
|
||||||
|
Ordering::Greater
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(psbt)
|
Ok(psbt)
|
||||||
}
|
}
|
||||||
@ -480,7 +504,7 @@ where
|
|||||||
use bitcoin::OutPoint;
|
use bitcoin::OutPoint;
|
||||||
use testutils::testutils;
|
use testutils::testutils;
|
||||||
|
|
||||||
let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"));
|
let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)"));
|
||||||
|
|
||||||
let mut database = MemoryDatabase::new();
|
let mut database = MemoryDatabase::new();
|
||||||
bdk::populate_test_db!(
|
bdk::populate_test_db!(
|
||||||
@ -760,7 +784,7 @@ impl fmt::Display for ScriptStatus {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::bitcoin::TxLock;
|
use crate::bitcoin::{PublicKey, TxLock};
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -990,4 +1014,51 @@ mod tests {
|
|||||||
|
|
||||||
assert!(amount.as_sat() > 0);
|
assert!(amount.as_sat() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This test ensures that the relevant script output of the transaction
|
||||||
|
/// created out of the PSBT is at index 0. This is important because
|
||||||
|
/// subscriptions to the transaction are on index `0` when broadcasting the
|
||||||
|
/// transaction.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn given_amounts_with_change_outputs_when_signing_tx_then_output_index_0_is_ensured_for_script(
|
||||||
|
) {
|
||||||
|
// We don't care about fees in this test, thus use a zero fee rate
|
||||||
|
struct NoFeeRate();
|
||||||
|
impl EstimateFeeRate for NoFeeRate {
|
||||||
|
fn estimate_feerate(&self, _target_block: usize) -> Result<FeeRate> {
|
||||||
|
Ok(FeeRate::from_sat_per_vb(0.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
|
||||||
|
Ok(bitcoin::Amount::from_sat(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This value is somewhat arbitrary but the indexation problem usually occurred
|
||||||
|
// on the first or second value (i.e. 547, 548) We keep the test
|
||||||
|
// iterations relatively low because these tests are expensive.
|
||||||
|
let above_dust = 547;
|
||||||
|
let balance = 2000;
|
||||||
|
|
||||||
|
let wallet = Wallet::new_funded(balance, NoFeeRate());
|
||||||
|
|
||||||
|
// sorting is only relevant for amounts that have a change output
|
||||||
|
// if the change output is below dust it will be dropped by the BDK
|
||||||
|
for amount in above_dust..(balance - (above_dust - 1)) {
|
||||||
|
let (A, B) = (PublicKey::random(), PublicKey::random());
|
||||||
|
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let txlock_output = txlock.script_pubkey();
|
||||||
|
|
||||||
|
let tx = wallet.sign_and_finalize(txlock.into()).await.unwrap();
|
||||||
|
let tx_output = tx.output[0].script_pubkey.clone();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tx_output, txlock_output,
|
||||||
|
"Output {:?} index mismatch for amount {} and balance {}",
|
||||||
|
tx.output, amount, balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user