Skip to content
Closed
13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ path = "src/services/notaryd.rs"
[dependencies]
futures-channel = "0.3.28"
futures-util = "0.3.28"
lightning = { git = "https://github.com/civkit/rust-lightning.git", branch = "civkit-branch" }
lightning-net-tokio = { git = "https://github.com/civkit/rust-lightning.git", branch = "civkit-branch" }
lightning-invoice = { git = "https://github.com/civkit/rust-lightning.git", branch = "civkit-branch" }
lightning = { git = "https://github.com/DhananjayPurohit/rust-lightning-wizards.git", branch = "civkit-branch" }
lightning-net-tokio = { git = "https://github.com/DhananjayPurohit/rust-lightning-wizards.git", branch = "civkit-branch" }
lightning-invoice = { git = "https://github.com/DhananjayPurohit/rust-lightning-wizards.git", branch = "civkit-branch" }
tokio = { version = "1", features = [ "io-util", "macros", "rt", "rt-multi-thread", "sync", "net", "time" ] }
tokio-tungstenite = "0.19.0"
bitcoin = "0.29.0"
Expand All @@ -45,14 +45,19 @@ serde_json = { version = "1.0" }
toml = "0.5.8"
serde_derive = "1.0"
serde = "1.0.130"
rusqlite = "0.29.0"
rusqlite = { version = "0.29.0", features = ["bundled"] }
simplelog = "0.7.1"
dirs = "3.0.1"
log = "0.4.14"
staking_credentials = { git = "https://github.com/civkit/staking-credentials.git", branch = "main" }
reqwest = "0.11.20"
base64 = "0.21.4"
jsonrpc = "0.14.0"
rs_merkle = "1.4.1"
bitcoincore-rpc = "0.17.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove the usage of this crate. Latest branch introduce a functional RPC client, gives us more flexibility on parameters formatting.

hex = "0.4.3"
bip32 = { version = "0.5.1", features = ["secp256k1"] }


[build-dependencies]
tonic-build = "0.9"
Expand Down
13 changes: 13 additions & 0 deletions example-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,16 @@ cli_port = 50031

[logging]
level = "info"

[mainstay]
url = "http://localhost:4000"
position = 1
token = "14b2b754-5806-4157-883c-732baf88849c"
base_pubkey = "031dd94c5262454986a2f0a6c557d2cbe41ec5a8131c588b9367c9310125a8a7dc"
chain_code = "0a090f710e47968aee906804f211cf10cde9a11e14908ca0f78cc55dd190ceaa"

[bicoind]
host = "http://127.0.0.1"
port = 18443
rpc_user = "civkitd_client"
rpc_password = "hello_world"
17 changes: 16 additions & 1 deletion src/bitcoind_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use tokio::sync::Mutex as TokioMutex;

use tokio::time::{sleep, Duration};

use crate::inclusionproof::InclusionProof;
use crate::verifycommitment::{verify_commitments, verify_slot_proof, verify_merkle_root_inclusion};
use crate::nostr_db::get_hashes_of_all_events;

#[derive(Debug)]
pub enum BitcoindRequest {
CheckRpcCall,
Expand Down Expand Up @@ -47,8 +51,19 @@ impl BitcoindClient {

}

pub async fn verifytxoutproof() {
pub async fn verifytxoutproof(txid: String, slot: usize, mut inclusion_proof: InclusionProof) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where we’re out-of-sync:

verifytxoutproof is a RPC method in Bitcoin Core: https://github.com/bitcoin/bitcoin/blob/master/src/rpc/txoutproof.cpp#L123

This method takes a CMerkleBlock and then return a result if the pointed to txid has been included in the chain. CMerkleBlock contains a CPartialMerkleTree (which internally points to a txid).

Here verifytxoutproof should be just a call to self.rpc_client.call(“verifytxoutproof”, &[merkle_block]) to realize the latest step of mainstay proof verification, namely that the txid has been included in the chain.

Now in term of API, we have two options (at least):

  • a) move this verification code (commitment, slot proof, merkle root inclusion) in the client-side (i.e into civkit-sample)
  • b) have just the client with a method verifiyinclusionproof and a given txid

As the ultimate step of the verification flow is dependent on chain access and txidindex (which takes some server ressource), mobile clients will most of the time rely on a server. So I think we can have this code here, just wrapping verify_commitments / verify_slot_proof and verify_merkle_root_inclusion in a dedicated mainstay method and have this in notaryd calling back verifytxoutproof for the latest step).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to clarify what we want to do here and how.

In order to verify the merkle root inclusion in the mainstay transaction we need to get the scriptPubKey of the tx and to verify it's confirmation. The current method is that the mainstay proof just contains the merkle root (and path) and the TxID, and the verification process consists of using getrawtransaction to confirm the txid is confirmed AND get the scriptPubKey from the raw/deserialised tx returned (then verify scriptPubKey is tweaked with the merkle root).

This is the simplest approach as it only means the mainstay 'proof' object needs the TxID and merke root, however it also requires a node with txindex=1.

If we want to enable verification of proofs with a pruned node (with txindex=0) then we can use verifytxoutproof . But for this to work, we need to know the a) The full raw transaction and b) The proof generated by gettxoutproof . These would have to be included in the 'proof' object.

gettxoutproof requires txindex=1 so a a pruned node would only suffice for verification, you would need a full node for generating proofs.

Does this make sense - we should go with this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to enable verification of proofs with a pruned node (with txindex=0) then we can use verifytxoutproof . But for this to work, we need to know the a) The full raw transaction and b) The proof generated by gettxoutproof . These would have to be included in the 'proof' object.

I think we’re in sync here - Note to call verifytxoutproof you only need the merkle block generated by gettxoutproof, the full raw transaction isn’t a requirement, see the tests in rpc_txoutproof.py.

That said, of course better to use getrawtransaction if this fits the current usage of verifying the merkle root inclusion in a mainstay transaction. Note you can use getrawtransaction with the current civkitd rpc framework with Client::call().

Note my feedback was mostly on the resource trade-offs and verification responsibility between client and server. As both alternatives are requiring txindex=1, I don’t think it matters. I can see a new verifyinclusionproof with the scriptpubkey, the txid and the merkle root, the commitment and the merkle path. What is unclear to me is how the user learns the scriptpubkey, from our conversation here: #64 (comment)

I think we should go with this, even if the high-level API we should introduce for the civkit user at the client-level (i.e currently civkit-sample is unclear to me, we can address this in a follow-up PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can only learn the scriptPubKey from the raw (deserialised) tx - that's why it needs to be included in the proof if can't be retrieved with getrawtransaction .

let event_commitments = get_hashes_of_all_events().await.unwrap();
if !verify_commitments(event_commitments, &mut inclusion_proof) {
return false;
}
if !verify_slot_proof(slot, &mut inclusion_proof) {
return false;
}
if !verify_merkle_root_inclusion(txid, &mut inclusion_proof) {
return false;
}

true
}

//TODO: run and dispatch call to bitcoind
Expand Down
10 changes: 7 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ pub struct Logging {

#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
pub struct Mainstay {
pub url: String,
pub position: i32,
pub token: String,
pub url: String,
pub position: u64,
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
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;
172 changes: 169 additions & 3 deletions src/verifycommitment.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
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 bitcoincore_rpc::{Auth, Client, RpcApi};
use bitcoincore_rpc::bitcoin::Txid;
use std::str::FromStr;
use hex::{encode, decode};
use bip32::{ExtendedPublicKey, ExtendedKeyAttrs, PublicKey, DerivationPath, ChildNumber};
use crate::verifycommitment_test::{MockClient, test_merkle_root};

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 +25,160 @@ 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(txid: String, inclusion_proof: &mut InclusionProof) -> bool {
if cfg!(test) {
let client = MockClient::new();

match client.get_raw_transaction_info(&Txid::from_str(&txid).unwrap(), None) {
Ok(transaction) => {
let script_pubkey_from_tx = encode(&transaction.vout[0].script_pub_key.hex);
let merkle_root = decode(test_merkle_root).expect("Invalid merkle root hex string");
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;
}
Err(error) => {
println!("Error: {:?}", error);
}
}
} else {
let client = Client::new(format!("{}:{}/", inclusion_proof.config.bitcoind_params.host, inclusion_proof.config.bitcoind_params.port).as_str(),
Auth::UserPass(inclusion_proof.config.bitcoind_params.rpc_user.to_string(),
inclusion_proof.config.bitcoind_params.rpc_password.to_string())).unwrap();

match client.get_raw_transaction_info(&Txid::from_str(&txid).unwrap(), None) {
Ok(transaction) => {
let script_pubkey_from_tx = encode(&transaction.vout[0].script_pub_key.hex);
let merkle_root = inclusion_proof.merkle_root.lock().unwrap().as_bytes().to_vec();
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;
}
Err(error) => {
println!("Error: {:?}", error);
}
}
};

return false;
}

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
}
93 changes: 93 additions & 0 deletions src/verifycommitment_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use crate::util;
use std::fs;
use crate::config::Config;
use crate::inclusionproof::InclusionProof;
use crate::verifycommitment::{verify_merkle_root_inclusion};
use bitcoincore_rpc::bitcoin::Txid;
use bitcoin::BlockHash;
use bitcoincore_rpc::Client;
use bitcoincore_rpc::json::{GetRawTransactionResult};
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";

pub struct MockClient {}

impl MockClient {
pub fn new() -> Self {
MockClient {}
}
pub fn get_raw_transaction_info(&self, txid: &Txid, blockhash: Option<&BlockHash>) -> Result<GetRawTransactionResult, Box<dyn std::error::Error>> {
let tx_info: GetRawTransactionResult = from_str(tx_data)?;
Ok(tx_info)
}
}

#[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 mut inclusion_proof = InclusionProof::new(
"".to_string(),
"".to_string(),
"".to_string(),
Vec::new(),
config.clone()
);

let result = verify_merkle_root_inclusion("b891111d35ffc72709140b7bd2a82fde20deca53831f42a96704dede42c793d2".to_string(), &mut inclusion_proof);
assert_eq!(result, true);
}