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:
| M | research.md | | | 33 | +++++++++++++++++++++------------ |
| M | src/bin/test.rs | | | 263 | +++++++++++++++++++++++++++++++------------------------------------------------ |
| M | src/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(