Skip to content

Commit

Permalink
Add listing of timelocked UTXOs in wallet
Browse files Browse the repository at this point in the history
These timelocked UTXOs come from the situation where a contract
transaction has been broadcasted.
  • Loading branch information
chris-belcher committed Oct 5, 2021
1 parent b25f5d3 commit e07b9ce
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 41 deletions.
77 changes: 60 additions & 17 deletions src/main.rs
Expand Up @@ -8,10 +8,7 @@ use std::iter::repeat;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};

use bitcoin::hashes::{
hex::ToHex,
hash160::Hash as Hash160,
};
use bitcoin::hashes::{hash160::Hash as Hash160, hex::ToHex};
use bitcoin::Amount;
use bitcoin_wallet::mnemonic;
use bitcoincore_rpc::{Auth, Client, Error, RpcApi};
Expand Down Expand Up @@ -146,10 +143,10 @@ fn display_wallet_balance(wallet_file_name: &PathBuf, long_form: Option<bool>) {
utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations));
let utxo_count = utxos.len();
let balance: Amount = utxos.iter().fold(Amount::ZERO, |acc, u| acc + u.amount);
println!("= wallet balance =");
println!("= spendable wallet balance =");
println!(
"{:16} {:24} {:8} {:<7} value",
"outpoint", "address", "swapcoin", "conf",
"outpoint", "address", "type", "conf",
);
for utxo in utxos {
let txid = utxo.txid.to_hex();
Expand All @@ -164,7 +161,11 @@ fn display_wallet_balance(wallet_file_name: &PathBuf, long_form: Option<bool>) {
if long_form { &addr } else { &addr[0..10] },
if long_form { "" } else { "...." },
if long_form { &"" } else { &addr[addr.len() - 10..addr.len()] },
if utxo.witness_script.is_some() { "yes" } else { "no" },
if utxo.witness_script.is_some() {
"swapcoin"
} else {
if utxo.descriptor.is_some() { "seed" } else { "timelock" }
},
utxo.confirmations,
utxo.amount
);
Expand Down Expand Up @@ -225,6 +226,33 @@ fn display_wallet_balance(wallet_file_name: &PathBuf, long_form: Option<bool>) {
);
}
}

let (_incoming_contract_utxos, mut outgoing_contract_utxos) =
wallet.find_live_contract_unspents(&rpc).unwrap();
if outgoing_contract_utxos.len() > 0 {
outgoing_contract_utxos.sort_by(|a, b| b.1.confirmations.cmp(&a.1.confirmations));
println!("= live timelocked contracts =");
println!(
"{:16} {:8} {:<7} {:<8} {:6}",
"outpoint", "timelock", "conf", "locked?", "value"
);
for (outgoing_swapcoin, utxo) in outgoing_contract_utxos {
let txid = utxo.txid.to_hex();
let timelock =
read_locktime_from_contract(&outgoing_swapcoin.contract_redeemscript).unwrap();
#[rustfmt::skip]
println!("{}{}{}:{} {:<8} {:<7} {:<8} {}",
if long_form { &txid } else {&txid[0..6] },
if long_form { "" } else { ".." },
if long_form { &"" } else { &txid[58..64] },
utxo.vout,
timelock,
utxo.confirmations,
if utxo.confirmations >= timelock.into() { "unlocked" } else { "locked" },
utxo.amount
);
}
}
}

fn display_wallet_keys(wallet_file_name: &PathBuf) {
Expand Down Expand Up @@ -309,7 +337,7 @@ fn run_taker(wallet_file_name: &PathBuf) {
fn recover_from_incomplete_coinswap(
wallet_file_name: &PathBuf,
hashvalue: Hash160,
dont_broadcast: bool
dont_broadcast: bool,
) {
let mut wallet = match Wallet::load_wallet_from_file(wallet_file_name) {
Ok(w) => w,
Expand Down Expand Up @@ -337,14 +365,23 @@ fn recover_from_incomplete_coinswap(
let incomplete_coinswap = incomplete_coinswap.unwrap();

for (ii, outgoing_swapcoin) in incomplete_coinswap.1.iter().enumerate() {
wallet.import_redeemscript(&rpc, &outgoing_swapcoin.1.contract_redeemscript,
wallet_sync::CoreAddressLabelType::Wallet).unwrap();
wallet
.import_redeemscript(
&rpc,
&outgoing_swapcoin.1.contract_redeemscript,
wallet_sync::CoreAddressLabelType::Wallet,
)
.unwrap();

let signed_contract_tx = outgoing_swapcoin.1.get_fully_signed_contract_tx();
if dont_broadcast {
let txhex = bitcoin::consensus::encode::serialize_hex(&signed_contract_tx);
println!("contract_tx_{} = \n{}", ii, txhex);
let accepted = rpc.test_mempool_accept(&[txhex.clone()]).unwrap().iter().any(|tma| tma.allowed);
let accepted = rpc
.test_mempool_accept(&[txhex.clone()])
.unwrap()
.iter()
.any(|tma| tma.allowed);
assert!(accepted);
} else {
let txid = rpc.send_raw_transaction(&signed_contract_tx).unwrap();
Expand Down Expand Up @@ -377,7 +414,7 @@ enum Subcommand {
/// Prints current wallet balance.
WalletBalance {
/// Whether to print entire TXIDs and addresses
long_form: Option<bool>
long_form: Option<bool>,
},

/// Dumps all information in wallet file for debugging
Expand All @@ -398,8 +435,8 @@ enum Subcommand {
/// Hashvalue as hex string which uniquely identifies the coinswap
hashvalue: Hash160,
/// Dont broadcast transactions, only output their transaction hex string
dont_broadcast: Option<bool>
}
dont_broadcast: Option<bool>,
},
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -427,9 +464,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Subcommand::CoinswapSend => {
run_taker(&args.wallet_file_name);
}
Subcommand::RecoverFromIncompleteCoinswap { hashvalue, dont_broadcast } => {
recover_from_incomplete_coinswap(&args.wallet_file_name, hashvalue,
dont_broadcast.unwrap_or(false));
Subcommand::RecoverFromIncompleteCoinswap {
hashvalue,
dont_broadcast,
} => {
recover_from_incomplete_coinswap(
&args.wallet_file_name,
hashvalue,
dont_broadcast.unwrap_or(false),
);
}
}

Expand Down
145 changes: 121 additions & 24 deletions src/wallet_sync.rs
Expand Up @@ -50,7 +50,7 @@ use rand::rngs::OsRng;
use rand::RngCore;

use crate::contracts;
use crate::contracts::{read_hashvalue_from_contract, SwapCoin};
use crate::contracts::{read_hashvalue_from_contract, read_locktime_from_contract, SwapCoin};
use crate::error::Error;

//these subroutines are coded so that as much as possible they keep all their
Expand Down Expand Up @@ -180,8 +180,14 @@ impl IncomingSwapCoin {
let sig_mine = secp.sign(&sighash, &self.my_privkey);
let sig_other = secp.sign(&sighash, &self.other_privkey.unwrap());

apply_two_signatures_to_2of2_multisig_spend(&my_pubkey, &self.other_pubkey, &sig_mine,
&sig_other, input, redeemscript);
apply_two_signatures_to_2of2_multisig_spend(
&my_pubkey,
&self.other_pubkey,
&sig_mine,
&sig_other,
input,
redeemscript,
);
Ok(())
}
}
Expand Down Expand Up @@ -236,12 +242,16 @@ impl OutgoingSwapCoin {
let sig_mine = secp.sign(&sighash, &self.my_privkey);

let mut signed_contract_tx = self.contract_tx.clone();
apply_two_signatures_to_2of2_multisig_spend(&my_pubkey, &self.other_pubkey, &sig_mine,
&self.others_contract_sig.unwrap(), &mut signed_contract_tx.input[index],
&multisig_redeemscript);
apply_two_signatures_to_2of2_multisig_spend(
&my_pubkey,
&self.other_pubkey,
&sig_mine,
&self.others_contract_sig.unwrap(),
&mut signed_contract_tx.input[index],
&multisig_redeemscript,
);
signed_contract_tx
}

}

pub trait WalletSwapCoin {
Expand Down Expand Up @@ -687,9 +697,35 @@ impl Wallet {
Ok(())
}

fn is_utxo_ours_and_spendable(&self, u: &ListUnspentResultEntry) -> bool {
fn create_contract_scriptpubkey_swapcoin_hashmap(&self) -> HashMap<Script, &OutgoingSwapCoin> {
self.outgoing_swap_coins
.values()
.map(|osc| {
(
Address::p2wsh(&osc.contract_redeemscript, NETWORK).script_pubkey(),
osc,
)
})
.collect::<HashMap<Script, &OutgoingSwapCoin>>()
}

fn is_utxo_ours_and_spendable(
&self,
u: &ListUnspentResultEntry,
contract_scriptpubkeys_outgoing_swapcoins: &HashMap<Script, &OutgoingSwapCoin>,
) -> bool {
if u.descriptor.is_none() {
return false;
let swapcoin = contract_scriptpubkeys_outgoing_swapcoins.get(&u.script_pub_key);
if swapcoin.is_none() {
return false;
}
let swapcoin = swapcoin.unwrap();
let timelock = read_locktime_from_contract(&swapcoin.contract_redeemscript);
if timelock.is_none() {
return false;
}
let timelock = timelock.unwrap();
return u.confirmations >= timelock.into();
}
let descriptor = u.descriptor.as_ref().unwrap();
if let Some(ret) = self.get_hd_path_from_descriptor(&descriptor) {
Expand Down Expand Up @@ -725,10 +761,14 @@ impl Wallet {
//https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/148
rpc.call::<Value>("lockunspent", &[Value::Bool(true)])?;

let contract_scriptpubkeys_outgoing_swapcoins =
self.create_contract_scriptpubkey_swapcoin_hashmap();
let all_unspents = rpc.list_unspent(None, None, None, None, None)?;
let utxos_to_lock = &all_unspents
.into_iter()
.filter(|u| !self.is_utxo_ours_and_spendable(u))
.filter(|u| {
!self.is_utxo_ours_and_spendable(u, &contract_scriptpubkeys_outgoing_swapcoins)
})
.map(|u| OutPoint {
txid: u.txid,
vout: u.vout,
Expand All @@ -742,12 +782,16 @@ impl Wallet {
&self,
rpc: &Client,
) -> Result<Vec<ListUnspentResultEntry>, Error> {
let contract_scriptpubkeys_outgoing_swapcoins =
self.create_contract_scriptpubkey_swapcoin_hashmap();
rpc.call::<Value>("lockunspent", &[Value::Bool(true)])
.map_err(|e| Error::Rpc(e))?;
Ok(rpc
.list_unspent(None, None, None, None, None)?
.iter()
.filter(|u| self.is_utxo_ours_and_spendable(u))
.filter(|u| {
self.is_utxo_ours_and_spendable(u, &contract_scriptpubkeys_outgoing_swapcoins)
})
.cloned()
.collect::<Vec<ListUnspentResultEntry>>())
}
Expand Down Expand Up @@ -832,6 +876,60 @@ impl Wallet {
Ok(incomplete_swapcoin_groups)
}

// live contract refers to a contract tx which has been broadcast
// i.e. where there are UTXOs protected by contract_redeemscript's that we know about
pub fn find_live_contract_unspents(
&self,
rpc: &Client,
) -> Result<
(
Vec<(&IncomingSwapCoin, ListUnspentResultEntry)>,
Vec<(&OutgoingSwapCoin, ListUnspentResultEntry)>,
),
Error,
> {
// populate hashmaps where key is contract scriptpubkey and value is the swapcoin
let contract_scriptpubkeys_incoming_swapcoins = self
.incoming_swap_coins
.values()
.map(|isc| {
(
Address::p2wsh(&isc.contract_redeemscript, NETWORK).script_pubkey(),
isc,
)
})
.collect::<HashMap<Script, &IncomingSwapCoin>>();
let contract_scriptpubkeys_outgoing_swapcoins =
self.create_contract_scriptpubkey_swapcoin_hashmap();

rpc.call::<Value>("lockunspent", &[Value::Bool(true)])
.map_err(|e| Error::Rpc(e))?;
let listunspent = rpc.list_unspent(None, None, None, None, None)?;

let (incoming_swap_coins_utxos, outgoing_swap_coins_utxos): (Vec<_>, Vec<_>) = listunspent
.iter()
.map(|u| {
(
contract_scriptpubkeys_incoming_swapcoins.get(&u.script_pub_key),
contract_scriptpubkeys_outgoing_swapcoins.get(&u.script_pub_key),
u,
)
})
.filter(|isc_osc_u| isc_osc_u.0.is_some() || isc_osc_u.1.is_some())
.partition(|isc_osc_u| isc_osc_u.0.is_some());

Ok((
incoming_swap_coins_utxos
.iter()
.map(|isc_osc_u| (*isc_osc_u.0.unwrap(), isc_osc_u.2.clone()))
.collect::<Vec<(&IncomingSwapCoin, ListUnspentResultEntry)>>(),
outgoing_swap_coins_utxos
.iter()
.map(|isc_osc_u| (*isc_osc_u.1.unwrap(), isc_osc_u.2.clone()))
.collect::<Vec<(&OutgoingSwapCoin, ListUnspentResultEntry)>>(),
))
}

// returns None if not a hd descriptor (but possibly a swapcoin (multisig) descriptor instead)
fn get_hd_path_from_descriptor<'a>(&self, descriptor: &'a str) -> Option<(&'a str, u32, i32)> {
//e.g
Expand Down Expand Up @@ -1327,19 +1425,18 @@ pub fn create_multisig_redeemscript(key1: &PublicKey, key2: &PublicKey) -> Scrip
}

fn apply_two_signatures_to_2of2_multisig_spend(
key1: &PublicKey,
key2: &PublicKey,
sig1: &Signature,
sig2: &Signature,
input: &mut TxIn,
redeemscript: &Script) {

let (sig_first, sig_second) =
if key1.serialize()[..] < key2.serialize()[..] {
(sig1, sig2)
} else {
(sig2, sig1)
};
key1: &PublicKey,
key2: &PublicKey,
sig1: &Signature,
sig2: &Signature,
input: &mut TxIn,
redeemscript: &Script,
) {
let (sig_first, sig_second) = if key1.serialize()[..] < key2.serialize()[..] {
(sig1, sig2)
} else {
(sig2, sig1)
};

input.witness.push(Vec::new()); //first is multisig dummy
input.witness.push(sig_first.serialize_der().to_vec());
Expand Down

0 comments on commit e07b9ce

Please sign in to comment.