depolymerization

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

commit 2e04d71b925c02e6a2d33c82cd248492b4ac5967
parent af006fa67623989f3604711e33804b8273b047ec
Author: Antoine A <>
Date:   Wed, 17 Nov 2021 18:20:10 +0100

Add refund and revert parallel instrumentation test as it introduce spurious conflict error

Diffstat:
Mresearch.md | 33+++++++++++++++++++++------------
Msrc/bin/test.rs | 263+++++++++++++++++++++++++++++++------------------------------------------------
Msrc/lib.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
3 files changed, 187 insertions(+), 191 deletions(-)

diff --git a/research.md b/research.md @@ -103,11 +103,32 @@ TODO: ### Refunds +There is no reliable way to know how send a transaction + +#### Cheapest and least reliable way + +Send back to unspent transaction (there may be none and the user may not control +all of them) + +#### Better but still limited + +Get the transaction address from every input and send back to them Require to +keep a transaction index -txindex + To refund we could send bitcoins back to the sender address (minus some fee of course). However some people use wallet solutions using a shared address for multiple users, in that case the refund will go to the solution but not to the designated user. +- get raw transaction details +- remove refund fee from sent amount +- send an equal part to each remaining addresses making them pay transactions + fees +- ignore insufficient_funds + +RPC error code : +https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h + Do we care ? ### Sources @@ -124,18 +145,6 @@ Bitcoin Metadata. Journal of Grid Computing. 10.1007/s10723-019-09473-3. OP_RETURN test -## Refund - -- get raw transaction details -- remove refund fee from sent amount -- filter out fake addresses and self address (the onde in 'details') -- send an equal part to each remaining addresses making them pay transactions - fees -- ignore insufficient_funds - -RPC error code : -https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h - ## Ethereum - A transaction is from one address to another diff --git a/src/bin/test.rs b/src/bin/test.rs @@ -1,10 +1,5 @@ use core::panic; -use std::{ - collections::HashSet, - panic::AssertUnwindSafe, - sync::{Arc, Condvar, Mutex}, - thread::JoinHandle, -}; +use std::{collections::HashSet, panic::AssertUnwindSafe}; use bitcoincore_rpc::{ bitcoin::{Address, Amount, Txid}, @@ -12,6 +7,7 @@ use bitcoincore_rpc::{ Client, RpcApi, }; use depolymerization::{ + refund, rpc::{common_rpc, dirty_guess_network, network_dir_path, wallet_rpc, Network, CLIENT, WIRE}, utils::rand_key, ClientExtended, @@ -20,7 +16,7 @@ use owo_colors::OwoColorize; /// Instrumentation tes pub fn main() { - let test_amount = Amount::from_sat(420); + let test_amount = Amount::from_sat(1000); // Network check let network = dirty_guess_network(); @@ -106,56 +102,77 @@ pub fn main() { ); } - let mut runner = TestRunner::new(network, client_rpc, wire_rpc, client_addr, wire_addr); - runner.run_test( - "OpReturn metadata", - move |miner, client_rpc, wire_rpc, client_addr, wire_addr| { - // Send metadata - let msg = "J'aime le chocolat".as_bytes(); - let id = client_rpc - .send_op_return(&wire_addr, test_amount, msg) + let mut runner = TestRunner::new(); + runner.test("OpReturn metadata", || { + // Send metadata + let msg = "J'aime le chocolat".as_bytes(); + let id = client_rpc + .send_op_return(&wire_addr, test_amount, msg) + .unwrap(); + // Check in mempool + assert!( + tx_exist(&client_rpc, &id, 0, Category::Send).unwrap(), + "Not in mempool" + ); + // Check mined + next_block(network, &client_rpc, &client_addr); + assert!( + tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), + "Not mined" + ); + // Check extract + let (_, extracted) = wire_rpc.get_tx_op_return(&id).unwrap(); + assert_eq!(msg, extracted, "Corrupted metadata"); + }); + runner.test("SegWit metadata", || { + // Send metadata + let key = rand_key(); + let id = client_rpc + .send_segwit_key(&wire_addr, test_amount, &key) + .unwrap(); + // Check in mempool + assert!( + tx_exist(&client_rpc, &id, 0, Category::Send).unwrap(), + "Not in mempool" + ); + // Check mined + next_block(network, &client_rpc, &client_addr); + assert!( + tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), + "Not mined" + ); + // Check extract + 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 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(); - // Check in mempool - assert!( - tx_exist(&client_rpc, &id, 0, Category::Send).unwrap(), - "Not in mempool" - ); - // Check mined - miner.next_block(network, &client_rpc, &client_addr); - assert!( - tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), - "Not mined" - ); - // Check extract - let (_, extracted) = wire_rpc.get_tx_op_return(&id).unwrap(); - assert_eq!(msg, extracted, "Corrupted metadata"); - }, - ); - - runner.run_test( - "SegWit metadata", - move |miner, client_rpc, wire_rpc, client_addr, wire_addr| { - // Send metadata - let key = rand_key(); - let id = client_rpc - .send_segwit_key(&wire_addr, test_amount, &key) + let refund = client_rpc + .get_transaction(&refund_id, None) + .unwrap() + .amount + .abs() + .to_unsigned() .unwrap(); - // Check in mempool - assert!( - tx_exist(&client_rpc, &id, 0, Category::Send).unwrap(), - "Not in mempool" - ); - // Check mined - miner.next_block(network, &client_rpc, &client_addr); - assert!( - tx_exist(&wire_rpc, &id, 1, Category::Receive).unwrap(), - "Not mined" - ); - // Check extract - let (_, extracted) = wire_rpc.get_tx_segwit_key(&id).unwrap(); - assert_eq!(key, extracted, "Corrupted metadata"); - }, - ); + assert!(refund <= test_amount - refund_fee); + assert!((fee + refund + refund_fee - test_amount) < Amount::from_sat(200)); + return; + } + }); + // TODO refund minimal amount + // TODO refund send_many runner.conclude(); } @@ -176,130 +193,54 @@ fn tx_exist( Ok(found) } -/// Listen to block allowing multiple test to wait concurrently for new blocks -struct Miner { - height: Mutex<usize>, - cond: Condvar, -} - -impl Miner { - fn new(network: Network) -> Arc<Self> { - let miner = Arc::new(Self { - height: Mutex::new(0), - cond: Condvar::new(), - }); - let clone = miner.clone(); - std::thread::spawn(move || { - let rpc = common_rpc(network).unwrap(); - loop { - rpc.wait_for_new_block(0).ok(); - { - let mut locked = clone.height.lock().unwrap(); - *locked += 1; - } - clone.cond.notify_all(); - } - }); - return miner; - } - - /// Wait for a new block to be mined - fn next_block(&self, network: Network, rpc: &Client, address: &Address) { - match network { - Network::RegTest => { - // Manually mine a block - rpc.generate_to_address(1, address).unwrap(); - } - _ => { - // Wait for the next block - let mut height = self.height.lock().unwrap(); - let prev = *height; - loop { - height = self.cond.wait(height).unwrap(); - if *height > prev { - return; - } - } - } +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(); } } } -/// Rune test in parallel and track success and errors +/// Run test track success and errors struct TestRunner { - joins: Vec<JoinHandle<bool>>, - miner: Arc<Miner>, - client_rpc: Arc<Client>, - wire_rpc: Arc<Client>, - client_addr: Arc<Address>, - wire_addr: Arc<Address>, + nb_ok: usize, + nb_err: usize, } impl TestRunner { - fn new( - network: Network, - client_rpc: Client, - wire_rpc: Client, - client_addr: Address, - wire_addr: Address, - ) -> Self { + fn new() -> Self { Self { - joins: Vec::new(), - miner: Miner::new(network), - client_rpc: Arc::new(client_rpc), - wire_rpc: Arc::new(wire_rpc), - client_addr: Arc::new(client_addr), - wire_addr: Arc::new(wire_addr), + nb_err: 0, + nb_ok: 0, } } - fn run_test( - &mut self, - name: &'static str, - test: impl FnOnce(Arc<Miner>, Arc<Client>, Arc<Client>, Arc<Address>, Arc<Address>) -> () - + Send - + 'static, - ) { - let miner = self.miner.clone(); - let client_rpc = self.client_rpc.clone(); - let wire_rpc = self.wire_rpc.clone(); - let client_addr = self.client_addr.clone(); - let wire_addr = self.wire_addr.clone(); - self.joins.push(std::thread::spawn(move || { - println!("{}", format_args!("{} start", name).cyan()); - - let result = std::panic::catch_unwind(AssertUnwindSafe(move || { - test(miner, client_rpc, wire_rpc, client_addr, wire_addr) - })); - if result.is_ok() { - println!("{}", format_args!("{} OK", name).green()) - } else { - dbg!(&result); - println!("{}", format_args!("{} ERR", name).red()) - } - return result.is_ok(); - })); + fn test(&mut self, name: &str, test: impl FnOnce() -> ()) { + println!("{}", name.cyan()); + + let result = std::panic::catch_unwind(AssertUnwindSafe(test)); + if result.is_ok() { + println!("{}", "OK".green()); + self.nb_ok += 1; + } else { + dbg!(&result); + println!("{}", "ERR".red()); + self.nb_err += 1; + } } /// Wait for tests completion and print results fn conclude(self) { - let mut nb_ok = 0; - let mut nb_err = 0; - - for join in self.joins { - let result = join.join().unwrap(); - if result { - nb_ok += 1; - } else { - nb_err += 1; - } - } - println!( "Result for {} tests: {} ok and {} err", - nb_ok + nb_err, - nb_ok, - nb_err + self.nb_ok + self.nb_err, + self.nb_ok, + self.nb_err ); } } diff --git a/src/lib.rs b/src/lib.rs @@ -4,7 +4,7 @@ use bitcoincore_rpc::{ hashes::hex::{FromHex, ToHex}, Address, Amount, Network, Txid, }, - json::ScriptPubkeyType, + json::{GetTransactionResultDetailCategory as Category, ScriptPubkeyType}, jsonrpc::serde_json::{json, Value}, Client, RpcApi, }; @@ -125,7 +125,11 @@ pub fn decode_segwit_msg(segwit_addrs: &[impl AsRef<str>]) -> Result<[u8; 32], D } /// Send transaction to multiple recipients -fn send_many(client: &Client, recipients: Vec<(String, Amount)>) -> bitcoincore_rpc::Result<Txid> { +fn send_many( + client: &Client, + recipients: Vec<(String, Amount)>, + fee_from: Vec<String>, +) -> bitcoincore_rpc::Result<Txid> { let amounts = Value::Object( recipients .into_iter() @@ -135,25 +139,67 @@ fn send_many(client: &Client, recipients: Vec<(String, Amount)>) -> bitcoincore_ client.call( "sendmany", &[ - "".into(), // dummy - amounts, // amounts - 0.into(), // minconf - "".into(), // comment - Value::Null, // substractfeefrom - false.into(), // replaceable - Value::Null, // conf_target - Value::Null, // estimate mode - 1.into(), // fee rate - false.into(), // verbose + "".into(), // dummy + amounts, // amounts + 0.into(), // minconf + "".into(), // comment + fee_from.into(), // substractfeefrom + false.into(), // replaceable + Value::Null, // conf_target + Value::Null, // estimate mode + 1.into(), // fee rate + false.into(), // verbose ], ) } -pub fn refund(rpc: &Client, id: &Txid) -> bitcoincore_rpc::Result<Txid> { - let tx = rpc.get_transaction(id, None)?; - //tx.details. - //assert!(tx) - Ok(*id) +/// 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) +/// +/// 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 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); + } + + // 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)) } /// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction @@ -206,7 +252,7 @@ impl ClientExtended for Client { .into_iter() .map(|addr| (addr, segwit_min_amount())), ); - send_many(self, recipients) + send_many(self, recipients, Vec::new()) } fn get_tx_segwit_key(