diff --git a/Cargo.toml b/Cargo.toml index 2070f37..5fa2d2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,9 @@ staking_credentials = { git = "https://github.com/civkit/staking-credentials.git reqwest = "0.11.20" base64 = "0.21.4" jsonrpc = "0.14.0" +rs_merkle = "1.4.1" +hex = "0.4.3" +bip32 = { version = "0.5.1", features = ["secp256k1"] } [build-dependencies] tonic-build = "0.9" diff --git a/src/bitcoind_client.rs b/src/bitcoind_client.rs index db04b5b..8fe43c4 100644 --- a/src/bitcoind_client.rs +++ b/src/bitcoind_client.rs @@ -23,6 +23,9 @@ use tokio::sync::Mutex as TokioMutex; use tokio::time::{sleep, Duration}; +use crate::inclusionproof::InclusionProof; +use crate::verifycommitment::{verify_merkle_root_inclusion}; + #[derive(Debug)] pub enum BitcoindRequest { CheckRpcCall, @@ -56,8 +59,8 @@ impl BitcoindClient { } - pub async fn verifytxoutproof() { - + pub async fn verifytxoutproof(mut inclusion_proof: InclusionProof) -> bool { + return verify_merkle_root_inclusion(&mut inclusion_proof); } //TODO: run and dispatch call to bitcoind diff --git a/src/config.rs b/src/config.rs index d87c3df..f83499c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,6 +47,8 @@ pub struct Mainstay { pub url: String, pub position: i32, pub token: String, + pub base_pubkey: String, + pub chain_code: String, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] @@ -84,6 +86,8 @@ impl Default for Config { url: "https://mainstay.xyz/api/v1".to_string(), position: 1, token: "14b2b754-5806-4157-883c-732baf88849c".to_string(), + base_pubkey: "031dd94c5262454986a2f0a6c557d2cbe41ec5a8131c588b9367c9310125a8a7dc".to_string(), + chain_code: "0a090f710e47968aee906804f211cf10cde9a11e14908ca0f78cc55dd190ceaa".to_string(), }, bitcoind_params: BitcoindParams { host: "https://127.0.0.1".to_string(), diff --git a/src/inclusionproof.rs b/src/inclusionproof.rs index 2dba292..d80afa6 100644 --- a/src/inclusionproof.rs +++ b/src/inclusionproof.rs @@ -11,17 +11,20 @@ use std::sync::Arc; use std::thread; use std::sync::Mutex; use tokio::time::{sleep, Duration}; -use serde_json::Value; +use serde_json::{Value, from_str, to_value}; use crate::mainstay::{get_proof}; use crate::config::Config; use crate::nostr_db::{write_new_inclusion_proof_db}; +use crate::rpcclient::{Client, Auth}; pub struct InclusionProof { pub txid: Arc>, pub commitment: Arc>, pub merkle_root: Arc>, pub ops: Arc>>, + pub txoutproof: Arc>, + pub raw_tx: Arc>, pub config: Config, } @@ -31,12 +34,14 @@ pub struct Ops { } impl InclusionProof { - pub fn new(txid: String, commitment: String, merkle_root: String, ops: Vec, our_config: Config) -> Self { + pub fn new(txid: String, commitment: String, merkle_root: String, ops: Vec, txout_proof: String, raw_tx: Value, our_config: Config) -> Self { InclusionProof { txid: Arc::new(Mutex::new(txid)), commitment: Arc::new(Mutex::new(commitment)), merkle_root: Arc::new(Mutex::new(merkle_root)), ops: Arc::new(Mutex::new(ops)), + txoutproof: Arc::new(Mutex::new(txout_proof)), + raw_tx: Arc::new(Mutex::new(raw_tx)), config: our_config, } } @@ -67,6 +72,25 @@ impl InclusionProof { Ops { append, commitment } }) .collect(); + + let client = Client::new(format!("{}:{}/", self.config.bitcoind_params.host, self.config.bitcoind_params.port).as_str(), + Auth::UserPass(self.config.bitcoind_params.rpc_user.to_string(), + self.config.bitcoind_params.rpc_password.to_string())).unwrap(); + let txid_json_value = to_value(txid).unwrap(); + let txid_json = Value::Array(vec![txid_json_value]); + if let Ok(response) = client.call("gettxoutproof", &[txid_json]) { + if let Some(raw_value) = response.result { + let mut txout_proof = raw_value.get().to_string(); + *self.txoutproof.lock().unwrap() = txout_proof; + } + } + + if let Ok(response) = client.call("getrawtransaction", &[Value::String(txid.to_string()), Value::Bool(true)]) { + if let Some(raw_value) = response.result { + let json_value: Value = from_str(raw_value.get()).unwrap(); + *self.raw_tx.lock().unwrap() = json_value; + } + } write_new_inclusion_proof_db(self).await; } }, diff --git a/src/lib.rs b/src/lib.rs index 317e43f..bc990e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,3 +113,4 @@ pub mod mainstay; pub mod inclusionproof; pub mod verifycommitment; pub mod rpcclient; +pub mod verifycommitment_test; diff --git a/src/server.rs b/src/server.rs index f8f24cd..5cb5d53 100644 --- a/src/server.rs +++ b/src/server.rs @@ -61,6 +61,7 @@ use tokio::sync::{oneshot, mpsc}; use tokio_tungstenite::WebSocketStream; use tonic::{transport::Server, Request, Response, Status}; +use serde_json::Value; //TODO: rename boarctrl to something like relayctrl ? pub mod adminctrl { @@ -441,7 +442,7 @@ fn main() -> Result<(), Box> { let service_manager_arc = Arc::new(ServiceManager::new(node_signer, anchor_manager, service_mngr_events_send, service_mngr_peer_send, manager_send_dbrequests, manager_send_bitcoind_request, send_events_gateway, config.clone())); // We initialize the inclusion proof with txid, commitment and merkle proof as empty strings. - let mut inclusion_proof = InclusionProof::new("".to_string(), "".to_string(), "".to_string(), Vec::new(), config.clone()); + let mut inclusion_proof = InclusionProof::new("".to_string(), "".to_string(), "".to_string(), Vec::new(), "".to_string(), Value::Null, config.clone()); let addr = format!("[::1]:{}", cli.cli_port).parse()?; diff --git a/src/verifycommitment.rs b/src/verifycommitment.rs index 10ee704..6ba074c 100644 --- a/src/verifycommitment.rs +++ b/src/verifycommitment.rs @@ -1,8 +1,15 @@ -use bitcoin_hashes::{sha256, Hash}; +use bitcoin_hashes::{sha256, Hash, hash160}; +use crate::inclusionproof::{InclusionProof}; +use rs_merkle::{MerkleTree, MerkleProof}; +use rs_merkle::algorithms::Sha256; +use std::str::FromStr; +use hex::{encode, decode}; +use bip32::{ExtendedPublicKey, ExtendedKeyAttrs, PublicKey, DerivationPath, ChildNumber}; +use serde_json::{from_str, Value}; -pub fn verify_commitments(event_commitments: Vec>, latest_commitment: Vec) -> bool { +pub fn verify_commitments(event_commitments: Vec>, inclusion_proof: &mut InclusionProof) -> bool { let mut concatenated_hash = Vec::new(); - + let mut latest_commitment = inclusion_proof.commitment.lock().unwrap().as_bytes().to_vec(); for event_commitment in &event_commitments { if concatenated_hash.is_empty() { concatenated_hash.extend_from_slice(&event_commitments[0]); @@ -16,3 +23,126 @@ pub fn verify_commitments(event_commitments: Vec>, latest_commitment: Ve calculated_commitment == latest_commitment } + +pub fn verify_slot_proof(slot: usize, inclusion_proof: &mut InclusionProof) -> bool { + let merkle_root = inclusion_proof.merkle_root.lock().unwrap(); + let commitment = inclusion_proof.commitment.lock().unwrap(); + let ops = inclusion_proof.ops.lock().unwrap(); + let ops_commitments: Vec<&str> = ops.iter().map(|pth| pth.commitment.as_str()).collect(); + + let leaf_hashes: Vec<[u8; 32]> = ops_commitments + .iter() + .map(|x| sha256::Hash::hash(x.as_bytes()).into_inner()) + .collect(); + + let leaf_to_prove = leaf_hashes.get(slot).unwrap(); + + let merkle_tree = MerkleTree::::from_leaves(&leaf_hashes); + let merkle_proof = merkle_tree.proof(&[slot]); + let merkle_root = merkle_tree.root().unwrap(); + + let proof_bytes = merkle_proof.to_bytes(); + + let proof = MerkleProof::::try_from(proof_bytes).unwrap(); + + return proof.verify(merkle_root, &[slot], &[*leaf_to_prove], leaf_hashes.len()); +} + +pub fn verify_merkle_root_inclusion(inclusion_proof: &mut InclusionProof) -> bool { + let script_pubkey_from_tx = &inclusion_proof.raw_tx.lock().unwrap()["vout"][0]["scriptPubKey"]["hex"].as_str().unwrap().to_string(); + let merkle_root = decode(inclusion_proof.merkle_root.lock().unwrap().as_bytes().to_vec()).unwrap(); + let initial_public_key_hex = &inclusion_proof.config.mainstay.base_pubkey; + let initial_chain_code_hex = &inclusion_proof.config.mainstay.chain_code; + + let script_pubkey = derive_script_pubkey_from_merkle_root(merkle_root, initial_public_key_hex.to_string(), initial_chain_code_hex.to_string()); + return script_pubkey == *script_pubkey_from_tx; +} + +pub fn derive_script_pubkey_from_merkle_root(merkle_root: Vec, initial_public_key_hex: String, initial_chain_code_hex: String) -> String { + let rev_merkle_root: Vec = merkle_root.iter().rev().cloned().collect(); + let rev_merkle_root_hex = encode(rev_merkle_root); + let path = get_path_from_commitment(rev_merkle_root_hex).unwrap(); + + let initial_public_key_bytes = decode(initial_public_key_hex).expect("Invalid public key hex string"); + let mut public_key_bytes = [0u8; 33]; + public_key_bytes.copy_from_slice(&initial_public_key_bytes); + + let initial_public_key = bip32::secp256k1::PublicKey::from_bytes(public_key_bytes).expect("Invalid public key"); + let mut initial_chain_code = decode(initial_chain_code_hex).expect("Invalid chain code hex string"); + let mut initial_chain_code_array = [0u8; 32]; + initial_chain_code_array.copy_from_slice(initial_chain_code.as_mut_slice()); + + let attrs = ExtendedKeyAttrs { + depth: 0, + parent_fingerprint: Default::default(), + child_number: Default::default(), + chain_code: initial_chain_code_array, + }; + + let initial_extended_pubkey = ExtendedPublicKey::new(initial_public_key, attrs); + let (child_pubkey, child_chain_code) = derive_child_key_and_chaincode(&initial_extended_pubkey, &path.to_string()); + + let script = create_1_of_1_multisig_script(child_pubkey); + + let address = bitcoin::Address::p2sh(&script, bitcoin::Network::Bitcoin).unwrap(); + let script_pubkey = encode(address.script_pubkey()); + + script_pubkey +} + +pub fn get_path_from_commitment(commitment: String) -> Option { + let path_size = 16; + let child_size = 4; + + if commitment.len() != path_size * child_size { + return None; + } + + let mut derivation_path = String::new(); + for it in 0..path_size { + let index = &commitment[it * child_size..it * child_size + child_size]; + let decoded_index = u64::from_str_radix(index, 16).unwrap(); + derivation_path.push_str(&decoded_index.to_string()); + if it < path_size - 1 { + derivation_path.push('/'); + } + } + + Some(derivation_path) +} + +fn derive_child_key_and_chaincode(mut parent: &ExtendedPublicKey, path: &str) -> (bip32::secp256k1::PublicKey, [u8; 32]) { + let mut extended_key = parent.clone(); + let mut chain_code = parent.attrs().chain_code.clone(); + let mut public_key = parent.public_key().clone(); + for step in path.split('/') { + match step { + "m" => continue, + number => { + if let Ok(index) = number.parse::() { + let new_extended_key = extended_key.derive_child(ChildNumber(index)).expect("Failed to derive child key"); + chain_code = new_extended_key.attrs().chain_code; + public_key = *new_extended_key.public_key(); + extended_key = new_extended_key.clone(); + } else { + panic!("Invalid derivation path step: {}", step); + } + } + } + } + (public_key, chain_code) +} + +fn create_1_of_1_multisig_script(pubkey: bip32::secp256k1::PublicKey) -> bitcoin::blockdata::script::Script { + let public_key = bitcoin::util::key::PublicKey { + inner: bitcoin::secp256k1::PublicKey::from_slice(&pubkey.to_bytes()).unwrap(), + compressed: true, + }; + let script = bitcoin::blockdata::script::Builder::new() + .push_opcode(bitcoin::blockdata::opcodes::all::OP_PUSHNUM_1) + .push_key(&public_key) + .push_opcode(bitcoin::blockdata::opcodes::all::OP_PUSHNUM_1) + .push_opcode(bitcoin::blockdata::opcodes::all::OP_CHECKMULTISIG) + .into_script(); + script +} diff --git a/src/verifycommitment_test.rs b/src/verifycommitment_test.rs new file mode 100644 index 0000000..7e79a6c --- /dev/null +++ b/src/verifycommitment_test.rs @@ -0,0 +1,83 @@ +use crate::util; +use std::fs; +use crate::config::Config; +use crate::inclusionproof::InclusionProof; +use crate::verifycommitment::{verify_merkle_root_inclusion}; +use bitcoin::BlockHash; +use crate::rpcclient::Client; +use serde_json::from_str; + +const tx_data: &str = r#" +{ + "txid": "b891111d35ffc72709140b7bd2a82fde20deca53831f42a96704dede42c793d2", + "hash": "b891111d35ffc72709140b7bd2a82fde20deca53831f42a96704dede42c793d2", + "version": 2, + "size": 194, + "vsize": 194, + "weight": 776, + "locktime": 0, + "vin": [ + { + "txid": "047352f01e5e3f8adc04a797311dde3917f274e55ceafb78edc39ff5d87d16c5", + "vout": 0, + "scriptSig": { + "asm": "0 30440220049d3138f841b63e96725cb9e86a53a92cd1d9e1b0740f5d4cd2ae0bcab684bf0220208d555c7e24e4c01cf67dfa9161091533e9efd6d1602bb53a49f7195c16b037[ALL] 5121036bd7943325ed9c9e1a44d98a8b5759c4bf4807df4312810ed5fc09dfb967811951ae", + "hex": "004730440220049d3138f841b63e96725cb9e86a53a92cd1d9e1b0740f5d4cd2ae0bcab684bf0220208d555c7e24e4c01cf67dfa9161091533e9efd6d1602bb53a49f7195c16b03701255121036bd7943325ed9c9e1a44d98a8b5759c4bf4807df4312810ed5fc09dfb967811951ae" + }, + "sequence": 4294967293 + } + ], + "vout": [ + { + "value": 0.01040868, + "n": 0, + "scriptPubKey": { + "asm": "OP_HASH160 29d13058087ddf2d48de404376fdcb5c4abff4bc OP_EQUAL", + "desc": "addr(35W8E71bdDhQw4ZC7uUZvXG3qhyWVYxfMB)#4rtfrxzg", + "hex": "a91429d13058087ddf2d48de404376fdcb5c4abff4bc87", + "address": "35W8E71bdDhQw4ZC7uUZvXG3qhyWVYxfMB", + "type": "scripthash" + } + } + ], + "hex": "0200000001c5167dd8f59fc3ed78fbea5ce574f21739de1d3197a704dc8a3f5e1ef0527304000000006f004730440220049d3138f841b63e96725cb9e86a53a92cd1d9e1b0740f5d4cd2ae0bcab684bf0220208d555c7e24e4c01cf67dfa9161091533e9efd6d1602bb53a49f7195c16b03701255121036bd7943325ed9c9e1a44d98a8b5759c4bf4807df4312810ed5fc09dfb967811951aefdffffff01e4e10f000000000017a91429d13058087ddf2d48de404376fdcb5c4abff4bc8700000000","blockhash":"000000000000000000036cb20420528cf0f00abb3a5716d80b5c87146b764d47", + "confirmations":15235, + "time":1690540748, + "blocktime":1690540748 +}"#; + +pub const TEST_MERKLE_ROOT: &str = "8d0ad2782d8f6e3f63c6f9611841c239630b55061d558abcc6bac53349edac70"; + +#[test] +fn test_verify_merkle_root_inclusion() { + + let data_dir = util::get_default_data_dir(); + + let config_path = data_dir.join("example-config.toml"); + + // Read the configuration file + let contents = fs::read_to_string(&config_path); + let config = match contents { + Ok(data) => { + toml::from_str(&data).expect("Could not deserialize the config file content") + }, + Err(_) => { + // If there's an error reading the file, use the default configuration + Config::default() + } + }; + + let json_value: Value = from_str(TX_DATA_RES).unwrap(); + let mut inclusion_proof = InclusionProof::new( + "".to_string(), + "".to_string(), + TEST_MERKLE_ROOT.to_string(), + Vec::new(), + "".to_string(), + json_value, + config.clone() + ); + + let result = verify_merkle_root_inclusion("b891111d35ffc72709140b7bd2a82fde20deca53831f42a96704dede42c793d2".to_string(), &mut inclusion_proof); + assert_eq!(result, true); +}