depolymerization

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

commit d576e6948658a54ef2612846ab2f39d094fd9ae2
parent 80c10274b5cc5e2f16c1e92ba0de930eacbaa2fb
Author: Antoine A <>
Date:   Mon, 15 Nov 2021 17:21:59 +0100

Add OP_RETURN metadata support and start structuring like a library

Diffstat:
MCargo.lock | 5+++--
MCargo.toml | 2++
Mbenches/metadata.rs | 4++--
Mresearch.md | 2+-
Msrc/lib.rs | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/main.rs | 111++++++++++++++++++++++---------------------------------------------------------
Asrc/rpc_patch.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 275 insertions(+), 97 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -294,6 +294,7 @@ dependencies = [ "fastrand", "rand", "rustyline", + "serde", ] [[package]] @@ -800,9 +801,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml @@ -18,6 +18,8 @@ bech32 = "0.8.1" rand = { version = "0.8.4", features = ["getrandom"] } # Fast unsecure random fastrand = "1.5.0" +# Serialization library +serde = { version = "1.0.130", features = ["derive"] } [dev-dependencies] # statistics-driven micro-benchmarks diff --git a/benches/metadata.rs b/benches/metadata.rs @@ -1,6 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; use depolymerization::{ - decode_segwit_msg, encode_segwit_msg, + decode_segwit_msg, encode_segwit_key, utils::{rand_addresses, rand_key}, Network, }; @@ -10,7 +10,7 @@ fn criterion_benchmark(c: &mut Criterion) { group.bench_function("encode", |b| { b.iter_batched( rand_key, - |key| encode_segwit_msg(Network::MainNet, &key), + |key| encode_segwit_key(Network::MainNet, &key), criterion::BatchSize::SmallInput, ); }); diff --git a/research.md b/research.md @@ -117,7 +117,7 @@ Bitcoin Metadata. Journal of Grid Computing. 10.1007/s10723-019-09473-3. ## Out metadata with OP_RETURN -- `generaterawtransaction` with one output and a data +- `createrawtransaction` with one output and a data - `fundrawtransaction` for automatic correctness - `signrawtransactionwithwallet` - `sendrawtransaction` diff --git a/src/lib.rs b/src/lib.rs @@ -1,12 +1,15 @@ use bech32::{u5, FromBase32, ToBase32, Variant}; -use bitcoincore_rpc::bitcoin::Amount; +use bitcoincore_rpc::{Client, RpcApi, bitcoin::{Address, Amount, Txid, hashes::hex::{FromHex, ToHex}}, json::ScriptPubkeyType, jsonrpc::serde_json::{json, Value}}; use rand::{rngs::OsRng, RngCore}; +use rpc_patch::{ClientPatched, GetTransactionFull}; +mod rpc_patch; + +/// Bitcoin networks #[derive(Debug, Clone, Copy)] pub enum Network { MainNet, TestNet, - RegTest, } @@ -18,7 +21,7 @@ impl Network { Network::RegTest => "bcrt", } } - + pub fn dir(&self) -> &'static str { match self { Network::MainNet => "mainnet", @@ -28,18 +31,21 @@ impl Network { } } -pub fn segwit_min_amount() -> Amount { +/// Minimum dust amount to perform a transaction to a segwit address +fn segwit_min_amount() -> Amount { // https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp return Amount::from_sat(294); } -pub fn encode_segwit_addr(network: Network, data: &[u8; 20]) -> String { +/// Encode metadata into a segwit address +fn encode_segwit_addr(network: Network, metada: &[u8; 20]) -> String { // We use the version 0 with bech32 encoding let mut buf = vec![u5::try_from_u8(0).unwrap()]; - buf.extend_from_slice(&data.to_base32()); + buf.extend_from_slice(&metada.to_base32()); bech32::encode(network.segwit_hrp(), buf, Variant::Bech32).unwrap() } +/// Encode half of a 32B key into a segwit address fn encode_segwit_key_half( network: Network, is_first: bool, @@ -60,7 +66,8 @@ fn encode_segwit_key_half( encode_segwit_addr(network, &buf) } -pub fn encode_segwit_msg(network: Network, msg: &[u8; 32]) -> [String; 2] { +/// Encode a 32B key into two segwit adresses +pub fn encode_segwit_key(network: Network, msg: &[u8; 32]) -> [String; 2] { // Generate a random magic identifier let mut magic_id = [0; 4]; OsRng.fill_bytes(&mut magic_id); @@ -74,6 +81,153 @@ pub fn encode_segwit_msg(network: Network, msg: &[u8; 32]) -> [String; 2] { ] } +/// Send transaction to multiple recipients +fn send_many(client: &Client, recipients: Vec<(String, Amount)>) -> bitcoincore_rpc::Result<Txid> { + let amounts = Value::Object( + recipients + .into_iter() + .map(|(addr, amount)| (addr, amount.as_btc().into())) + .collect(), + ); + 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 + ], + ) +} + +/// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction +pub trait ClientExtended { + // TODO error handling for get functions + + /// Send a transaction with a 32B key as metadata encoded using fake segwit addresses + fn send_segwit_key( + &self, + network: Network, + to: &Address, + amount: Amount, + metadata: &[u8; 32], + ) -> bitcoincore_rpc::Result<Txid>; + + /// Get detailed information about an in-wallet transaction and it's 32B metadata key encoded using fake segwit addresses + fn get_tx_segwit_key( + &self, + id: &Txid, + ) -> bitcoincore_rpc::Result<(GetTransactionFull, [u8; 32])>; + + /// Send a transaction with metadata encoded using OP_RETURN + fn send_op_return( + &self, + to: &Address, + amount: Amount, + metadata: &[u8], + ) -> bitcoincore_rpc::Result<Txid>; + + /// Get detailed information about an in-wallet transaction and its op_return metadata + fn get_tx_op_return(&self, id: &Txid) + -> bitcoincore_rpc::Result<(GetTransactionFull, Vec<u8>)>; +} + +impl ClientExtended for Client { + fn send_segwit_key( + &self, + network: Network, + to: &Address, + amount: Amount, + metadata: &[u8; 32], + ) -> bitcoincore_rpc::Result<Txid> { + let addresses = encode_segwit_key(network, metadata); + let mut recipients = vec![(to.to_string(), amount)]; + recipients.extend( + addresses + .into_iter() + .map(|addr| (addr, segwit_min_amount())), + ); + send_many(self, recipients) + } + + fn get_tx_segwit_key( + &self, + id: &Txid, + ) -> bitcoincore_rpc::Result<(GetTransactionFull, [u8; 32])> { + let full = self.get_transaction_full(id)?; + + let addresses: Vec<String> = full + .decoded + .vout + .iter() + .filter_map(|it| { + it.script_pub_key + .address + .as_ref() + .map(|addr| addr.to_string()) + }) + .collect(); + + // TODO error handling + let metadata = decode_segwit_msg(&addresses).unwrap(); + + Ok((full, metadata)) + } + + fn send_op_return( + &self, + to: &Address, + amount: Amount, + metadata: &[u8], + ) -> bitcoincore_rpc::Result<Txid> { + assert!(metadata.len() > 0, "No medatata"); + assert!(metadata.len() <= 80, "Max 80 bytes"); + + // Create a raw transaction with the recipient and the metadata + let hex: String = self.call( + "createrawtransaction", + &[ + Value::Array(vec![]), + // Recipient + json!( + [ + {&to.to_string(): amount.as_btc()}, + {"data": metadata.to_hex()} + ]), + ], + )?; + // Let bitcoincore handle the funding logic + let funded = self.fund_raw_transaction(hex, None, None)?; + let signed = self.sign_raw_transaction_with_wallet(&funded.hex, None, None)?; + self.send_raw_transaction(&signed.hex) + } + + fn get_tx_op_return( + &self, + id: &Txid, + ) -> bitcoincore_rpc::Result<(GetTransactionFull, Vec<u8>)> { + let full = self.get_transaction_full(id)?; + + let op_return_out = full + .decoded + .vout + .iter() + .find(|it| it.script_pub_key.type_ == ScriptPubkeyType::NullData) + // TODO error handling + .unwrap(); + let hex = op_return_out.script_pub_key.asm.split_once(' ').unwrap().1; + let metadata= Vec::from_hex(hex).unwrap(); + + Ok((full, metadata)) + } +} + #[derive(Debug, Clone)] pub enum DecodeError { TooManyAddress, @@ -117,7 +271,13 @@ pub fn decode_segwit_msg(segwit_addrs: &[impl AsRef<str>]) -> Result<[u8; 32], D // Keep only the addresses with duplicated magic id let matches: Vec<&(bool, [u8; 4], [u8; 16])> = decoded .iter() - .filter(|(_, magic, _)| decoded.iter().filter(|(_, other, _)| other == magic).count() > 1) + .filter(|(_, magic, _)| { + decoded + .iter() + .filter(|(_, other, _)| other == magic) + .count() + > 1 + }) .collect(); assert_eq!(matches.len(), 2, "Magic ID collision"); @@ -129,7 +289,7 @@ pub fn decode_segwit_msg(segwit_addrs: &[impl AsRef<str>]) -> Result<[u8; 32], D } pub mod utils { - use crate::{encode_segwit_addr, encode_segwit_msg, Network}; + use crate::{encode_segwit_addr, encode_segwit_key, Network}; pub fn rand_key() -> [u8; 32] { let mut key = [0; 32]; @@ -149,7 +309,7 @@ pub mod utils { .take(2) .collect(); - let mut addresses = encode_segwit_msg(network, &key).to_vec(); + let mut addresses = encode_segwit_key(network, &key).to_vec(); addresses.append(&mut rng_address); fastrand::shuffle(&mut addresses); addresses @@ -159,7 +319,7 @@ pub mod utils { #[cfg(test)] mod test { use crate::{ - decode_segwit_msg, encode_segwit_msg, + decode_segwit_msg, encode_segwit_key, utils::{rand_addresses, rand_key}, Network, }; @@ -168,7 +328,7 @@ mod test { fn test_shuffle() { for _ in 0..1000 { let key = rand_key(); - let mut addresses = encode_segwit_msg(Network::RegTest, &key); + let mut addresses = encode_segwit_key(Network::RegTest, &key); fastrand::shuffle(&mut addresses); let decoded = decode_segwit_msg(&addresses.iter().map(|s| s.as_str()).collect::<Vec<&str>>()) diff --git a/src/main.rs b/src/main.rs @@ -1,11 +1,10 @@ -use std::{collections::HashSet, iter::repeat_with, path::PathBuf, str::FromStr}; +use std::{collections::HashSet, path::PathBuf, str::FromStr}; use bitcoincore_rpc::{ - bitcoin::{Address, Amount, Txid}, - jsonrpc::serde_json::Value, + bitcoin::{Amount, Txid}, Auth, Client, RpcApi, }; -use depolymerization::{Network, decode_segwit_msg, encode_segwit_msg, segwit_min_amount, utils::rand_key}; +use depolymerization::{utils::rand_key, ClientExtended, Network}; const CLIENT: &str = "client"; const WIRE: &str = "wire"; @@ -60,68 +59,13 @@ fn wallet_rpc(network: Network, wallet: &str) -> Client { .expect(&format!("Failed to open wallet '{}' client", wallet)) } -fn send_many(client: &Client, recipients: Vec<(String, Amount)>) -> bitcoincore_rpc::Result<Txid> { - let amounts = Value::Object( - recipients - .into_iter() - .map(|(addr, amount)| (addr, amount.as_btc().into())) - .collect(), - ); - 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 - ], - ) -} - -fn send_with_metadata( - network: Network, - rpc: &Client, - to: &Address, - amount: Amount, - metadata: &[u8], -) -> bitcoincore_rpc::Result<Txid> { - let addresses = encode_segwit_msg(network, &metadata.try_into().unwrap()); - let mut recipients = vec![(to.to_string(), amount)]; - recipients.extend( - addresses - .into_iter() - .map(|addr| (addr, segwit_min_amount())), - ); - send_many(rpc, recipients) -} - -fn last_metadata(rpc: &Client) -> bitcoincore_rpc::Result<Vec<u8>> { - let txs = rpc.list_transactions(None, None, None, None)?; - let last = txs.last().unwrap(); - - let info: Value = rpc.call( - "gettransaction", - &[last.info.txid.to_string().into(), Value::Null, true.into()], - )?; - let addresses: Vec<&str> = info["decoded"]["vout"] - .as_array() +fn last_transaction(rpc: &Client) -> bitcoincore_rpc::Result<Txid> { + Ok(rpc + .list_transactions(None, None, None, None)? + .last() .unwrap() - .into_iter() - .filter_map(|it| { - if it["value"].as_f64().unwrap() == segwit_min_amount().as_btc() { - Some(it["scriptPubKey"]["address"].as_str().unwrap()) - } else { - None - } - }) - .collect(); - Ok(decode_segwit_msg(&addresses).unwrap().to_vec()) + .info + .txid) } fn main() { @@ -178,14 +122,9 @@ fn main() { let rl = rl.readline(">> "); match rl { Ok(line) => { - send_with_metadata( - network, - &client_rpc, - &wire_addr, - Amount::from_sat(4200), - line.as_bytes(), - ) - .unwrap(); + client_rpc + .send_op_return(&wire_addr, Amount::from_sat(4200), line.as_bytes()) + .unwrap(); client_rpc.generate_to_address(1, &client_addr).unwrap(); } Err(_) => break, @@ -195,17 +134,29 @@ fn main() { println!("Start wire"); loop { wire_rpc.wait_for_new_block(60 * 60 * 1000).ok(); - let decoded = last_metadata(&wire_rpc).unwrap(); + let last = last_transaction(&wire_rpc).unwrap(); + let (_, decoded) = wire_rpc.get_tx_op_return(&last).unwrap(); println!(">> {}", String::from_utf8_lossy(&decoded)); } } else { - // Send metadata + // OP RETURN test + let msg = "J'aime le chocolat".as_bytes(); + client_rpc + .send_op_return(&wire_addr, Amount::from_sat(4200), msg) + .unwrap(); + client_rpc.generate_to_address(1, &client_addr).unwrap(); + let last = last_transaction(&wire_rpc).unwrap(); + let (_, decoded) = wire_rpc.get_tx_op_return(&last).unwrap(); + assert_eq!(&msg, &decoded.as_slice()); + + // Segwit test let key = rand_key(); - send_with_metadata(network, &client_rpc, &wire_addr, Amount::from_sat(4200), &key).unwrap(); - // Mine one block + client_rpc + .send_segwit_key(network, &wire_addr, Amount::from_sat(4200), &key) + .unwrap(); client_rpc.generate_to_address(1, &client_addr).unwrap(); - // Read metadata - let decoded = last_metadata(&wire_rpc).unwrap(); - assert_eq!(&key, &decoded); + let last = last_transaction(&wire_rpc).unwrap(); + let (_, decoded) = wire_rpc.get_tx_segwit_key(&last).unwrap(); + assert_eq!(key, decoded); } } diff --git a/src/rpc_patch.rs b/src/rpc_patch.rs @@ -0,0 +1,64 @@ +//! 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, +}; + +pub trait ClientPatched { + fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull>; +} + +impl ClientPatched for Client { + fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull> { + self.call( + "gettransaction", + &[id.to_string().into(), Value::Null, true.into()], + ) + } +} + +/// v22.0 replace "reqSigs" and "addresses" for the saner "address" +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResultVoutScriptPubKey22 { + pub asm: String, + #[serde(with = "bitcoincore_rpc::bitcoincore_rpc_json::serde_hex")] + pub hex: Vec<u8>, + #[serde(rename = "type")] + pub type_: ScriptPubkeyType, + pub address: Option<Address>, +} + +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResultVout22 { + #[serde(with = "bitcoincore_rpc::bitcoin::util::amount::serde::as_btc")] + pub value: Amount, + pub n: u32, + pub script_pub_key: GetRawTransactionResultVoutScriptPubKey22, +} + +/// 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 { + 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>, +} + +/// "gettransaction" with decoded raw transaction +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +pub struct GetTransactionFull { + #[serde(flatten)] + pub tx: GetTransactionResult, + pub decoded: TransactionDecoded, +}