From 07e5ea3fd2afae4e1b84abc57b0937c6e74ab3b0 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 5 Dec 2022 19:03:25 +0100 Subject: [PATCH] commands: fill non_witness_utxo field in PSBT inputs for createspend We are still in segwit v0, so signers need it!! --- src/commands/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++------- src/jsonrpc/mod.rs | 3 ++- tests/test_rpc.py | 13 +++++++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c38f8fc2..89247970 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -57,6 +57,7 @@ pub enum CommandError { /* out value */ bitcoin::Amount, /* target feerate */ u64, ), + FetchingTransaction(bitcoin::OutPoint), SanityCheckFailure(Psbt), UnknownSpend(bitcoin::Txid), // FIXME: when upgrading Miniscript put the actual error there @@ -82,6 +83,9 @@ impl fmt::Display for CommandError { "Cannot create a {} sat/vb transaction with input value {} and output value {}", feerate, in_val, out_val ), + Self::FetchingTransaction(op) => { + write!(f, "Could not fetch transaction for coin {}", op) + } Self::SanityCheckFailure(psbt) => write!( f, "BUG! Please report this. Failed sanity checks for PSBT '{:?}'.", @@ -283,7 +287,7 @@ impl DaemonControl { } let mut db_conn = self.db.connection(); - // Iterate through given outpoints to fetch the coins (hence checking there existence + // Iterate through given outpoints to fetch the coins (hence checking their existence // at the same time). We checked there is at least one, therefore after this loop the // list of coins is not empty. // While doing so, we record the total input value of the transaction to later compute @@ -292,12 +296,23 @@ impl DaemonControl { let mut sat_vb = 0; let mut txins = Vec::with_capacity(destinations.len()); let mut psbt_ins = Vec::with_capacity(destinations.len()); + let mut spent_txs = HashMap::with_capacity(coins_outpoints.len()); let coins = db_conn.coins_by_outpoints(coins_outpoints); for op in coins_outpoints { + // Get the coin from our in-DB unspent txos let coin = coins.get(op).ok_or(CommandError::UnknownOutpoint(*op))?; if coin.is_spent() { return Err(CommandError::AlreadySpent(*op)); } + // Fetch the transaction that created it if necessary + if !spent_txs.contains_key(op) { + let tx = self + .bitcoin + .wallet_transaction(&op.txid) + .ok_or(CommandError::FetchingTransaction(*op))?; + spent_txs.insert(*op, tx.0); + } + in_value += coin.amount; txins.push(bitcoin::TxIn { previous_output: *op, @@ -306,6 +321,7 @@ impl DaemonControl { ..bitcoin::TxIn::default() }); + // Populate the PSBT input with the information needed by signers. let coin_desc = self.derived_desc(coin); sat_vb += desc_sat_vb(&coin_desc); let witness_script = Some(coin_desc.witness_script()); @@ -313,11 +329,13 @@ impl DaemonControl { value: coin.amount.to_sat(), script_pubkey: coin_desc.script_pubkey(), }); + let non_witness_utxo = spent_txs.get(op).cloned(); let bip32_derivation = coin_desc.bip32_derivations(); psbt_ins.push(PsbtIn { witness_script, witness_utxo, bip32_derivation, + non_witness_utxo, ..PsbtIn::default() }); } @@ -728,14 +746,27 @@ mod tests { #[test] fn create_spend() { - let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new()); - let control = &ms.handle.control; - - // Arguments sanity checking let dummy_op = bitcoin::OutPoint::from_str( "3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0", ) .unwrap(); + let mut dummy_bitcoind = DummyBitcoind::new(); + dummy_bitcoind.txs.insert( + dummy_op.txid, + ( + bitcoin::Transaction { + version: 2, + lock_time: bitcoin::PackedLockTime(0), + input: vec![], + output: vec![], + }, + None, + ), + ); + let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new()); + let control = &ms.handle.control; + + // Arguments sanity checking let dummy_addr = bitcoin::Address::from_str("bc1qnsexk3gnuyayu92fc3tczvc7k62u22a22ua2kv").unwrap(); let dummy_value = 10_000; @@ -774,6 +805,7 @@ mod tests { spend_block: None, }]); let res = control.create_spend(&destinations, &[dummy_op], 1).unwrap(); + assert!(res.psbt.inputs[0].non_witness_utxo.is_some()); let tx = res.psbt.unsigned_tx; assert_eq!(tx.input.len(), 1); assert_eq!(tx.input[0].previous_output, dummy_op); @@ -844,11 +876,6 @@ mod tests { #[test] fn update_spend() { - let ms = DummyLiana::new(DummyBitcoind::new(), DummyDatabase::new()); - let control = &ms.handle.control; - let mut db_conn = control.db().lock().unwrap().connection(); - - // Add two (unconfirmed) coins in DB let dummy_op_a = bitcoin::OutPoint::from_str( "3753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:0", ) @@ -857,6 +884,22 @@ mod tests { "4753a1d74c0af8dd0a0f3b763c14faf3bd9ed03cbdf33337a074fb0e9f6c7810:1", ) .unwrap(); + let mut dummy_bitcoind = DummyBitcoind::new(); + let dummy_tx = bitcoin::Transaction { + version: 2, + lock_time: bitcoin::PackedLockTime(0), + input: vec![], + output: vec![], + }; + dummy_bitcoind + .txs + .insert(dummy_op_a.txid, (dummy_tx.clone(), None)); + dummy_bitcoind.txs.insert(dummy_op_b.txid, (dummy_tx, None)); + let ms = DummyLiana::new(dummy_bitcoind, DummyDatabase::new()); + let control = &ms.handle.control; + let mut db_conn = control.db().lock().unwrap().connection(); + + // Add two (unconfirmed) coins in DB db_conn.new_unspent_coins(&[ Coin { outpoint: dummy_op_a, diff --git a/src/jsonrpc/mod.rs b/src/jsonrpc/mod.rs index c0258d9d..24ca83be 100644 --- a/src/jsonrpc/mod.rs +++ b/src/jsonrpc/mod.rs @@ -164,7 +164,8 @@ impl From for Error { | commands::CommandError::AlreadyRescanning => { Error::new(ErrorCode::InvalidParams, e.to_string()) } - commands::CommandError::SanityCheckFailure(_) + commands::CommandError::FetchingTransaction(..) + | commands::CommandError::SanityCheckFailure(_) | commands::CommandError::RescanTrigger(..) => { Error::new(ErrorCode::InternalError, e.to_string()) } diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 66db99e5..3ba4e6f5 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -3,7 +3,11 @@ import random import time from fixtures import * -from test_framework.serializations import PSBT, PSBT_IN_PARTIAL_SIG +from test_framework.serializations import ( + PSBT, + PSBT_IN_PARTIAL_SIG, + PSBT_IN_NON_WITNESS_UTXO, +) from test_framework.utils import wait_for, COIN, RpcError, get_txid, spend_coins @@ -111,6 +115,13 @@ def test_create_spend(lianad, bitcoind): assert len(spend_psbt.o) == 4 assert len(spend_psbt.tx.vout) == 4 + # The transaction must contain the spent transaction for each input + spent_txs = [bitcoind.rpc.gettransaction(op[:64]) for op in outpoints] + for i, psbt_in in enumerate(spend_psbt.i): + assert psbt_in.map[PSBT_IN_NON_WITNESS_UTXO] == bytes.fromhex( + spent_txs[i]["hex"] + ) + # We can sign it and broadcast it. signed_psbt = lianad.sign_psbt(PSBT.from_base64(res["psbt"])) finalized_psbt = lianad.finalize_psbt(signed_psbt)