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:
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,
+}