depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

commit 4b9821373b8f2ddf3d91fffbaf2d7ac08e0afef6
parent 8f7c61d1dd3c5116ad69f73a57fa54a52254f37f
Author: Antoine A <>
Date:   Mon, 22 Nov 2021 16:25:50 +0100

Make refund work with complex transactions

Diffstat:
Msrc/bin/test.rs | 204++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/lib.rs | 40++++++++++++----------------------------
Msrc/rpc_patch.rs | 32++++++++++++++++++++++++++------
3 files changed, 198 insertions(+), 78 deletions(-)

diff --git a/src/bin/test.rs b/src/bin/test.rs @@ -1,9 +1,10 @@ use core::panic; -use std::{collections::HashSet, panic::AssertUnwindSafe}; +use std::{collections::HashSet, iter::repeat_with, panic::AssertUnwindSafe}; use bitcoincore_rpc::{ bitcoin::{Address, Amount, Txid}, json::GetTransactionResultDetailCategory as Category, + jsonrpc::serde_json::{json, Value}, Client, RpcApi, }; use depolymerization::{ @@ -14,9 +15,11 @@ use depolymerization::{ }; use owo_colors::OwoColorize; +const RESERVE: &str = "reserve"; + /// Instrumentation test pub fn main() { - let test_amount = Amount::from_sat(1000); + let test_amount = Amount::from_sat(1500); // Network check let network = dirty_guess_network(); @@ -38,27 +41,77 @@ pub fn main() { .collect(); let rpc = common_rpc(network).expect("Failed to open common client"); - if !existing_wallets.contains(CLIENT) || !existing_wallets.contains(WIRE) { + if !existing_wallets.contains(CLIENT) + || !existing_wallets.contains(WIRE) + || !existing_wallets.contains(RESERVE) + { println!("Generate tests wallets"); // Create wallets - rpc.create_wallet(&WIRE, None, None, None, None).unwrap(); - rpc.create_wallet(&CLIENT, None, None, None, None).unwrap(); + rpc.create_wallet(&WIRE, None, None, None, None).ok(); + rpc.create_wallet(&CLIENT, None, None, None, None).ok(); + rpc.create_wallet(&RESERVE, None, None, None, None).ok(); } // Load wallets + rpc.load_wallet(&WIRE).ok(); rpc.load_wallet(&CLIENT).ok(); + rpc.load_wallet(&RESERVE).ok(); } // Client initialization let client_rpc = wallet_rpc(network, CLIENT); let wire_rpc = wallet_rpc(network, WIRE); + let reserve_rpc = wallet_rpc(network, RESERVE); let client_addr = client_rpc.get_new_address(None, None).unwrap(); let wire_addr = wire_rpc.get_new_address(None, None).unwrap(); + let reserve_addr = reserve_rpc.get_new_address(None, None).unwrap(); + + let next_block = || { + match network { + Network::RegTest => { + // Manually mine a block + reserve_rpc.generate_to_address(1, &reserve_addr).unwrap(); + } + _ => { + // Wait for next network block + reserve_rpc.wait_for_new_block(0).ok(); + } + } + }; // Balance check { - // TODO transfer all wire money to client + // Transfer all wire money to client + let wire_balance = wire_rpc.get_balance(None, None).unwrap(); + wire_rpc + .send_to_address( + &client_addr, + wire_balance, + None, + None, + Some(true), + None, + None, + None, + ) + .ok(); + // Transfer all wire money to client + let reserve_balance = reserve_rpc.get_balance(None, None).unwrap(); + reserve_rpc + .send_to_address( + &client_addr, + reserve_balance, + None, + None, + Some(true), + None, + None, + None, + ) + .ok(); + next_block(); + let balance = client_rpc.get_balance(None, None).unwrap(); let min_balance = test_amount * 3; if balance < min_balance { @@ -116,7 +169,7 @@ pub fn main() { "Not in mempool" ); // Check mined - next_block(network, &client_rpc, &client_addr); + next_block(); assert!( tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), "Not mined" @@ -137,7 +190,7 @@ pub fn main() { "Not in mempool" ); // Check mined - next_block(network, &client_rpc, &client_addr); + next_block(); assert!( tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), "Not mined" @@ -146,34 +199,110 @@ pub fn main() { let (_, extracted) = wire_rpc.get_tx_segwit_key(&id).unwrap(); assert_eq!(key, extracted, "Corrupted metadata"); }); - runner.test("Refund simple", || loop { - let refund_fee = Amount::from_sat(300); + + let refund_fee = Amount::from_sat(300); + runner.test("Refund simple", || { + let before = client_rpc.get_balance(None, None).unwrap(); let send_id = client_rpc .send_to_address(&wire_addr, test_amount, None, None, None, None, None, None) .unwrap(); - next_block(network, &client_rpc, &client_addr); - if let Some(refund_id) = refund(&wire_rpc, &send_id, refund_fee).unwrap() { - next_block(network, &client_rpc, &client_addr); - let fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0] - .fee - .unwrap() - .abs() - .to_unsigned() - .unwrap(); - let refund = client_rpc - .get_transaction(&refund_id, None) - .unwrap() - .amount - .abs() - .to_unsigned() - .unwrap(); - assert!(refund <= test_amount - refund_fee); - assert!((fee + refund + refund_fee - test_amount) < Amount::from_sat(200)); - return; - } + next_block(); + let refund_id = refund(&wire_rpc, &send_id, refund_fee).unwrap(); + next_block(); + let refund_tx_fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + let after = client_rpc.get_balance(None, None).unwrap(); + assert_eq!(before - after, refund_tx_fee + refund_fee + send_tx_fee); + }); + runner.test("Refund complex", || { + // Generate 6 new addresses + let addresses: Vec<Address> = + repeat_with(|| client_rpc.get_new_address(None, None).unwrap()) + .take(6) + .collect(); + // Send transaction to self with 1, 2 and 3 outputs + let txs: Vec<Txid> = [&addresses[0..1], &addresses[1..3], &addresses[3..]] + .into_iter() + .map(|addresses| { + let hex: String = client_rpc + .call( + "createrawtransaction", + &[ + Value::Array(vec![]), + Value::Array( + addresses + .iter() + .map(|addr| json!({&addr.to_string(): test_amount.as_btc()})) + .collect(), + ), + ], + ) + .unwrap(); + let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap(); + let signed = client_rpc + .sign_raw_transaction_with_wallet(&funded.hex, None, None) + .unwrap(); + client_rpc.send_raw_transaction(&signed.hex).unwrap() + }) + .collect(); + next_block(); + let before = client_rpc.get_balance(None, None).unwrap(); + // Send a transaction with multiple input from multiple transaction of different outputs len + let hex: String = client_rpc + .call( + "createrawtransaction", + &[ + Value::Array( + txs.into_iter() + .enumerate() + .map(|(i, tx)| { + json!({ + "txid": tx.to_string(), + "vout": i, + }) + }) + .collect(), + ), + json!([ + {&wire_addr.to_string(): (test_amount*3).as_btc()} + ]), + ], + ) + .unwrap(); + let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap(); + let signed = client_rpc + .sign_raw_transaction_with_wallet(&funded.hex, None, None) + .unwrap(); + let send_id = client_rpc.send_raw_transaction(&signed.hex).unwrap(); + next_block(); + let refund_id = refund(&wire_rpc, &send_id, refund_fee).unwrap(); + next_block(); + let after = client_rpc.get_balance(None, None).unwrap(); + let refund_tx_fee = wire_rpc.get_transaction(&refund_id, None).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0] + .fee + .unwrap() + .abs() + .to_unsigned() + .unwrap(); + assert_eq!(before - after, refund_tx_fee + refund_fee + send_tx_fee); }); // TODO refund minimal amount - // TODO refund send_many runner.conclude(); } @@ -194,19 +323,6 @@ fn tx_exist( Ok(found) } -fn next_block(network: Network, rpc: &Client, address: &Address) { - match network { - Network::RegTest => { - // Manually mine a block - rpc.generate_to_address(1, address).unwrap(); - } - _ => { - // Wait for next network block - rpc.wait_for_new_block(0).ok(); - } - } -} - /// Run test track success and errors struct TestRunner { nb_ok: usize, diff --git a/src/lib.rs b/src/lib.rs @@ -155,52 +155,36 @@ fn send_many( /// Refund a transaction /// -/// There is no reliable way to refund a transaction as you cannot know -/// which addresses the sender has used and if it can use the refund -/// (some addresses are shared) +/// There is no reliable way to refund a transaction as you cannot know if the addresses used are shared /// /// This is not a best-effort solution but a reasonable one pub fn refund( rpc: &Client, id: &Txid, refund_fee: Amount, -) -> bitcoincore_rpc::Result<Option<Txid>> { - // TODO take +) -> bitcoincore_rpc::Result<Txid> { // TODO handle insufficient_funds error let full = rpc.get_transaction_full(id)?; let detail = &full.tx.details[0]; - - // This is our addresses - let addr = detail.address.as_ref().unwrap().to_string(); assert_eq!(detail.category, Category::Receive); - // List all addresses except self - let addresses: Vec<String> = full - .decoded - .vout - .iter() - .filter_map(|out| { - out.script_pub_key - .address - .as_ref() - .map(|addr| addr.to_string()) - .filter(|str| str != &addr) - }) - .collect(); - - if addresses.is_empty() { - // No way to known how to refund we keep the money - return Ok(None); + + // List all addresses + let mut addresses = Vec::new(); + for vin in full.decoded.vin { + if let Some(id) = &vin.txid { + let tx = rpc.get_raw_tx(id)?; + let it = tx.vout.iter().find(|it| it.n == vin.vout.unwrap()).unwrap(); + addresses.push(it.script_pub_key.address.as_ref().unwrap().to_string()); + } } - // We assume every address that is not ours are the sender's - // Split received amount (minus refund fee) equally to each addresses let split = (detail.amount.to_unsigned().unwrap() - refund_fee) / addresses.len() as u64; let recipients: Vec<(String, Amount)> = addresses.iter().map(|addr| (addr.clone(), split)).collect(); // Send refund making recipient pay the transaction fees let id = send_many(rpc, recipients, addresses)?; - Ok(Some(id)) + Ok(id) } /// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction diff --git a/src/rpc_patch.rs b/src/rpc_patch.rs @@ -1,15 +1,11 @@ //! bitcoincore-rpc does not handle all the command we need and is not compatible with bitcoincore v22.0 that we use. //! We add additional typed command with a custom trait -use bitcoincore_rpc::{ - bitcoin::{Address, Amount, Txid, Wtxid}, - json::{GetRawTransactionResultVin, GetTransactionResult, ScriptPubkeyType}, - jsonrpc::serde_json::Value, - Client, RpcApi, -}; +use bitcoincore_rpc::{Client, RpcApi, bitcoin::{Address, Amount, BlockHash, Txid, Wtxid}, json::{GetRawTransactionResultVin, GetTransactionResult, ScriptPubkeyType}, jsonrpc::serde_json::Value}; pub trait ClientPatched { fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull>; + fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22>; } impl ClientPatched for Client { @@ -19,6 +15,9 @@ impl ClientPatched for Client { &[id.to_string().into(), Value::Null, true.into()], ) } + fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22> { + self.call("getrawtransaction", &[id.to_string().into(), true.into()]) + } } /// v22.0 replace "reqSigs" and "addresses" for the saner "address" @@ -42,6 +41,27 @@ pub struct GetRawTransactionResultVout22 { pub script_pub_key: GetRawTransactionResultVoutScriptPubKey22, } +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResult22 { + #[serde(rename = "in_active_chain")] + pub in_active_chain: Option<bool>, + #[serde(with = "bitcoincore_rpc::bitcoincore_rpc_json::serde_hex")] + pub hex: Vec<u8>, + pub txid: Txid, + pub hash: Wtxid, + pub size: usize, + pub vsize: usize, + pub version: u32, + pub locktime: u32, + pub vin: Vec<GetRawTransactionResultVin>, + pub vout: Vec<GetRawTransactionResultVout22>, + pub blockhash: Option<BlockHash>, + pub confirmations: Option<u32>, + pub time: Option<usize>, + pub blocktime: Option<usize>, +} + /// Decoded raw transtion from"gettransaction" verbose does not return field already given in the simple form #[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] pub struct TransactionDecoded {