Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions src/bitcoind_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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(),
Expand Down
28 changes: 26 additions & 2 deletions src/inclusionproof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<String>>,
pub commitment: Arc<Mutex<String>>,
pub merkle_root: Arc<Mutex<String>>,
pub ops: Arc<Mutex<Vec<Ops>>>,
pub txoutproof: Arc<Mutex<String>>,
pub raw_tx: Arc<Mutex<Value>>,
pub config: Config,
}

Expand All @@ -31,12 +34,14 @@ pub struct Ops {
}

impl InclusionProof {
pub fn new(txid: String, commitment: String, merkle_root: String, ops: Vec<Ops>, our_config: Config) -> Self {
pub fn new(txid: String, commitment: String, merkle_root: String, ops: Vec<Ops>, 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,
}
}
Expand Down Expand Up @@ -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;
}
},
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,4 @@ pub mod mainstay;
pub mod inclusionproof;
pub mod verifycommitment;
pub mod rpcclient;
pub mod verifycommitment_test;
3 changes: 2 additions & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -441,7 +442,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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()?;

Expand Down
136 changes: 133 additions & 3 deletions src/verifycommitment.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>, latest_commitment: Vec<u8>) -> bool {
pub fn verify_commitments(event_commitments: Vec<Vec<u8>>, 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]);
Expand All @@ -16,3 +23,126 @@ pub fn verify_commitments(event_commitments: Vec<Vec<u8>>, 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::<Sha256>::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::<Sha256>::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<u8>, initial_public_key_hex: String, initial_chain_code_hex: String) -> String {
let rev_merkle_root: Vec<u8> = 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<String> {
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<bip32::secp256k1::PublicKey>, 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::<u32>() {
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
}
83 changes: 83 additions & 0 deletions src/verifycommitment_test.rs
Original file line number Diff line number Diff line change
@@ -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);
}