From c8f3f1f14ef04e3a7f53ef23b6c487c51cb37368 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 13 Jan 2025 09:05:06 +0100 Subject: [PATCH 01/44] working musig --- host/Cargo.toml | 4 +- host/src/main.rs | 182 ++++++++++++++++++++++++++++++++++++-- methods/guest/src/main.rs | 13 ++- shared/Cargo.toml | 3 +- shared/src/lib.rs | 14 +++ 5 files changed, 206 insertions(+), 10 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index 995d61c..71d77a4 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -18,7 +18,9 @@ sha2 = "0.10.8" bitcoin_hashes = "0.14.0" k256 = { version = "0.13.3", features = ["serde"] } serde_json = "1.0.128" +musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } +secp256k1 = "0.30.0" [features] cuda = ["risc0-zkvm/cuda"] -default = [] \ No newline at end of file +default = [] diff --git a/host/src/main.rs b/host/src/main.rs index ac2254c..e7f4de4 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -14,16 +14,23 @@ use rustreexo::accumulator::stump::Stump; use std::str::FromStr; use std::time::SystemTime; -use bitcoin::consensus::{deserialize}; +use bitcoin::consensus::deserialize; use bitcoin::key::Keypair; -use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing}; -use bitcoin::{Address, BlockHash, Network, ScriptBuf, Transaction}; +use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification}; +use bitcoin::{Address, BlockHash, Network, PrivateKey, ScriptBuf, Transaction}; +use clap::builder::TypedValueParser; use k256::schnorr; use k256::schnorr::signature::Verifier; use rustreexo::accumulator::proof::Proof; use serde::{Deserialize, Serialize}; -use shared::get_leaf_hashes; +use musig2::{ + AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, + SecondRound, +}; +use k256::PublicKey; + +use shared::{get_leaf_hashes, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { let sk = SecretKey::new(&mut rand::thread_rng()); @@ -74,6 +81,18 @@ struct Args { #[arg(long)] priv_key: Option, + #[arg(long)] + node_key_1: Option, + + #[arg(long)] + node_key_2: Option, + + #[arg(long)] + bitcoin_key_1: Option, + + #[arg(long)] + bitcoin_key_2: Option, + /// Network to use. #[arg(long, default_value_t = Network::Testnet)] network: Network, @@ -91,6 +110,28 @@ struct CliStump { pub leaves: u64, } +fn extract_keypair( + secp: &Secp256k1, + priv_str: &str, + network: Network, +) -> Keypair { + let keypair = if priv_str == "new" { + gen_keypair(&secp) + } else { + let sk = SecretKey::from_str(&priv_str).unwrap(); + Keypair::from_secret_key(&secp, &sk) + }; + + let (internal_key, _parity) = keypair.x_only_public_key(); + let script_buf = ScriptBuf::new_p2tr(&secp, internal_key, None); + let addr = Address::from_script(script_buf.as_script(), network).unwrap(); + println!("priv: {}", hex::encode(keypair.secret_key().secret_bytes())); + println!("pub: {}", internal_key); + println!("address: {}", addr); + + keypair +} + fn main() { // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` tracing_subscriber::fmt() @@ -102,6 +143,36 @@ fn main() { let secp = Secp256k1::new(); let network = args.network; + + println!("node_key_1:"); + let kp_node1 = extract_keypair(&secp, args.node_key_1.unwrap().as_str(), network); + println!("node_key_2:"); + let kp_node2 = extract_keypair(&secp, args.node_key_2.unwrap().as_str(), network); + println!("bitcoin_key_1:"); + let kp_bitcoin1 = extract_keypair(&secp, args.bitcoin_key_1.unwrap().as_str(), network); + println!("bitcoin_key_2:"); + let kp_bitcoin2 = extract_keypair(&secp, args.bitcoin_key_2.unwrap().as_str(), network); + + let msg_to_sign = args.msg.unwrap(); + + let (musig_pubs, musig_sig) = create_musig( + vec![kp_node1, kp_node2, kp_bitcoin1, kp_bitcoin2], + &msg_to_sign, + ); + + + assert_eq!( + verify_musig(musig_pubs.clone(), musig_sig, &msg_to_sign), + true, + ); + + if !verify_musig(musig_pubs.clone(), musig_sig, &msg_to_sign) { + println!("musig failed"); + return; + } + + println!("musig successfully verified"); + // Generate a new keypair or use the given private key. let keypair = match args.priv_key.as_deref() { Some(priv_str) => { @@ -193,7 +264,6 @@ fn main() { } }; - let msg_to_sign = args.msg.unwrap(); let msg_bytes = msg_to_sign.as_bytes(); let digest = sha256::Hash::hash(msg_bytes); let digest_bytes = digest.to_byte_array(); @@ -286,6 +356,10 @@ fn main() { .unwrap() .write(&block_hash) .unwrap() + .write(&musig_pubs) + .unwrap() + .write(&musig_sig.as_slice()) + .unwrap() .build() .unwrap(); @@ -294,7 +368,9 @@ fn main() { // Proof information by proving the specified ELF binary. // This struct contains the receipt along with statistics about execution of the guest - let prove_info = prover.prove_with_opts(env, METHOD_ELF, &proof_type).unwrap(); + let prove_info = prover + .prove_with_opts(env, METHOD_ELF, &proof_type) + .unwrap(); println!("Proving took {:?}", start_time.elapsed().unwrap()); // extract the receipt. @@ -321,3 +397,97 @@ fn verify_receipt(receipt: &Receipt, s: &Stump) { println!("priv key hash: {}", sk_hash); println!("signed msg: {}", msg); } +fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) { + let mut pubs: Vec = Vec::new(); + + for kp in keys.clone() { + let bytes = kp.secret_key().secret_bytes(); + let pk = schnorr::SigningKey::from_bytes(&bytes).unwrap(); + + let ver_key = pk.verifying_key(); + pubs.push(ver_key.into()); + } + + + let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); + + // This is the key which the group has control over. + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + + println!("all good {:?}", aggregated_pubkey); + + let nonce_seed = [0xACu8; 32]; + + // This is how `FirstRound` derives the nonce internally. + let mut public_nonces = Vec::new(); + let mut sec_nonces = Vec::new(); + for (i, k) in pubs.iter().enumerate() { + let secnonce = SecNonce::build(nonce_seed) + // .with_seckey(scalar) + .with_pubkey(pubs[i].clone()) + .with_message(&message) + .with_aggregated_pubkey(aggregated_pubkey) + .with_extra_input(&(i as u32).to_be_bytes()) + .build(); + + sec_nonces.push(secnonce.clone()); + let our_public_nonce = secnonce.public_nonce(); + + public_nonces.push(our_public_nonce); + } + + + // We manually aggregate the nonces together and then construct our partial signature. + let aggregated_nonce: AggNonce = public_nonces.iter().sum(); + let mut partial_signatures = Vec::new(); + for (i, k) in keys.clone().iter().enumerate() { + // let sk = bitcoin::secp256k1::SecretKey::from_str(&k).unwrap(); + let b = k.secret_bytes(); + let priv_str = hex::encode(b); + let scalar: musig2::secp::Scalar = priv_str.parse().unwrap(); + + let our_partial_signature: PartialSignature = musig2::sign_partial( + &key_agg_ctx, + scalar, + sec_nonces[i].clone(), + &aggregated_nonce, + &message, + ) + .expect("error creating partial signature"); + + partial_signatures.push(our_partial_signature); + } + + + /// Signatures should be verified upon receipt and invalid signatures + /// should be blamed on the signer who sent them. + for (i, partial_signature) in partial_signatures.clone().into_iter().enumerate() { + + let their_pubkey: PublicKey = key_agg_ctx.get_pubkey(i).unwrap(); + let their_pubnonce = &public_nonces[i]; + + musig2::verify_partial( + &key_agg_ctx, + partial_signature, + &aggregated_nonce, + their_pubkey, + their_pubnonce, + &message, + ) + .expect("received invalid signature from a peer"); + } + + let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( + &key_agg_ctx, + &aggregated_nonce, + partial_signatures, + message, + ) + .expect("error aggregating signatures"); + + + musig2::verify_single(aggregated_pubkey, &final_signature, message) + .expect("aggregated signature must be valid"); + + (pubs, final_signature) +} diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index a4dca79..115c7c4 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -12,8 +12,9 @@ use bitcoin::script::{Builder, PushBytes}; use k256::schnorr; use k256::schnorr::signature::Verifier; use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::PublicKey; -use shared::get_leaf_hashes; +use shared::{get_leaf_hashes, verify_musig}; pub fn new_p2tr( internal_key: UntweakedPublicKey, @@ -72,6 +73,15 @@ fn main() { let vout: u32 = env::read(); let block_height: u32 = env::read(); let block_hash: BlockHash = env::read(); + let musig_pubs: Vec = env::read(); + let musig_sig_bytes: Vec = env::read(); + let musig_sig = schnorr::Signature::try_from(musig_sig_bytes.as_slice()).unwrap(); + + let msg = from_utf8(msg_bytes.as_slice()).unwrap(); + assert_eq!( + verify_musig(musig_pubs.clone(), musig_sig.to_bytes(), &msg), + true, + ); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); @@ -91,7 +101,6 @@ fn main() { let mut hasher = Sha512_256::new(); hasher.update(&priv_key.to_bytes()); let sk_hash = hex::encode(hasher.finalize()); - let msg = from_utf8(msg_bytes.as_slice()).unwrap(); let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 90a457b..bceefca 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] bitcoin = { version = "0.32.5"} sha2 = "0.10.8" -bitcoin_hashes = "0.14.0" \ No newline at end of file +bitcoin_hashes = "0.14.0" +musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index f1de370..c6cb3b8 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -5,6 +5,9 @@ use sha2::{Digest, Sha512_256}; use bitcoin::consensus::Encodable; use bitcoin::{BlockHash, Transaction}; +use k256::PublicKey; + +use musig2::{k256, AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, SecondRound}; pub const UTREEXO_TAG_V1: [u8; 64] = [ 0x5b, 0x83, 0x2d, 0xb8, 0xca, 0x26, 0xc2, 0x5b, 0xe1, 0xc5, 0x42, 0xd6, 0xcc, 0xed, 0xdd, 0xa8, @@ -43,3 +46,14 @@ pub fn get_leaf_hashes( .finalize(); sha256::Hash::from_slice(leaf_hash.as_slice()).expect("parent_hash: Engines shouldn't be Err") } + +pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &str) -> bool { + + let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); + let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + + musig2::verify_single(aggregated_pubkey, &sig, message) + .expect("aggregated signature must be valid"); + + true +} From f31c889b0bd0cf76c27874744207cb7251021748 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 13 Jan 2025 11:08:36 +0100 Subject: [PATCH 02/44] working musig with guest proving --- host/src/main.rs | 55 ++++++++++++++++++++++++++++++--------- methods/guest/src/main.rs | 27 +++++++++++++------ shared/src/lib.rs | 14 +++++++--- 3 files changed, 72 insertions(+), 24 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index e7f4de4..6ab9a7b 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -17,20 +17,20 @@ use std::time::SystemTime; use bitcoin::consensus::deserialize; use bitcoin::key::Keypair; use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification}; -use bitcoin::{Address, BlockHash, Network, PrivateKey, ScriptBuf, Transaction}; +use bitcoin::{Address, BlockHash, Network, PrivateKey, ScriptBuf, Transaction, XOnlyPublicKey}; use clap::builder::TypedValueParser; use k256::schnorr; use k256::schnorr::signature::Verifier; use rustreexo::accumulator::proof::Proof; use serde::{Deserialize, Serialize}; +use k256::PublicKey; use musig2::{ AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, SecondRound, }; -use k256::PublicKey; -use shared::{get_leaf_hashes, verify_musig}; +use shared::{aggregate_keys, get_leaf_hashes, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { let sk = SecretKey::new(&mut rand::thread_rng()); @@ -132,6 +132,19 @@ fn extract_keypair( keypair } +fn address(secp: &Secp256k1, pubkey: PublicKey, network: Network) { + let verifying_key = schnorr::VerifyingKey::try_from(pubkey).unwrap(); + // let ver_key = schnorr::VerifyingKey{ + // inner: pubkey, + // }; + let pubx = XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); + + let script_buf = ScriptBuf::new_p2tr(&secp, pubx, None); + let addr = Address::from_script(script_buf.as_script(), network).unwrap(); + println!("pub: {}", pubx); + println!("address: {}", addr); +} + fn main() { // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` tracing_subscriber::fmt() @@ -160,19 +173,25 @@ fn main() { &msg_to_sign, ); - assert_eq!( verify_musig(musig_pubs.clone(), musig_sig, &msg_to_sign), true, ); - if !verify_musig(musig_pubs.clone(), musig_sig, &msg_to_sign) { - println!("musig failed"); - return; - } + let (musig_pubs2, musig_sig2) = create_musig(vec![kp_bitcoin1, kp_bitcoin2], &msg_to_sign); println!("musig successfully verified"); + assert_eq!( + verify_musig(musig_pubs2.clone(), musig_sig2, &msg_to_sign), + true, + ); + + println!("musig with 2 pubs successfully verified"); + + let tap_key = aggregate_keys(musig_pubs2); + address(&secp, tap_key, network); + // Generate a new keypair or use the given private key. let keypair = match args.priv_key.as_deref() { Some(priv_str) => { @@ -296,9 +315,13 @@ fn main() { assert_eq!(lh, leaf_hash); // We will prove inclusion in the UTXO set of the key we control. - let (internal_key, _parity) = keypair.unwrap().x_only_public_key(); + //let (internal_key, _parity) = keypair.unwrap().x_only_public_key(); let priv_bytes = keypair.unwrap().secret_key().secret_bytes(); let priv_key = schnorr::SigningKey::from_bytes(&priv_bytes).unwrap(); + + let verifying_key = schnorr::VerifyingKey::try_from(tap_key).unwrap(); + let internal_key= XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); + let script_pubkey = ScriptBuf::new_p2tr(&secp, internal_key, None); assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); @@ -411,6 +434,16 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); + for kp in keys.clone() { + // let sk = bitcoin::secp256k1::SecretKey::from_str(&k).unwrap(); + let bytes = kp.secret_key().secret_bytes(); + let str = hex::encode(bytes); + println!("priv key: {}", str); + let scalar: musig2::secp::Scalar = str.parse().unwrap(); + let p = scalar.base_point_mul(); + key_agg_ctx.key_coefficient(p).unwrap(); + } + // This is the key which the group has control over. let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); @@ -436,7 +469,6 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) public_nonces.push(our_public_nonce); } - // We manually aggregate the nonces together and then construct our partial signature. let aggregated_nonce: AggNonce = public_nonces.iter().sum(); let mut partial_signatures = Vec::new(); @@ -458,11 +490,9 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) partial_signatures.push(our_partial_signature); } - /// Signatures should be verified upon receipt and invalid signatures /// should be blamed on the signer who sent them. for (i, partial_signature) in partial_signatures.clone().into_iter().enumerate() { - let their_pubkey: PublicKey = key_agg_ctx.get_pubkey(i).unwrap(); let their_pubnonce = &public_nonces[i]; @@ -485,7 +515,6 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) ) .expect("error aggregating signatures"); - musig2::verify_single(aggregated_pubkey, &final_signature, message) .expect("aggregated signature must be valid"); diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 115c7c4..4195fca 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -14,7 +14,7 @@ use k256::schnorr::signature::Verifier; use k256::elliptic_curve::sec1::ToEncodedPoint; use k256::PublicKey; -use shared::{get_leaf_hashes, verify_musig}; +use shared::{get_leaf_hashes, verify_musig, aggregate_keys}; pub fn new_p2tr( internal_key: UntweakedPublicKey, @@ -62,6 +62,13 @@ fn tap_tweak( } fn main() { + //TODO: take in nodeid1, nodeid2, bitcoinkey1, bitcoinkey2 need tweak? + // check combining bitcoin keys give a key that is in the UTXO set. + // combine all 4 keys and check that the signature is valid for the aggregate key + // How to avoid proof reuse? cannot do hash of priv key easily, since there are two nodes maybe + // do hash of the individual public keys? since they won't ever go onchain + + // read the input let msg_bytes: Vec = env::read(); let priv_key: schnorr::SigningKey = env::read(); @@ -83,13 +90,17 @@ fn main() { true, ); + // Aggregate the bitcoin keys. + let tap_pub = aggregate_keys(vec![musig_pubs[2], musig_pubs[3]]); + let ver_key = schnorr::VerifyingKey::try_from(tap_pub).unwrap(); + let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); - let internal_key = priv_key.verifying_key(); +// let internal_key = priv_key.verifying_key(); // We'll check that the given public key corresponds to an output in the utxo set. - let pubx = XOnlyPublicKey::from_slice(internal_key.to_bytes().as_slice()).unwrap(); + let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); let script_pubkey = new_p2tr(pubx, None); // assert internal key is in tx used to calc leaf hash @@ -102,11 +113,11 @@ fn main() { hasher.update(&priv_key.to_bytes()); let sk_hash = hex::encode(hasher.finalize()); - let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); - - internal_key - .verify(msg_bytes.as_slice(), &schnorr_sig) - .expect("schnorr verification failed"); +// let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); +// +// ver_key +// .verify(msg_bytes.as_slice(), &schnorr_sig) +// .expect("schnorr verification failed"); // write public output to the journal env::commit(&s); diff --git a/shared/src/lib.rs b/shared/src/lib.rs index c6cb3b8..b326ad9 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -7,7 +7,10 @@ use bitcoin::consensus::Encodable; use bitcoin::{BlockHash, Transaction}; use k256::PublicKey; -use musig2::{k256, AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, SecondRound}; +use musig2::{ + k256, AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, + SecNonceSpices, SecondRound, +}; pub const UTREEXO_TAG_V1: [u8; 64] = [ 0x5b, 0x83, 0x2d, 0xb8, 0xca, 0x26, 0xc2, 0x5b, 0xe1, 0xc5, 0x42, 0xd6, 0xcc, 0xed, 0xdd, 0xa8, @@ -47,11 +50,16 @@ pub fn get_leaf_hashes( sha256::Hash::from_slice(leaf_hash.as_slice()).expect("parent_hash: Engines shouldn't be Err") } -pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &str) -> bool { - +pub fn aggregate_keys(pubs: Vec) -> PublicKey { let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + aggregated_pubkey +} + +pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &str) -> bool { + let aggregated_pubkey: PublicKey = aggregate_keys(pubs); + musig2::verify_single(aggregated_pubkey, &sig, message) .expect("aggregated signature must be valid"); From 33995cafc68b93d27f2990c43e5978e65c2981f3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 13 Jan 2025 12:59:44 +0100 Subject: [PATCH 03/44] remove single sig, move to using pk hash as unique ID --- host/src/main.rs | 76 +-------------------------------------- methods/guest/src/main.rs | 25 ++++++------- 2 files changed, 12 insertions(+), 89 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 6ab9a7b..984a0c0 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -76,11 +76,6 @@ struct Args { #[arg(short, long)] msg: Option, - /// Sign the message using the given private key. Pass "new" to generate one at random. Leave - /// this blank if verifying a receipt. - #[arg(long)] - priv_key: Option, - #[arg(long)] node_key_1: Option, @@ -156,7 +151,6 @@ fn main() { let secp = Secp256k1::new(); let network = args.network; - println!("node_key_1:"); let kp_node1 = extract_keypair(&secp, args.node_key_1.unwrap().as_str(), network); println!("node_key_2:"); @@ -168,6 +162,7 @@ fn main() { let msg_to_sign = args.msg.unwrap(); + let (musig_pubs, musig_sig) = create_musig( vec![kp_node1, kp_node2, kp_bitcoin1, kp_bitcoin2], &msg_to_sign, @@ -192,38 +187,6 @@ fn main() { let tap_key = aggregate_keys(musig_pubs2); address(&secp, tap_key, network); - // Generate a new keypair or use the given private key. - let keypair = match args.priv_key.as_deref() { - Some(priv_str) => { - let keypair = if priv_str == "new" { - gen_keypair(&secp) - } else { - let sk = SecretKey::from_str(&priv_str).unwrap(); - Keypair::from_secret_key(&secp, &sk) - }; - - let (internal_key, _parity) = keypair.x_only_public_key(); - let script_buf = ScriptBuf::new_p2tr(&secp, internal_key, None); - let addr = Address::from_script(script_buf.as_script(), network).unwrap(); - println!("priv: {}", hex::encode(keypair.secret_key().secret_bytes())); - println!("pub: {}", internal_key); - println!("address: {}", addr); - - if priv_str == "new" { - return; - } - - Some(keypair) - } - _ => { - if args.prove { - println!("priv key needed"); - return; - } - None - } - }; - let receipt_file = if args.prove { let r = File::create(args.receipt_file.unwrap()).unwrap(); r @@ -315,10 +278,6 @@ fn main() { assert_eq!(lh, leaf_hash); // We will prove inclusion in the UTXO set of the key we control. - //let (internal_key, _parity) = keypair.unwrap().x_only_public_key(); - let priv_bytes = keypair.unwrap().secret_key().secret_bytes(); - let priv_key = schnorr::SigningKey::from_bytes(&priv_bytes).unwrap(); - let verifying_key = schnorr::VerifyingKey::try_from(tap_key).unwrap(); let internal_key= XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); @@ -331,46 +290,14 @@ fn main() { assert_eq!(acc.verify(&proof, &[leaf_hash]), Ok(true)); println!("stump proof verified"); - // Sign using the tweaked key. - let sig = secp.sign_schnorr(&msg, &keypair.unwrap()); - - // Verify signature. - let (pubkey, _) = keypair.unwrap().x_only_public_key(); - println!("pubkey: {}", pubkey); - - let sig_bytes = sig.serialize(); - println!("secp signature: {}", hex::encode(sig_bytes)); - secp.verify_schnorr(&sig, &msg, &pubkey) - .expect("secp verification failed"); - - let pub_bytes = pubkey.serialize(); - - println!("creating verifying key"); - let verifying_key = schnorr::VerifyingKey::from_bytes(&pub_bytes).unwrap(); - println!( - "created verifying key: {}", - hex::encode(verifying_key.to_bytes()) - ); - - let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); - println!("schnorr signature: {}", hex::encode(schnorr_sig.to_bytes())); - - verifying_key - .verify(msg_bytes, &schnorr_sig) - .expect("schnorr verification failed"); - let start_time = SystemTime::now(); let env = ExecutorEnv::builder() .write(&msg_bytes) .unwrap() - .write(&priv_key) - .unwrap() .write(&acc) .unwrap() .write(&proof) .unwrap() - .write(&sig_bytes.as_slice()) - .unwrap() .write(&tx) .unwrap() .write(&vout) @@ -431,7 +358,6 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) pubs.push(ver_key.into()); } - let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); for kp in keys.clone() { diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 4195fca..5a1f1e7 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -71,10 +71,8 @@ fn main() { // read the input let msg_bytes: Vec = env::read(); - let priv_key: schnorr::SigningKey = env::read(); let s: Stump = env::read(); let proof: Proof = env::read(); - let sig_bytes: Vec = env::read(); let tx: Transaction = env::read(); let vout: u32 = env::read(); @@ -82,6 +80,7 @@ fn main() { let block_hash: BlockHash = env::read(); let musig_pubs: Vec = env::read(); let musig_sig_bytes: Vec = env::read(); + let musig_sig = schnorr::Signature::try_from(musig_sig_bytes.as_slice()).unwrap(); let msg = from_utf8(msg_bytes.as_slice()).unwrap(); @@ -91,14 +90,14 @@ fn main() { ); // Aggregate the bitcoin keys. - let tap_pub = aggregate_keys(vec![musig_pubs[2], musig_pubs[3]]); + let bitcoin_key1 = musig_pubs[2]; + let bitcoin_key2 = musig_pubs[3]; + let tap_pub = aggregate_keys(vec![bitcoin_key1, bitcoin_key2]); let ver_key = schnorr::VerifyingKey::try_from(tap_pub).unwrap(); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); -// let internal_key = priv_key.verifying_key(); - // We'll check that the given public key corresponds to an output in the utxo set. let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); let script_pubkey = new_p2tr(pubx, None); @@ -109,18 +108,16 @@ fn main() { // Assert it is in the set. assert_eq!(s.verify(&proof, &[leaf_hash]), Ok(true)); - let mut hasher = Sha512_256::new(); - hasher.update(&priv_key.to_bytes()); - let sk_hash = hex::encode(hasher.finalize()); + let vk1 = schnorr::VerifyingKey::try_from(bitcoin_key1).unwrap(); + let vk2 = schnorr::VerifyingKey::try_from(bitcoin_key2).unwrap(); -// let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); -// -// ver_key -// .verify(msg_bytes.as_slice(), &schnorr_sig) -// .expect("schnorr verification failed"); + let mut hasher = Sha512_256::new(); + hasher.update(&vk1.to_bytes()); + hasher.update(&vk2.to_bytes()); + let pk_hash = hex::encode(hasher.finalize()); // write public output to the journal env::commit(&s); - env::commit(&sk_hash); + env::commit(&pk_hash); env::commit(&msg); } \ No newline at end of file From 860014f15e3d7cc91dc8adf1bbcae4ce033850f3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 15 Jan 2025 13:48:28 +0100 Subject: [PATCH 04/44] dep: use rustreexo with fixex Stump serialization --- host/Cargo.toml | 2 +- methods/guest/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index 71d77a4..c616da0 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -9,7 +9,7 @@ shared = { path = "../shared" } risc0-zkvm = { version = "1.2.0"} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" -rustreexo = { version = "0.3.0", features = ["with-serde"] } +rustreexo = { git = "https://github.com/halseth/rustreexo.git", branch = "usize-64-bit-le", features = ["with-serde"] } bitcoin = { version = "0.32.5", features = ["std", "rand-std", "serde"] } bincode = "1.3.3" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 162e9b3..656532f 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } risc0-zkvm = { version = "1.2.0", default-features = false, features = ['std'] } -rustreexo = { version = "0.3.0", features = ["with-serde"] } +rustreexo = { git = "https://github.com/halseth/rustreexo.git", branch = "usize-64-bit-le", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } From 5f8d2c1c34990c68ea1587b7eab5162020fc854c Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 15 Jan 2025 13:49:18 +0100 Subject: [PATCH 05/44] host+guest: check stump hash instead of full stump --- host/src/main.rs | 22 +++++++++++++--------- methods/guest/src/main.rs | 6 +++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 984a0c0..95422cc 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -29,7 +29,7 @@ use musig2::{ AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, SecondRound, }; - +use sha2::{Digest, Sha512_256}; use shared::{aggregate_keys, get_leaf_hashes, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { @@ -162,7 +162,6 @@ fn main() { let msg_to_sign = args.msg.unwrap(); - let (musig_pubs, musig_sig) = create_musig( vec![kp_node1, kp_node2, kp_bitcoin1, kp_bitcoin2], &msg_to_sign, @@ -173,10 +172,10 @@ fn main() { true, ); - let (musig_pubs2, musig_sig2) = create_musig(vec![kp_bitcoin1, kp_bitcoin2], &msg_to_sign); - println!("musig successfully verified"); + let (musig_pubs2, musig_sig2) = create_musig(vec![kp_bitcoin1, kp_bitcoin2], &msg_to_sign); + assert_eq!( verify_musig(musig_pubs2.clone(), musig_sig2, &msg_to_sign), true, @@ -279,7 +278,7 @@ fn main() { // We will prove inclusion in the UTXO set of the key we control. let verifying_key = schnorr::VerifyingKey::try_from(tap_key).unwrap(); - let internal_key= XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); + let internal_key = XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); let script_pubkey = ScriptBuf::new_p2tr(&secp, internal_key, None); @@ -337,15 +336,20 @@ fn main() { } fn verify_receipt(receipt: &Receipt, s: &Stump) { - let (receipt_stump, sk_hash, msg): (Stump, String, String) = receipt.journal.decode().unwrap(); + let (stump_hash, pk_hash, msg): (String, String, String) = receipt.journal.decode().unwrap(); - assert_eq!(&receipt_stump, s, "stumps not equal"); + let mut hasher = Sha512_256::new(); + s.serialize(&mut hasher).unwrap(); + let h = hex::encode(hasher.finalize()); // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. - receipt.verify(METHOD_ID).unwrap(); - println!("priv key hash: {}", sk_hash); + println!("bitcoin keys hash: {}", pk_hash); println!("signed msg: {}", msg); + println!("stump hash: {}", stump_hash); + + assert_eq!(stump_hash, h, "stumps not equal"); + receipt.verify(METHOD_ID).unwrap(); } fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) { let mut pubs: Vec = Vec::new(); diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 5a1f1e7..bf44f90 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -116,8 +116,12 @@ fn main() { hasher.update(&vk2.to_bytes()); let pk_hash = hex::encode(hasher.finalize()); + let mut shasher = Sha512_256::new(); + s.serialize(&mut shasher).unwrap(); + let stump_hash = hex::encode(shasher.finalize()); + // write public output to the journal - env::commit(&s); + env::commit(&stump_hash); env::commit(&pk_hash); env::commit(&msg); } \ No newline at end of file From 5d60e001ad4000d5039647f38e1b8286992334d8 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 16 Jan 2025 14:52:42 +0100 Subject: [PATCH 06/44] dep: use main rusttreexo --- host/Cargo.toml | 2 +- methods/guest/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index c616da0..877fc87 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -9,7 +9,7 @@ shared = { path = "../shared" } risc0-zkvm = { version = "1.2.0"} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" -rustreexo = { git = "https://github.com/halseth/rustreexo.git", branch = "usize-64-bit-le", features = ["with-serde"] } +rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } bitcoin = { version = "0.32.5", features = ["std", "rand-std", "serde"] } bincode = "1.3.3" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 656532f..658f6be 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } risc0-zkvm = { version = "1.2.0", default-features = false, features = ['std'] } -rustreexo = { git = "https://github.com/halseth/rustreexo.git", branch = "usize-64-bit-le", features = ["with-serde"] } +rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } From c0108f8e4fd82b42d78b09065be2b917ad1bc06f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Sun, 19 Jan 2025 21:08:42 +0100 Subject: [PATCH 07/44] working musig --- host/src/main.rs | 166 +++++++++++++++++++++++++++----------- methods/guest/src/main.rs | 28 +++---- shared/Cargo.toml | 1 + shared/src/lib.rs | 11 ++- 4 files changed, 142 insertions(+), 64 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 95422cc..a4b1734 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -15,7 +15,7 @@ use std::str::FromStr; use std::time::SystemTime; use bitcoin::consensus::deserialize; -use bitcoin::key::Keypair; +use bitcoin::key::{Keypair, UntweakedPublicKey}; use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification}; use bitcoin::{Address, BlockHash, Network, PrivateKey, ScriptBuf, Transaction, XOnlyPublicKey}; use clap::builder::TypedValueParser; @@ -30,7 +30,7 @@ use musig2::{ SecondRound, }; use sha2::{Digest, Sha512_256}; -use shared::{aggregate_keys, get_leaf_hashes, verify_musig}; +use shared::{aggregate_keys, get_leaf_hashes, sort_keypairs, sort_pubkeys, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { let sk = SecretKey::new(&mut rand::thread_rng()); @@ -72,9 +72,23 @@ struct Args { #[arg(long)] vout: Option, - /// Message to sign. - #[arg(short, long)] - msg: Option, + #[arg(long)] + msg_hex: Option, + + #[arg(long)] + musig_sig: Option, + + #[arg(long)] + node_key_1_priv: Option, + + #[arg(long)] + node_key_2_priv: Option, + + #[arg(long)] + bitcoin_key_1_priv: Option, + + #[arg(long)] + bitcoin_key_2_priv: Option, #[arg(long)] node_key_1: Option, @@ -105,6 +119,15 @@ struct CliStump { pub leaves: u64, } +fn parse_pubkey(pub_str: &str) -> PublicKey { + let pk_bytes = hex::decode(pub_str).unwrap(); + let pk = PublicKey::from_sec1_bytes(&pk_bytes).unwrap(); + + println!("sec1 pub: {}", hex::encode(pk_bytes)); + + pk +} + fn extract_keypair( secp: &Secp256k1, priv_str: &str, @@ -128,11 +151,8 @@ fn extract_keypair( } fn address(secp: &Secp256k1, pubkey: PublicKey, network: Network) { - let verifying_key = schnorr::VerifyingKey::try_from(pubkey).unwrap(); - // let ver_key = schnorr::VerifyingKey{ - // inner: pubkey, - // }; - let pubx = XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); + let pub_bytes: [u8; 32] = pubkey.to_sec1_bytes()[1..].try_into().unwrap(); + let pubx = XOnlyPublicKey::from_slice(&pub_bytes).unwrap(); let script_buf = ScriptBuf::new_p2tr(&secp, pubx, None); let addr = Address::from_script(script_buf.as_script(), network).unwrap(); @@ -151,40 +171,92 @@ fn main() { let secp = Secp256k1::new(); let network = args.network; + let mut keypairs = vec![]; + println!("node_key_1:"); - let kp_node1 = extract_keypair(&secp, args.node_key_1.unwrap().as_str(), network); + let pub_node1 = match args.node_key_1_priv { + Some(priv_str) => { + let kp = extract_keypair(&secp, &priv_str, network); + keypairs.push(kp); + PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() + } + None => parse_pubkey(&args.node_key_1.unwrap()), + }; + println!("node_key_2:"); - let kp_node2 = extract_keypair(&secp, args.node_key_2.unwrap().as_str(), network); + let pub_node2 = match args.node_key_2_priv { + Some(priv_str) => { + let kp = extract_keypair(&secp, &priv_str, network); + keypairs.push(kp); + PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() + } + None => parse_pubkey(&args.node_key_2.unwrap()), + }; + println!("bitcoin_key_1:"); - let kp_bitcoin1 = extract_keypair(&secp, args.bitcoin_key_1.unwrap().as_str(), network); + let pub_bitcoin1 = match args.bitcoin_key_1_priv { + Some(priv_str) => { + let kp = extract_keypair(&secp, &priv_str, network); + keypairs.push(kp); + PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() + } + None => parse_pubkey(&args.bitcoin_key_1.unwrap()), + }; + println!("bitcoin_key_2:"); - let kp_bitcoin2 = extract_keypair(&secp, args.bitcoin_key_2.unwrap().as_str(), network); + let pub_bitcoin2 = match args.bitcoin_key_2_priv { + Some(priv_str) => { + let kp = extract_keypair(&secp, &priv_str, network); + keypairs.push(kp); + PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() + } + None => parse_pubkey(&args.bitcoin_key_2.unwrap()), + }; - let msg_to_sign = args.msg.unwrap(); + sort_keypairs(&mut keypairs); - let (musig_pubs, musig_sig) = create_musig( - vec![kp_node1, kp_node2, kp_bitcoin1, kp_bitcoin2], - &msg_to_sign, - ); + let msg_to_sign = hex::decode(args.msg_hex.unwrap()).unwrap(); - assert_eq!( - verify_musig(musig_pubs.clone(), musig_sig, &msg_to_sign), - true, - ); + let all_pubs = vec![pub_node1, pub_node2, pub_bitcoin1, pub_bitcoin2]; + let mut musig_pubs = all_pubs.clone(); + sort_pubkeys(&mut musig_pubs); - println!("musig successfully verified"); + let mut bitcoin_pubs = vec![pub_bitcoin1, pub_bitcoin2]; + sort_pubkeys(&mut bitcoin_pubs); - let (musig_pubs2, musig_sig2) = create_musig(vec![kp_bitcoin1, kp_bitcoin2], &msg_to_sign); + for i in 0..musig_pubs.len() { + println!("key[{}]={}", i, hex::encode(musig_pubs[i].to_sec1_bytes())); + } + + let tap_key = aggregate_keys(bitcoin_pubs); + let tap_bytes = tap_key.to_sec1_bytes(); + println!("tap key : {}", hex::encode(&tap_bytes)); + address(&secp, tap_key, network); + + let musig_sig = match args.musig_sig { + Some(musig_sig) => hex::decode(musig_sig).unwrap(), + + // In case no signature is provided, we assume we are signing the message and private keys + // are available, + None => { + println!("signing"); + let (_, sig) = create_musig(keypairs, &msg_to_sign); + sig.to_vec() + } + }; + + println!("musig sig: {}", hex::encode(&musig_sig)); assert_eq!( - verify_musig(musig_pubs2.clone(), musig_sig2, &msg_to_sign), + verify_musig( + musig_pubs.clone(), + musig_sig.clone().try_into().unwrap(), + &msg_to_sign + ), true, ); - println!("musig with 2 pubs successfully verified"); - - let tap_key = aggregate_keys(musig_pubs2); - address(&secp, tap_key, network); + println!("musig successfully verified"); let receipt_file = if args.prove { let r = File::create(args.receipt_file.unwrap()).unwrap(); @@ -245,11 +317,6 @@ fn main() { } }; - let msg_bytes = msg_to_sign.as_bytes(); - let digest = sha256::Hash::hash(msg_bytes); - let digest_bytes = digest.to_byte_array(); - let msg = Message::from_digest(digest_bytes); - let proof: CliProof = serde_json::from_str(&args.utreexo_proof.unwrap()).unwrap(); let proof = Proof { targets: proof.targets, @@ -277,8 +344,8 @@ fn main() { assert_eq!(lh, leaf_hash); // We will prove inclusion in the UTXO set of the key we control. - let verifying_key = schnorr::VerifyingKey::try_from(tap_key).unwrap(); - let internal_key = XOnlyPublicKey::from_slice(verifying_key.to_bytes().as_slice()).unwrap(); + let internal_key = XOnlyPublicKey::from_slice(&tap_bytes[1..]).unwrap(); + println!("xonly tap key: {}", hex::encode(internal_key.serialize())); let script_pubkey = ScriptBuf::new_p2tr(&secp, internal_key, None); @@ -291,7 +358,7 @@ fn main() { let start_time = SystemTime::now(); let env = ExecutorEnv::builder() - .write(&msg_bytes) + .write(&msg_to_sign) .unwrap() .write(&acc) .unwrap() @@ -305,7 +372,7 @@ fn main() { .unwrap() .write(&block_hash) .unwrap() - .write(&musig_pubs) + .write(&all_pubs) .unwrap() .write(&musig_sig.as_slice()) .unwrap() @@ -336,7 +403,7 @@ fn main() { } fn verify_receipt(receipt: &Receipt, s: &Stump) { - let (stump_hash, pk_hash, msg): (String, String, String) = receipt.journal.decode().unwrap(); + let (stump_hash, pk_hash, msg): (String, String, Vec) = receipt.journal.decode().unwrap(); let mut hasher = Sha512_256::new(); s.serialize(&mut hasher).unwrap(); @@ -345,32 +412,33 @@ fn verify_receipt(receipt: &Receipt, s: &Stump) { // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. println!("bitcoin keys hash: {}", pk_hash); - println!("signed msg: {}", msg); + println!("signed msg: {}", hex::encode(msg)); println!("stump hash: {}", stump_hash); assert_eq!(stump_hash, h, "stumps not equal"); receipt.verify(METHOD_ID).unwrap(); } -fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) { +fn create_musig(keys: Vec, message: &Vec) -> (Vec, [u8; 64]) { let mut pubs: Vec = Vec::new(); for kp in keys.clone() { let bytes = kp.secret_key().secret_bytes(); - let pk = schnorr::SigningKey::from_bytes(&bytes).unwrap(); - - let ver_key = pk.verifying_key(); - pubs.push(ver_key.into()); + let str = hex::encode(bytes); + let scalar: musig2::secp::Scalar = str.parse().unwrap(); + let p = scalar.base_point_mul(); + let pubkey = PublicKey::from(p); + pubs.push(pubkey.into()); } let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); for kp in keys.clone() { - // let sk = bitcoin::secp256k1::SecretKey::from_str(&k).unwrap(); let bytes = kp.secret_key().secret_bytes(); let str = hex::encode(bytes); println!("priv key: {}", str); let scalar: musig2::secp::Scalar = str.parse().unwrap(); let p = scalar.base_point_mul(); + println!("point: {}", hex::encode(p.serialize())); key_agg_ctx.key_coefficient(p).unwrap(); } @@ -441,11 +509,11 @@ fn create_musig(keys: Vec, message: &str) -> (Vec, [u8; 64]) &key_agg_ctx, &aggregated_nonce, partial_signatures, - message, + &message, ) .expect("error aggregating signatures"); - musig2::verify_single(aggregated_pubkey, &final_signature, message) + musig2::verify_single(aggregated_pubkey, &final_signature, &message) .expect("aggregated signature must be valid"); (pubs, final_signature) diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index bf44f90..84e299b 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -14,7 +14,7 @@ use k256::schnorr::signature::Verifier; use k256::elliptic_curve::sec1::ToEncodedPoint; use k256::PublicKey; -use shared::{get_leaf_hashes, verify_musig, aggregate_keys}; +use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys}; pub fn new_p2tr( internal_key: UntweakedPublicKey, @@ -78,28 +78,31 @@ fn main() { let vout: u32 = env::read(); let block_height: u32 = env::read(); let block_hash: BlockHash = env::read(); - let musig_pubs: Vec = env::read(); + let all_pubs: Vec = env::read(); let musig_sig_bytes: Vec = env::read(); - let musig_sig = schnorr::Signature::try_from(musig_sig_bytes.as_slice()).unwrap(); + let mut musig_pubs = all_pubs.clone(); + sort_pubkeys(&mut musig_pubs); - let msg = from_utf8(msg_bytes.as_slice()).unwrap(); assert_eq!( - verify_musig(musig_pubs.clone(), musig_sig.to_bytes(), &msg), + verify_musig(musig_pubs.clone(), musig_sig_bytes.clone().try_into().unwrap(), &msg_bytes), true, ); // Aggregate the bitcoin keys. let bitcoin_key1 = musig_pubs[2]; let bitcoin_key2 = musig_pubs[3]; - let tap_pub = aggregate_keys(vec![bitcoin_key1, bitcoin_key2]); - let ver_key = schnorr::VerifyingKey::try_from(tap_pub).unwrap(); + let mut bitcoin_keys =vec![bitcoin_key1, bitcoin_key2]; + sort_pubkeys(&mut bitcoin_keys); + let tap_pub = aggregate_keys(bitcoin_keys); + + let pub_bytes : [u8; 32]= tap_pub.to_sec1_bytes()[1..].try_into().unwrap(); + let pubx = XOnlyPublicKey::from_slice(&pub_bytes).unwrap(); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); // We'll check that the given public key corresponds to an output in the utxo set. - let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); let script_pubkey = new_p2tr(pubx, None); // assert internal key is in tx used to calc leaf hash @@ -108,12 +111,9 @@ fn main() { // Assert it is in the set. assert_eq!(s.verify(&proof, &[leaf_hash]), Ok(true)); - let vk1 = schnorr::VerifyingKey::try_from(bitcoin_key1).unwrap(); - let vk2 = schnorr::VerifyingKey::try_from(bitcoin_key2).unwrap(); - let mut hasher = Sha512_256::new(); - hasher.update(&vk1.to_bytes()); - hasher.update(&vk2.to_bytes()); + hasher.update(&bitcoin_key1.to_sec1_bytes()); + hasher.update(&bitcoin_key2.to_sec1_bytes()); let pk_hash = hex::encode(hasher.finalize()); let mut shasher = Sha512_256::new(); @@ -123,5 +123,5 @@ fn main() { // write public output to the journal env::commit(&stump_hash); env::commit(&pk_hash); - env::commit(&msg); + env::commit(&msg_bytes); } \ No newline at end of file diff --git a/shared/Cargo.toml b/shared/Cargo.toml index bceefca..5629eff 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -8,3 +8,4 @@ bitcoin = { version = "0.32.5"} sha2 = "0.10.8" bitcoin_hashes = "0.14.0" musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index b326ad9..70343ea 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,6 +4,7 @@ use bitcoin_hashes::Hash as BitcoinHash; use sha2::{Digest, Sha512_256}; use bitcoin::consensus::Encodable; +use bitcoin::key::Keypair; use bitcoin::{BlockHash, Transaction}; use k256::PublicKey; @@ -57,7 +58,7 @@ pub fn aggregate_keys(pubs: Vec) -> PublicKey { aggregated_pubkey } -pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &str) -> bool { +pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &Vec) -> bool { let aggregated_pubkey: PublicKey = aggregate_keys(pubs); musig2::verify_single(aggregated_pubkey, &sig, message) @@ -65,3 +66,11 @@ pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &str) -> bool true } + +pub fn sort_pubkeys(pubkeys: &mut Vec) { + pubkeys.sort_by(|a, b| a.to_sec1_bytes().cmp(&b.to_sec1_bytes())); +} + +pub fn sort_keypairs(kp: &mut Vec) { + kp.sort_by(|a, b| a.public_key().serialize().cmp(&b.public_key().serialize())); +} From 62542f9db8b7a2fa6a7cc2400b23f1a1c2d09437 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 23 Jan 2025 10:36:39 +0100 Subject: [PATCH 08/44] guest: fix index into correct vector --- methods/guest/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 84e299b..490593b 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -90,8 +90,8 @@ fn main() { ); // Aggregate the bitcoin keys. - let bitcoin_key1 = musig_pubs[2]; - let bitcoin_key2 = musig_pubs[3]; + let bitcoin_key1 = all_pubs[2]; + let bitcoin_key2 = all_pubs[3]; let mut bitcoin_keys =vec![bitcoin_key1, bitcoin_key2]; sort_pubkeys(&mut bitcoin_keys); let tap_pub = aggregate_keys(bitcoin_keys); From 4effe9389f330432f6f1af482ba1c69c72035fbc Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 23 Jan 2025 10:46:35 +0100 Subject: [PATCH 09/44] make node_keys public --- host/src/main.rs | 4 +++- methods/guest/src/main.rs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/host/src/main.rs b/host/src/main.rs index a4b1734..fcd432e 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -403,7 +403,7 @@ fn main() { } fn verify_receipt(receipt: &Receipt, s: &Stump) { - let (stump_hash, pk_hash, msg): (String, String, Vec) = receipt.journal.decode().unwrap(); + let (node_key1, node_key2, stump_hash, pk_hash, msg): (PublicKey, PublicKey, String, String, Vec) = receipt.journal.decode().unwrap(); let mut hasher = Sha512_256::new(); s.serialize(&mut hasher).unwrap(); @@ -411,6 +411,8 @@ fn verify_receipt(receipt: &Receipt, s: &Stump) { // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. + println!("committed node_key1 : {}", hex::encode(&node_key1.to_sec1_bytes())); + println!("committed node_key2 : {}", hex::encode(&node_key2.to_sec1_bytes())); println!("bitcoin keys hash: {}", pk_hash); println!("signed msg: {}", hex::encode(msg)); println!("stump hash: {}", stump_hash); diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 490593b..d362970 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -89,6 +89,9 @@ fn main() { true, ); + let node_key1 = all_pubs[0]; + let node_key2 = all_pubs[1]; + // Aggregate the bitcoin keys. let bitcoin_key1 = all_pubs[2]; let bitcoin_key2 = all_pubs[3]; @@ -121,6 +124,8 @@ fn main() { let stump_hash = hex::encode(shasher.finalize()); // write public output to the journal + env::commit(&node_key1); + env::commit(&node_key2); env::commit(&stump_hash); env::commit(&pk_hash); env::commit(&msg_bytes); From 075434bebb76b9646bd14bdc8929c9ee727dfda1 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 23 Jan 2025 13:37:33 +0100 Subject: [PATCH 10/44] start gossip doc --- docs/ln_gossip.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/ln_gossip.md diff --git a/docs/ln_gossip.md b/docs/ln_gossip.md new file mode 100644 index 0000000..3e229d2 --- /dev/null +++ b/docs/ln_gossip.md @@ -0,0 +1,81 @@ +### Privacy preserving Lightning gossip + +This document describes a proposal for making Lightning channel gossip more +private, avoiding the need for revealing the channel outpoint. + +It is based on Utreexo and zero knowledge proofs, and is accompanied with a +proof-of-concept Rust implementation. + +The proposal is created as an extension to the gossip v1.75 proposal for +taproot channel gossip and intended to be used as an optional feature for +privacy conscious users. + +## Privacy of Lightning channel gossip +TODO + +## Taproot gossip (gossip v1.75) +TODO: desribe current proposal + +Example channel_announcement_2: +```json +{ + "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "ShortChannelID": "1000:100:0", + "Capacity": 100000, + "NodeID1": "0246175dd633eaa1c1684a9e15d218d247e421b009642f3e82e9f138ad9d645e03", + "NodeID2": "02b651f157c5cf7bfbf07d52855c21aca861d7e3b8711d84cb04149e0a67151e16", + "BitcoinKey1": "027fc1e3f6f5a67b804cb86d813adf94f89ea1de63f630607d4c31242d58306043", + "BitcoinKey2": "03fef5084b98aa37757acce81d25148cfdb9592142567c6265e39b082e73c4d54", + "MerkleRootHash": null, + "Signature": { + "bytes": "5c14ad15b614c9f91fd5c66b7bfe3f3552427c6d5e6d598f5838c5d219cdd0b89c72ad6a3effe5d995387563b80dfb1b59da599c936c705ad8dfd6da8288b89b", + "sigType": 1 + }, +} +``` + +### ZK-gossip +What we propose is an extension to the taproot gossip proposal, that makes it +possible for the two channel parties to remove the link between the channel and +on-chain outpoint. + +In order to still be able to protect the network from channel DOS attacks, we +require the channel annoucement message to include a ZK-proof that proves the +inclusion of the channel in the UTXO set, and that it is controlled by the two +nodes in the graph. + +In order to create the ZK proof with these properties, we start with the data +already contained in the regular taproot gossip channel announcment: + +1) node_id_1, node_id_2 +2) bitcoin_key_1, bitcoin_key_2 +3) merkle_root_hash +4) signature + +In addition we assemble a Utreexo accumulator and a proof for the channel +output's inclusion in this accumulator. + +Using these pieces of data we create a ZK-proof that validates the following: + +0) bitcoin_keys = MuSig2.KeySort(bitcoin_key_1, bitcoin_key_2) +1) P_agg_out = MuSig2.KeyAgg(bitcoin_keys) +2) Verify that P_agg_out is in the UTXO set using the utreexo accumulator and proof. +3) P_agg = MuSig2.KeyAgg(MuSig2.KeySort(node_id_1, node_id_2, bitcoin_key_1, bitcoin_key_2)) +4) Verify the signaure against P_agg +5) pk_hash = hash(bitcoin_keys[0] || bitcoin_keys[1]) + +We then output (or make public) the two node_ids, the signed data, utreexo accumulator and pk_hash. + +Now we can output a proof (ZK-SNARK, groth16) of 256 bytes, and assemble a new channel announcment: + +```json + "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "Capacity": 100000, + "NodeID1": "0246175dd633eaa1c1684a9e15d218d247e421b009642f3e82e9f138ad9d645e03", + "NodeID2": "02b651f157c5cf7bfbf07d52855c21aca861d7e3b8711d84cb04149e0a67151e16", + "UtreexoRoot": "4b07311e3e774f0db3bcedee47c56fe30da326d9271d1a2558a5975ab4214478", + "ZKType": "0", + "ZKProof": "6508d8476086238076bb673005f9ef3bfe7f0c198a1d4f6fcee65e19478b422c512aefd004f8f476d0ef5939dc4339e3e19347a6ab60fe5714e9d3e3e77417499dbf18da68dfd942d79c8bf4cf811f615334f4643befb267a189d8e6b05509760bfd7add9aa9ecbce38db277bf11b1b94e147b504e75be5405066421aad8e10b49d105a33241742bafe611b4025ffa35d066fc87e11df595030d18b962ad5917ef1f73c97d660c1e62c7e392d51821ec342b2faf763d2a9177d13471c8b2a829578fd401d76aa8ae5642937f48573e657a5af14fda5f7a39216dda05b183121913088d2d0e0c1902d1f656b5d769b95040a40ef5a9ffd87f550545b0a5bc2505", +} +``` + From 962d336fed110ef2460aa28a70a0a76bad000350 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:39:59 +0100 Subject: [PATCH 11/44] print full pubkey --- host/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/host/src/main.rs b/host/src/main.rs index fcd432e..f41c00b 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -143,8 +143,10 @@ fn extract_keypair( let (internal_key, _parity) = keypair.x_only_public_key(); let script_buf = ScriptBuf::new_p2tr(&secp, internal_key, None); let addr = Address::from_script(script_buf.as_script(), network).unwrap(); + let pubkey = keypair.public_key(); println!("priv: {}", hex::encode(keypair.secret_key().secret_bytes())); - println!("pub: {}", internal_key); + println!("pubkey: {}", hex::encode(pubkey.serialize())); + println!("xonly pub: {}", internal_key); println!("address: {}", addr); keypair From 8d24d54930058abca497a6196e73fe22609a850e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:40:35 +0100 Subject: [PATCH 12/44] move verification early --- host/src/main.rs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index f41c00b..1fbc186 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -170,6 +170,23 @@ fn main() { let args = Args::parse(); + let receipt_file = if args.prove { + let r = File::create(args.receipt_file.unwrap()).unwrap(); + r + } else { + let r = File::open(args.receipt_file.unwrap()).unwrap(); + r + }; + + // If not proving, simply verify the passed receipt using the loaded utxo set. + let start_time = SystemTime::now(); + if !args.prove { + let receipt: Receipt = bincode::deserialize_from(receipt_file).unwrap(); + verify_receipt(&receipt); + println!("receipt verified in {:?}", start_time.elapsed().unwrap()); + return; + } + let secp = Secp256k1::new(); let network = args.network; @@ -260,13 +277,6 @@ fn main() { println!("musig successfully verified"); - let receipt_file = if args.prove { - let r = File::create(args.receipt_file.unwrap()).unwrap(); - r - } else { - let r = File::open(args.receipt_file.unwrap()).unwrap(); - r - }; let acc: CliStump = serde_json::from_str(&args.utreexo_acc.unwrap()).unwrap(); let acc = Stump { @@ -278,15 +288,8 @@ fn main() { .collect(), }; - let start_time = SystemTime::now(); - // If not proving, simply verify the passed receipt using the loaded utxo set. - if !args.prove { - let receipt: Receipt = bincode::deserialize_from(receipt_file).unwrap(); - verify_receipt(&receipt, &acc); - println!("receipt verified in {:?}", start_time.elapsed().unwrap()); - return; - } + let proof_type: ProverOpts = match args.proof_type.as_deref() { None => { From 007924dbac32615328f65b0d70afab48dd2a9bdc Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:40:55 +0100 Subject: [PATCH 13/44] print METHOD_ID --- host/src/main.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/host/src/main.rs b/host/src/main.rs index 1fbc186..704125f 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -424,6 +424,19 @@ fn verify_receipt(receipt: &Receipt, s: &Stump) { assert_eq!(stump_hash, h, "stumps not equal"); receipt.verify(METHOD_ID).unwrap(); + println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); +} + +fn to_bytes(h: [u32;8]) -> [u8; 32] { + let mut buf = [0u8; 32]; + for i in 0..8 { + let b: [u8; 4] = h[i].to_be_bytes(); + for j in 0..4 { + buf[i*4+j] = b[j]; + } + } + + buf } fn create_musig(keys: Vec, message: &Vec) -> (Vec, [u8; 64]) { let mut pubs: Vec = Vec::new(); From a5ab45be7d2e33e5fe0664b8b5b1b2838182bf32 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:41:20 +0100 Subject: [PATCH 14/44] remove stump assert from verify_receipt --- host/src/main.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 704125f..34eb391 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -397,7 +397,7 @@ fn main() { // extract the receipt. let receipt = prove_info.receipt; - verify_receipt(&receipt, &acc); + verify_receipt(&receipt); let seal_size = receipt.seal_size(); @@ -407,13 +407,9 @@ fn main() { bincode::serialize_into(receipt_file, &receipt).unwrap(); } -fn verify_receipt(receipt: &Receipt, s: &Stump) { +fn verify_receipt(receipt: &Receipt) { let (node_key1, node_key2, stump_hash, pk_hash, msg): (PublicKey, PublicKey, String, String, Vec) = receipt.journal.decode().unwrap(); - let mut hasher = Sha512_256::new(); - s.serialize(&mut hasher).unwrap(); - let h = hex::encode(hasher.finalize()); - // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. println!("committed node_key1 : {}", hex::encode(&node_key1.to_sec1_bytes())); @@ -422,7 +418,6 @@ fn verify_receipt(receipt: &Receipt, s: &Stump) { println!("signed msg: {}", hex::encode(msg)); println!("stump hash: {}", stump_hash); - assert_eq!(stump_hash, h, "stumps not equal"); receipt.verify(METHOD_ID).unwrap(); println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); } From a4f185c5e43b212b23217beb4844a4627368efba Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:42:32 +0100 Subject: [PATCH 15/44] print agg key --- host/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/host/src/main.rs b/host/src/main.rs index 34eb391..ef80224 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -264,6 +264,10 @@ fn main() { } }; + let agg_key = aggregate_keys(musig_pubs.clone()); + let agg_bytes = agg_key.to_sec1_bytes(); + println!("aggregate key : {}", hex::encode(&agg_bytes)); + println!("musig sig: {}", hex::encode(&musig_sig)); assert_eq!( From 836dcc3aa30b8571cdef644bb4d840fb458b02a9 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Mon, 27 Jan 2025 21:45:02 +0100 Subject: [PATCH 16/44] docs: add musig2 verification example --- docs/musig2.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/musig2.md diff --git a/docs/musig2.md b/docs/musig2.md new file mode 100644 index 0000000..d32a070 --- /dev/null +++ b/docs/musig2.md @@ -0,0 +1,133 @@ +## Musig2 example + +As outlined in the [Private Lightning Gossip](./ln_gossip.md) doc, a potential +use case of OutputZero is to make LN gossip more private. In order for this to +be realized, verification of Musig2 key aggregation in the ZK environment is +performed. + +In this example we'll show how this is done. + +### Create private keys and sign +The Musig2 key used in Taproot Lightning gossip is aggergated from 4 individual public keys. We start by creating 4 keypairs and specifying a message to sign. + +```bash +$ cargo run --release -- --node-key-1-priv "new" --node-key-2-priv "new" --bitcoin-key-1-priv "new" --bitcoin-key-2-priv "new" --msg-hex "`echo "this message" | xxd -p`" --network "signet" +node_key_1: +priv: 121f7492040ebd1a484b433ba661258963d7d36cfaeab7084e520ecba4e4c1b4 +pubkey: 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +xonly pub: 081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +address: tb1ppqym9scg7p66pscvjlelcmx445vsh9lq9v3mv93232hqvd7dppesp3mr34 +node_key_2: +priv: 09951546ca93633974dc37dfc0fa3fba4fc882189a0c047eead735927aeb6ef5 +pubkey: 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +xonly pub: b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +address: tb1pkvt6fv629lcjptgfglftjxmnwpzs6c4detk2sa437wn5smuj54psx5nfyn +bitcoin_key_1: +priv: a7862a16105e330c3d31a584df5f41c5a43e457d2d1fdb73cee76f92754ae863 +pubkey: 027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +xonly pub: 7536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +address: tb1prglt49pm50dtfj8wj3l7tqc94m8ytw4xm7x5jkn7famjx83nydpqkl08w7 +bitcoin_key_2: +priv: e40871a76761ae4554e2c2c07e2bc80e6e0dc6c1b25792e1851cf7deb369573d +pubkey: 03a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731 +xonly pub: a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731 +address: tb1pjrshj2h0yyds3c279xlz9e9uelvc7hzfdagamzrknp79xz0yr7ns7sj0wz +... +tap key : 0236827c6ebdd86cf2172a2caac10ef08ca3d7643021ccf16c8eca11b393ace4e8 +pub: 36827c6ebdd86cf2172a2caac10ef08ca3d7643021ccf16c8eca11b393ace4e8 +address: tb1px5m3w7t2v4nk8pxeaacgc3nn8vulqakfe2uzkavpy0q24ct3n36qghem5d +signing +... +aggregate key : 0205948efcf1da93342484e6df5000b9314765ac3566476ba3549cf4ecfa54fbf8 +musig sig: e9b9814e617421ca7d8c3c11979f5373fb6749d59f542edb5381be56682cd62f048988a148354c8e3cfabaf5126497d7ddff1c4e62efe5003629f358f9e47c75 +musig successfully verified +``` + +### Funding the output +Now that we have created the keys, we also have the possibility to send money +to them. So we'll go ahead and send some signet coins to the address of the +aggregate tap key (`agg[bitcoin_key1, bitcoin_key2]`): +`tb1px5m3w7t2v4nk8pxeaacgc3nn8vulqakfe2uzkavpy0q24ct3n36qghem5d`. + +In my case this resulted in the following transaction: [2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6](https://mempool.space/signet/tx/2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6). + +After this confirms we have an on-chain output we want to sign for _without +revealing which one it is._ + +### Getting the Utreexo proof +Make sure you have a running bitcoind on signet, and a [utreexo bridge +node](https://github.com/Davidson-Souza/rpc-utreexo-bridge) connected to it. + +We start by finding the _leaf hash_ for the output we just created. + +```bash +$ curl http://127.0.0.1:3000/leaf/2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6:1 | jq +{ + "data": { + "hash": "31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d", + "leaf_data": { + "block_hash": "000000038ea165485e344192ec434db1a90624dbc78bab14b6fe452645748923", + "block_height": 232415, + "hash": "31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d", + "is_coinbase": false, + "prevout": "2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6:1", + "utxo": { + "script_pubkey": "5120353717796a65676384d9ef708c46733b39f076c9cab82b758123c0aae1719c74", + "value": 553396 + } + } + }, + "error": null +} +``` + +We'll get the utreexo accumulator and the proof for the inclusion of this leaf +hash into the accumulator (we'll store these to file): + +```bash +$ curl http://127.0.0.1:3000/prove/31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d | jq -c '.data' > proof_utreexo.json +$ curl http://127.0.0.1:3000/acc | jq -c '.data' > acc_utreexo.json +$ bitcoin-cli --signet getrawtransaction 2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6 > tx.hex +``` + +### Generate the proof +Now we have all pieces ready to generate the full proof: + +```bash +$ cargo run --release -- --utreexo-acc "`cat acc_utreexo.json`" --utreexo-proof "`cat proof_utreexo.json`" --leaf-hash '31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d' --prove --receipt-file 'receipt.bin' --msg-hex "`echo "this message" | xxd -p`" --tx-hex "`cat tx.hex`" --vout 1 --block-height 232415 --block-hash '000000038ea165485e344192ec434db1a90624dbc78bab14b6fe452645748923' --node-key-1 "02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504" --node-key-2 "03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e" --bitcoin-key-1 "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --bitcoin-key-2 "03a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731" --proof-type 'default' --musig-sig 'e9b9814e617421ca7d8c3c11979f5373fb6749d59f542edb5381be56682cd62f048988a148354c8e3cfabaf5126497d7ddff1c4e62efe5003629f358f9e47c75' +... +Proving took 84.877619s +committed node_key1 : 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +committed node_key2 : 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +bitcoin keys hash: ec938aa2258d7369dada32facf0e8c10c67db48fc6c15a7bb2bbf931900e9d3e +signed msg: 74686973206d6573736167650a +stump hash: e48b939f7fe439c15cc0665d926f6d5f66a0355d60b1518ba7f2c0f79b233a15 +verified METHOD_ID=9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418 +receipt (2228856). seal size: 2225440. +``` + +### Proof types +Risc0 has support for a few different proof types. The deafault is a composite +proof, which can be reduced in size by using the compressed succint proof type. +These are both variants of ZK-STARKS. + +One can also specify the `groth16` proof type (currently only available on x86 +hardware), which will wrap the STARK proof in a ZK-SNARK and dramastically +reduce the proof size! We are talking a proof size of 256 bytes. More details +on the various proof types here: [Risc0 Proof System +Overview](https://dev.risczero.com/proof-system/). + +### Verification +We can verify the proof by ommitting the `--prove` flag: + +```bash +$ cargo run --release -- --receipt-file 'receipt.bin' +... +committed node_key1 : 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +committed node_key2 : 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +bitcoin keys hash: ec938aa2258d7369dada32facf0e8c10c67db48fc6c15a7bb2bbf931900e9d3e +signed msg: 74686973206d6573736167650a +stump hash: e48b939f7fe439c15cc0665d926f6d5f66a0355d60b1518ba7f2c0f79b233a15 +verified METHOD_ID=9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418 +receipt verified in 411.941ms +``` From 15cfb6adcef11379c5601831a864e15fe09910dc Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 28 Jan 2025 12:53:09 +0100 Subject: [PATCH 17/44] docs: update ln_gossip --- docs/ln_gossip.md | 108 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 20 deletions(-) diff --git a/docs/ln_gossip.md b/docs/ln_gossip.md index 3e229d2..9cee7b3 100644 --- a/docs/ln_gossip.md +++ b/docs/ln_gossip.md @@ -1,22 +1,22 @@ -### Privacy preserving Lightning gossip +## Privacy preserving Lightning gossip This document describes a proposal for making Lightning channel gossip more private, avoiding the need for revealing the channel outpoint. -It is based on Utreexo and zero knowledge proofs, and is accompanied with a +It is based on Utreexo and zero-knowledge proofs, and is accompanied with a proof-of-concept Rust implementation. The proposal is created as an extension to the gossip v1.75 proposal for taproot channel gossip and intended to be used as an optional feature for privacy conscious users. -## Privacy of Lightning channel gossip +### Privacy of Lightning channel gossip TODO -## Taproot gossip (gossip v1.75) +### Taproot gossip (gossip v1.75) TODO: desribe current proposal -Example channel_announcement_2: +Example `channel_announcement_2`: ```json { "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", @@ -34,10 +34,10 @@ Example channel_announcement_2: } ``` -### ZK-gossip +## ZK-gossip What we propose is an extension to the taproot gossip proposal, that makes it possible for the two channel parties to remove the link between the channel and -on-chain outpoint. +on-chain output. In order to still be able to protect the network from channel DOS attacks, we require the channel annoucement message to include a ZK-proof that proves the @@ -47,35 +47,103 @@ nodes in the graph. In order to create the ZK proof with these properties, we start with the data already contained in the regular taproot gossip channel announcment: -1) node_id_1, node_id_2 -2) bitcoin_key_1, bitcoin_key_2 -3) merkle_root_hash -4) signature +1) `node_id_1`, `node_id_2` +2) `bitcoin_key_1`, `bitcoin_key_2` +3) `merkle_root_hash` +4) `signature` +5) `capacity` + +(we'll ignore the `merkle_root_hash` for now). In addition we assemble a Utreexo accumulator and a proof for the channel output's inclusion in this accumulator. Using these pieces of data we create a ZK-proof that validates the following: -0) bitcoin_keys = MuSig2.KeySort(bitcoin_key_1, bitcoin_key_2) -1) P_agg_out = MuSig2.KeyAgg(bitcoin_keys) -2) Verify that P_agg_out is in the UTXO set using the utreexo accumulator and proof. -3) P_agg = MuSig2.KeyAgg(MuSig2.KeySort(node_id_1, node_id_2, bitcoin_key_1, bitcoin_key_2)) -4) Verify the signaure against P_agg -5) pk_hash = hash(bitcoin_keys[0] || bitcoin_keys[1]) +1) `bitcoin_keys = MuSig2.KeySort(bitcoin_key_1, bitcoin_key_2)` +2) `P_agg_out = MuSig2.KeyAgg(bitcoin_keys)` +3) Check `capacity <= vout.value` +4) Check `P_agg_out = vout.script_pubkey` +3) Verify that `vout` is in the UTXO set using the utreexo accumulator and proof. +4) `P_agg = MuSig2.KeyAgg(MuSig2.KeySort(node_id_1, node_id_2, bitcoin_key_1, bitcoin_key_2))` +5) Verify the signaure against `P_agg` +6) `pk_hash = hash(bitcoin_keys[0] || bitcoin_keys[1])` -We then output (or make public) the two node_ids, the signed data, utreexo accumulator and pk_hash. +We then output (or make public) the two `node_ids`, the signed data, utreexo accumulator and `pk_hash`. -Now we can output a proof (ZK-SNARK, groth16) of 256 bytes, and assemble a new channel announcment: +Now we can output a proof (ZK-SNARK, groth16) of 256 bytes, and assemble a new +`channel_announcement_zk` (since messages are TLV, this should really be +combined with the `channel_announcement_2` with appropriate fields set): ```json +{ "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", "Capacity": 100000, "NodeID1": "0246175dd633eaa1c1684a9e15d218d247e421b009642f3e82e9f138ad9d645e03", "NodeID2": "02b651f157c5cf7bfbf07d52855c21aca861d7e3b8711d84cb04149e0a67151e16", "UtreexoRoot": "4b07311e3e774f0db3bcedee47c56fe30da326d9271d1a2558a5975ab4214478", - "ZKType": "0", + "ZKType": "9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418", "ZKProof": "6508d8476086238076bb673005f9ef3bfe7f0c198a1d4f6fcee65e19478b422c512aefd004f8f476d0ef5939dc4339e3e19347a6ab60fe5714e9d3e3e77417499dbf18da68dfd942d79c8bf4cf811f615334f4643befb267a189d8e6b05509760bfd7add9aa9ecbce38db277bf11b1b94e147b504e75be5405066421aad8e10b49d105a33241742bafe611b4025ffa35d066fc87e11df595030d18b962ad5917ef1f73c97d660c1e62c7e392d51821ec342b2faf763d2a9177d13471c8b2a829578fd401d76aa8ae5642937f48573e657a5af14fda5f7a39216dda05b183121913088d2d0e0c1902d1f656b5d769b95040a40ef5a9ffd87f550545b0a5bc2505", + "PKHash": "be7d602934c5ce95000ee989748f6c892ce16fb4276389ec15bc0764fbc4bea5" } ``` +`ZKType` is the unique identifier for the verification program, and is often a +unique hash of the binary representation of the verifier. This makes it easy to +move to a new proof type. `pk_hash` acts as a unique channel identifier. + +(note: the `pk_hash` is not unique if the two nodes reuse their public keys for +a new output. Maybe this can be used to move the channel to a bigger UTXO +without closing it...) + +### Creating proofs +When opening a channel, the two channel counterparties must create the ZK-proof +in order to announce the channel. In the current POC this is requires a decent +hardware setup in order to be effective (~minutes on a beefy laptop). + +It should be noted that only nodes announcing public channels need to do this, +and they usually require a certain level of hardware to be effective routers +anyway. + +It is also assumed that proving time will come down as advances are made in +proof systems and hardware acceleration. + +### Handling received channel_announcement_zk +When a node receives a `channel_accnouncement_zk` message, it will first use +the `pk_hash` to check whether this is a channel already known to the node. The +`pk_hash` is deterministic and unique per channel. It will then verify the +proof if it has a type known to the node. Otherwise it will ignore it. + +Since we can no longer detect channels closing on-chain, we must require +periodic refreshes of announcememnts, proving the channel is still in the utxo +set. We propose setting this to around two weeks. With legacy channels we +already have the problem of not knowing whether an channel unspent on-chain is +active, so some kind of liveness signal is needed regardless. + +### Caveats +- Proving is slow. + - One is not really in a hurry announcing a channel, so this is most likely not a problem as these get optimized. +- Proofs are large. + - STARKS (using Risc0 as in the proof-of-concept) are around 200kB. + - Wrapping them in groth16 brings this down to 200 bytes. +- Verification is slow-ish. + - This can likely be heavily optimized. + - groth16 proofs are much faster to verify than STARKS. + +### Wins +- This can be used today, no softforks needed :) +- Easier to be a light-node. + - Light clients need to get a periodic refresh of their Utreexp accumulator + (from a trusted source, or a semi-trusted source that can prove that the + accumulator is correctly crafted). + - With the accumulator they can validate the proofs just as a full node. + +### Proof-of-concept +I've preperad a branch with accompanying code and documents walking through the +process of creating a proof from the original channel announcment: [Musig2 +example](https://github.com/halseth/output-zero/blob/musig2/docs/musig2.md). + +It is based on RiscZero, a versatile framework for creating proofs of execution +for RISC-V binaries. This means that is easy to add more contrainsts to the +verification of the UTXOs if useful. + From bbf010547a876fed6b4e917ae46b8876b8c4c554 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 11 Feb 2025 15:11:00 +0100 Subject: [PATCH 18/44] dep: bump k256 lib --- methods/guest/Cargo.toml | 5 ++--- shared/Cargo.toml | 6 +++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 658f6be..6ed8ad3 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -13,12 +13,11 @@ serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } sha2 = "0.10.8" -k256 = { version = "=0.13.3", features = ["arithmetic", "serde", "expose-field", "std", "ecdsa", "pkcs8", "schnorr"], default-features = false } -bitcoin_hashes = "0.14.0" +k256 = { version = "=0.13.4", features = ["arithmetic", "serde", "expose-field", "std", "ecdsa", "pkcs8", "schnorr"], default-features = false } [patch.crates-io] # Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint # multiplication accelerator support for all downstream usages of the following crates. sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } -k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } +k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.4-risczero.0" } crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } \ No newline at end of file diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 5629eff..022097c 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -8,4 +8,8 @@ bitcoin = { version = "0.32.5"} sha2 = "0.10.8" bitcoin_hashes = "0.14.0" musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } -hex = { version = "0.4.3", default-features = false, features = ["alloc"] } + +[patch.crates-io] +# Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint +# multiplication accelerator support for all downstream usages of the following crates. +sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } From 55263b2cdef00f709b98a3eadf4ac12e47716179 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 11 Feb 2025 15:21:45 +0100 Subject: [PATCH 19/44] dep: update zkvm --- host/Cargo.toml | 2 +- methods/guest/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index 877fc87..4160ce3 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] methods = { path = "../methods" } shared = { path = "../shared" } -risc0-zkvm = { version = "1.2.0"} +risc0-zkvm = { version = "1.2.3"} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 6ed8ad3..14deb38 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } -risc0-zkvm = { version = "1.2.0", default-features = false, features = ['std'] } +risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std'] } rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } From 408e21f8a9df0975a97d238becd79fff61611d4f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 11 Feb 2025 15:26:04 +0100 Subject: [PATCH 20/44] dep: update musig2 --- host/Cargo.toml | 2 +- shared/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index 4160ce3..37d3e38 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -18,7 +18,7 @@ sha2 = "0.10.8" bitcoin_hashes = "0.14.0" k256 = { version = "0.13.3", features = ["serde"] } serde_json = "1.0.128" -musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } +musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } secp256k1 = "0.30.0" [features] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 022097c..dc58a9c 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" bitcoin = { version = "0.32.5"} sha2 = "0.10.8" bitcoin_hashes = "0.14.0" -musig2 = { version = "0.2.2", default-features = false, features = ["k256"] } +musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } [patch.crates-io] # Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint From ae251d100669ad5cfcf03e16e42a137dcace7336 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 11 Feb 2025 15:28:48 +0100 Subject: [PATCH 21/44] update risc0-build --- methods/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/methods/Cargo.toml b/methods/Cargo.toml index bed6bf8..c60474a 100644 --- a/methods/Cargo.toml +++ b/methods/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [build-dependencies] -risc0-build = { version = "1.0.5" } +risc0-build = { version = "1.2.3" } [package.metadata.risc0] methods = ["guest"] From 1bfa7d877d5ac99696109d84447b26ece3ab785f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 11 Feb 2025 15:43:46 +0100 Subject: [PATCH 22/44] bump k256 precompile proving time 115s -> 50s on M1 Max --- host/Cargo.toml | 2 +- methods/Cargo.toml | 2 +- methods/guest/Cargo.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index 37d3e38..f6605d2 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] methods = { path = "../methods" } shared = { path = "../shared" } -risc0-zkvm = { version = "1.2.3"} +risc0-zkvm = { version = "1.2.3" , features = ["unstable"]} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } diff --git a/methods/Cargo.toml b/methods/Cargo.toml index c60474a..623020b 100644 --- a/methods/Cargo.toml +++ b/methods/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [build-dependencies] -risc0-build = { version = "1.2.3" } +risc0-build = { version = "1.2.3" , features = ["unstable"]} [package.metadata.risc0] methods = ["guest"] diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 14deb38..7912026 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } -risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std'] } +risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std', 'unstable'] } rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } @@ -19,5 +19,5 @@ k256 = { version = "=0.13.4", features = ["arithmetic", "serde", "expose-field", # Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint # multiplication accelerator support for all downstream usages of the following crates. sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } -k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.4-risczero.0" } +k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.4-risczero.1" } crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } \ No newline at end of file From a76bf63124e2909872c96ec29c8a40b04040571c Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 11:59:46 +0100 Subject: [PATCH 23/44] refactor imports + format --- host/src/main.rs | 41 ++++++++++++++------------- methods/guest/src/main.rs | 54 ++--------------------------------- shared/src/lib.rs | 59 +++++++++++++++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index ef80224..64e664e 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -4,7 +4,6 @@ use std::fs::File; use methods::{METHOD_ELF, METHOD_ID}; use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, Receipt}; -use bitcoin_hashes::sha256; use bitcoin_hashes::Hash as BitcoinHash; use clap::Parser; @@ -15,21 +14,17 @@ use std::str::FromStr; use std::time::SystemTime; use bitcoin::consensus::deserialize; -use bitcoin::key::{Keypair, UntweakedPublicKey}; -use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing, Verification}; -use bitcoin::{Address, BlockHash, Network, PrivateKey, ScriptBuf, Transaction, XOnlyPublicKey}; +use bitcoin::key::Keypair; +use bitcoin::secp256k1::{rand, Secp256k1, SecretKey, Signing, Verification}; +use bitcoin::{Address, BlockHash, Network, ScriptBuf, Transaction, XOnlyPublicKey}; use clap::builder::TypedValueParser; -use k256::schnorr; use k256::schnorr::signature::Verifier; use rustreexo::accumulator::proof::Proof; use serde::{Deserialize, Serialize}; use k256::PublicKey; -use musig2::{ - AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, SecNonceSpices, - SecondRound, -}; -use sha2::{Digest, Sha512_256}; +use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce}; +use sha2::Digest; use shared::{aggregate_keys, get_leaf_hashes, sort_keypairs, sort_pubkeys, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { @@ -281,7 +276,6 @@ fn main() { println!("musig successfully verified"); - let acc: CliStump = serde_json::from_str(&args.utreexo_acc.unwrap()).unwrap(); let acc = Stump { leaves: acc.leaves, @@ -292,9 +286,6 @@ fn main() { .collect(), }; - - - let proof_type: ProverOpts = match args.proof_type.as_deref() { None => { println!("using default proof type"); @@ -412,12 +403,24 @@ fn main() { } fn verify_receipt(receipt: &Receipt) { - let (node_key1, node_key2, stump_hash, pk_hash, msg): (PublicKey, PublicKey, String, String, Vec) = receipt.journal.decode().unwrap(); + let (node_key1, node_key2, stump_hash, pk_hash, msg): ( + PublicKey, + PublicKey, + String, + String, + Vec, + ) = receipt.journal.decode().unwrap(); // The receipt was verified at the end of proving, but the below code is an // example of how someone else could verify this receipt. - println!("committed node_key1 : {}", hex::encode(&node_key1.to_sec1_bytes())); - println!("committed node_key2 : {}", hex::encode(&node_key2.to_sec1_bytes())); + println!( + "committed node_key1 : {}", + hex::encode(&node_key1.to_sec1_bytes()) + ); + println!( + "committed node_key2 : {}", + hex::encode(&node_key2.to_sec1_bytes()) + ); println!("bitcoin keys hash: {}", pk_hash); println!("signed msg: {}", hex::encode(msg)); println!("stump hash: {}", stump_hash); @@ -426,12 +429,12 @@ fn verify_receipt(receipt: &Receipt) { println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); } -fn to_bytes(h: [u32;8]) -> [u8; 32] { +fn to_bytes(h: [u32; 8]) -> [u8; 32] { let mut buf = [0u8; 32]; for i in 0..8 { let b: [u8; 4] = h[i].to_be_bytes(); for j in 0..4 { - buf[i*4+j] = b[j]; + buf[i * 4 + j] = b[j]; } } diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index d362970..2904a23 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -5,61 +5,11 @@ use rustreexo::accumulator::node_hash::NodeHash; use rustreexo::accumulator::proof::Proof; use rustreexo::accumulator::stump::Stump; use sha2::{Digest, Sha512_256}; - -use bitcoin::key::{UntweakedPublicKey}; -use bitcoin::{ScriptBuf, Transaction, BlockHash, TapNodeHash, TapTweakHash, WitnessVersion, XOnlyPublicKey}; -use bitcoin::script::{Builder, PushBytes}; -use k256::schnorr; -use k256::schnorr::signature::Verifier; -use k256::elliptic_curve::sec1::ToEncodedPoint; +use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; use k256::PublicKey; -use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys}; - -pub fn new_p2tr( - internal_key: UntweakedPublicKey, - merkle_root: Option, -) -> ScriptBuf { - let output_key = tap_tweak(internal_key, merkle_root); - // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) - new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) -} - -fn new_witness_program_unchecked>( - version: WitnessVersion, - program: T, -) -> ScriptBuf { - let program = program.as_ref(); - debug_assert!(program.len() >= 2 && program.len() <= 40); - // In segwit v0, the program must be 20 or 32 bytes long. - debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32); - Builder::new().push_opcode(version.into()).push_slice(program).into_script() -} - - -fn tap_tweak( - internal_key: UntweakedPublicKey, - merkle_root: Option, -) -> XOnlyPublicKey { - let tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root).to_scalar(); - - let pub_bytes = internal_key.serialize(); - let pub_key : k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes).unwrap().into(); - let pub_point = pub_key.to_projective(); - - let tweak_bytes = &tweak.to_be_bytes(); - let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()).unwrap().public_key().to_projective(); - - let tweaked_point = pub_point + tweak_point; - let compressed = tweaked_point.to_encoded_point(true); - let x_coordinate = compressed.x().unwrap(); - - let ver_key = schnorr::VerifyingKey::from_bytes(&x_coordinate).unwrap(); - - let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); +use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys, new_p2tr}; - pubx -} fn main() { //TODO: take in nodeid1, nodeid2, bitcoinkey1, bitcoinkey2 need tweak? diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 70343ea..ff0217e 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,14 +4,16 @@ use bitcoin_hashes::Hash as BitcoinHash; use sha2::{Digest, Sha512_256}; use bitcoin::consensus::Encodable; -use bitcoin::key::Keypair; -use bitcoin::{BlockHash, Transaction}; +use bitcoin::key::{Keypair, UntweakedPublicKey}; +use bitcoin::script::{Builder, PushBytes}; +use bitcoin::{ + BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, WitnessVersion, XOnlyPublicKey, +}; +use k256::schnorr; use k256::PublicKey; -use musig2::{ - k256, AggNonce, FirstRound, KeyAggContext, PartialSignature, PubNonce, SecNonce, - SecNonceSpices, SecondRound, -}; +use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; +use musig2::{k256, KeyAggContext}; pub const UTREEXO_TAG_V1: [u8; 64] = [ 0x5b, 0x83, 0x2d, 0xb8, 0xca, 0x26, 0xc2, 0x5b, 0xe1, 0xc5, 0x42, 0xd6, 0xcc, 0xed, 0xdd, 0xa8, @@ -74,3 +76,48 @@ pub fn sort_pubkeys(pubkeys: &mut Vec) { pub fn sort_keypairs(kp: &mut Vec) { kp.sort_by(|a, b| a.public_key().serialize().cmp(&b.public_key().serialize())); } +pub fn new_p2tr(internal_key: UntweakedPublicKey, merkle_root: Option) -> ScriptBuf { + let output_key = tap_tweak(internal_key, merkle_root); + // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) + new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) +} + +fn new_witness_program_unchecked>( + version: WitnessVersion, + program: T, +) -> ScriptBuf { + let program = program.as_ref(); + debug_assert!(program.len() >= 2 && program.len() <= 40); + // In segwit v0, the program must be 20 or 32 bytes long. + debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32); + Builder::new() + .push_opcode(version.into()) + .push_slice(program) + .into_script() +} + +fn tap_tweak(internal_key: UntweakedPublicKey, merkle_root: Option) -> XOnlyPublicKey { + let tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root).to_scalar(); + + let pub_bytes = internal_key.serialize(); + let pub_key: k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes) + .unwrap() + .into(); + let pub_point = pub_key.to_projective(); + + let tweak_bytes = &tweak.to_be_bytes(); + let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) + .unwrap() + .public_key() + .to_projective(); + + let tweaked_point = pub_point + tweak_point; + let compressed = tweaked_point.to_encoded_point(true); + let x_coordinate = compressed.x().unwrap(); + + let ver_key = schnorr::VerifyingKey::from_bytes(&x_coordinate).unwrap(); + + let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); + + pubx +} From c93c0dfc702aede564cfa274d5fd04a147322ceb Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 12:20:44 +0100 Subject: [PATCH 24/44] shared: remove uneccesary pubkey parse --- shared/src/lib.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index ff0217e..92d65a8 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,4 +1,4 @@ -use bitcoin_hashes::sha256; +use bitcoin_hashes::{sha256, HashEngine}; use bitcoin_hashes::Hash as BitcoinHash; use sha2::{Digest, Sha512_256}; @@ -7,7 +7,7 @@ use bitcoin::consensus::Encodable; use bitcoin::key::{Keypair, UntweakedPublicKey}; use bitcoin::script::{Builder, PushBytes}; use bitcoin::{ - BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, WitnessVersion, XOnlyPublicKey, + BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, WitnessVersion, }; use k256::schnorr; use k256::PublicKey; @@ -76,10 +76,10 @@ pub fn sort_pubkeys(pubkeys: &mut Vec) { pub fn sort_keypairs(kp: &mut Vec) { kp.sort_by(|a, b| a.public_key().serialize().cmp(&b.public_key().serialize())); } -pub fn new_p2tr(internal_key: UntweakedPublicKey, merkle_root: Option) -> ScriptBuf { - let output_key = tap_tweak(internal_key, merkle_root); +pub fn new_p2tr(internal_key_bytes: [u8; 32], merkle_root: Option) -> ScriptBuf { + let output_key = tap_tweak(internal_key_bytes, merkle_root); // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) - new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) + new_witness_program_unchecked(WitnessVersion::V1, output_key) } fn new_witness_program_unchecked>( @@ -96,10 +96,12 @@ fn new_witness_program_unchecked>( .into_script() } -fn tap_tweak(internal_key: UntweakedPublicKey, merkle_root: Option) -> XOnlyPublicKey { - let tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root).to_scalar(); +fn tap_tweak(internal_key_bytes: [u8; 32], merkle_root: Option) -> [u8; 32] { + let mut eng = TapTweakHash::engine(); + eng.input(&internal_key_bytes); + let tweak= TapTweakHash::from_engine(eng).to_scalar(); - let pub_bytes = internal_key.serialize(); + let pub_bytes = internal_key_bytes; let pub_key: k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes) .unwrap() .into(); @@ -117,7 +119,7 @@ fn tap_tweak(internal_key: UntweakedPublicKey, merkle_root: Option) let ver_key = schnorr::VerifyingKey::from_bytes(&x_coordinate).unwrap(); - let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); + let pubx: [u8; 32] = ver_key.to_bytes().try_into().unwrap(); pubx } From fa8e6e740a442a9a61b0a85413d50eec64a14e0d Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 12:25:38 +0100 Subject: [PATCH 25/44] shared: remove scalar conversion --- shared/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 92d65a8..c416df2 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -99,7 +99,7 @@ fn new_witness_program_unchecked>( fn tap_tweak(internal_key_bytes: [u8; 32], merkle_root: Option) -> [u8; 32] { let mut eng = TapTweakHash::engine(); eng.input(&internal_key_bytes); - let tweak= TapTweakHash::from_engine(eng).to_scalar(); + let tweak_hash = TapTweakHash::from_engine(eng); let pub_bytes = internal_key_bytes; let pub_key: k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes) @@ -107,7 +107,7 @@ fn tap_tweak(internal_key_bytes: [u8; 32], merkle_root: Option) -> .into(); let pub_point = pub_key.to_projective(); - let tweak_bytes = &tweak.to_be_bytes(); + let tweak_bytes = &tweak_hash.to_byte_array(); let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) .unwrap() .public_key() From 36287f85960c3f0e7c76be076f880b98948d8c72 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 12:55:09 +0100 Subject: [PATCH 26/44] shared: remove pubkey parsing --- methods/guest/src/main.rs | 5 +---- shared/src/lib.rs | 15 ++++++--------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 2904a23..f10858b 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -49,14 +49,11 @@ fn main() { sort_pubkeys(&mut bitcoin_keys); let tap_pub = aggregate_keys(bitcoin_keys); - let pub_bytes : [u8; 32]= tap_pub.to_sec1_bytes()[1..].try_into().unwrap(); - let pubx = XOnlyPublicKey::from_slice(&pub_bytes).unwrap(); - let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); // We'll check that the given public key corresponds to an output in the utxo set. - let script_pubkey = new_p2tr(pubx, None); + let script_pubkey = new_p2tr(tap_pub, None); // assert internal key is in tx used to calc leaf hash assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); diff --git a/shared/src/lib.rs b/shared/src/lib.rs index c416df2..360685f 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -76,8 +76,8 @@ pub fn sort_pubkeys(pubkeys: &mut Vec) { pub fn sort_keypairs(kp: &mut Vec) { kp.sort_by(|a, b| a.public_key().serialize().cmp(&b.public_key().serialize())); } -pub fn new_p2tr(internal_key_bytes: [u8; 32], merkle_root: Option) -> ScriptBuf { - let output_key = tap_tweak(internal_key_bytes, merkle_root); +pub fn new_p2tr(internal_key: PublicKey, merkle_root: Option) -> ScriptBuf { + let output_key = tap_tweak(internal_key, merkle_root); // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) new_witness_program_unchecked(WitnessVersion::V1, output_key) } @@ -96,16 +96,13 @@ fn new_witness_program_unchecked>( .into_script() } -fn tap_tweak(internal_key_bytes: [u8; 32], merkle_root: Option) -> [u8; 32] { +fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; 32] { + let x_only_bytes : [u8; 32]= internal_key.to_sec1_bytes()[1..].try_into().unwrap(); let mut eng = TapTweakHash::engine(); - eng.input(&internal_key_bytes); + eng.input(&x_only_bytes); let tweak_hash = TapTweakHash::from_engine(eng); - let pub_bytes = internal_key_bytes; - let pub_key: k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes) - .unwrap() - .into(); - let pub_point = pub_key.to_projective(); + let pub_point = internal_key.to_projective(); let tweak_bytes = &tweak_hash.to_byte_array(); let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) From 3b67d8c0609960d3b4c044e49b41de875d60ef14 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 13:00:23 +0100 Subject: [PATCH 27/44] shared: remove x coordinate parsing --- shared/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 360685f..4992659 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,12 +4,11 @@ use bitcoin_hashes::Hash as BitcoinHash; use sha2::{Digest, Sha512_256}; use bitcoin::consensus::Encodable; -use bitcoin::key::{Keypair, UntweakedPublicKey}; +use bitcoin::key::{Keypair}; use bitcoin::script::{Builder, PushBytes}; use bitcoin::{ BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, WitnessVersion, }; -use k256::schnorr; use k256::PublicKey; use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; @@ -114,9 +113,7 @@ fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; let compressed = tweaked_point.to_encoded_point(true); let x_coordinate = compressed.x().unwrap(); - let ver_key = schnorr::VerifyingKey::from_bytes(&x_coordinate).unwrap(); - - let pubx: [u8; 32] = ver_key.to_bytes().try_into().unwrap(); + let pubx: [u8; 32] = x_coordinate.as_slice().try_into().unwrap(); pubx } From 4c31e6d21e3930d9417cffcc296dccb9fec732e6 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 12 Feb 2025 21:18:05 +0100 Subject: [PATCH 28/44] shared: remove ineffective patch Already patched in workspace root --- shared/Cargo.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/shared/Cargo.toml b/shared/Cargo.toml index dc58a9c..d2094dd 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -7,9 +7,4 @@ edition = "2021" bitcoin = { version = "0.32.5"} sha2 = "0.10.8" bitcoin_hashes = "0.14.0" -musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } - -[patch.crates-io] -# Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint -# multiplication accelerator support for all downstream usages of the following crates. -sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } +musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } \ No newline at end of file From 8a2342d99ed97b7c702f7b2231afce94136e1cd1 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 13 Feb 2025 10:40:43 +0100 Subject: [PATCH 29/44] docs/ln_gossip fixups --- docs/ln_gossip.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/ln_gossip.md b/docs/ln_gossip.md index 9cee7b3..e85eff8 100644 --- a/docs/ln_gossip.md +++ b/docs/ln_gossip.md @@ -14,7 +14,10 @@ privacy conscious users. TODO ### Taproot gossip (gossip v1.75) -TODO: desribe current proposal +See Elle's deep dive here: [Updates to the Gossip 1.75 proposal post LN summit meeting](https://delvingbitcoin.org/t/updates-to-the-gossip-1-75-proposal-post-ln-summit-meeting/1202). + +Tl;dr: a new `channel_announcement_2` message that carries a Musig2 signature +proving the two nodes control a certain UTXO. Example `channel_announcement_2`: ```json @@ -109,7 +112,7 @@ It is also assumed that proving time will come down as advances are made in proof systems and hardware acceleration. ### Handling received channel_announcement_zk -When a node receives a `channel_accnouncement_zk` message, it will first use +When a node receives a `channel_announcement_zk` message, it will first use the `pk_hash` to check whether this is a channel already known to the node. The `pk_hash` is deterministic and unique per channel. It will then verify the proof if it has a type known to the node. Otherwise it will ignore it. From 50cb2c239134f14033c47f9c608e718b6e262524 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 18 Feb 2025 11:57:31 -0500 Subject: [PATCH 30/44] blind using beta*G This probably is not safe, since prover can choose beta*G to fit any taproot on-chain and then show inlcusion proof for it. --- host/src/main.rs | 62 +++++++++++++++++++++++---------------- methods/guest/src/main.rs | 61 ++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 53 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 64e664e..664981f 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -247,6 +247,12 @@ fn main() { println!("tap key : {}", hex::encode(&tap_bytes)); address(&secp, tap_key, network); + let tap_blind_point = pub_bitcoin1.to_projective() + pub_bitcoin2.to_projective(); + let tap_blind_key: PublicKey = tap_blind_point.try_into().unwrap(); + println!("tap blind key : {}", hex::encode(&tap_blind_key.to_sec1_bytes())); + address(&secp, tap_blind_key, network); + let tap_bytes = tap_blind_key.to_sec1_bytes(); + let musig_sig = match args.musig_sig { Some(musig_sig) => hex::decode(musig_sig).unwrap(), @@ -358,8 +364,8 @@ fn main() { let start_time = SystemTime::now(); let env = ExecutorEnv::builder() - .write(&msg_to_sign) - .unwrap() + //.write(&msg_to_sign) + //.unwrap() .write(&acc) .unwrap() .write(&proof) @@ -372,10 +378,16 @@ fn main() { .unwrap() .write(&block_hash) .unwrap() - .write(&all_pubs) + // Pubkey + .write(&pub_bitcoin1) .unwrap() - .write(&musig_sig.as_slice()) + // Blinding key + .write(&pub_bitcoin2) .unwrap() +// .write(&all_pubs) +// .unwrap() +// .write(&musig_sig.as_slice()) +// .unwrap() .build() .unwrap(); @@ -403,27 +415,27 @@ fn main() { } fn verify_receipt(receipt: &Receipt) { - let (node_key1, node_key2, stump_hash, pk_hash, msg): ( - PublicKey, - PublicKey, - String, - String, - Vec, - ) = receipt.journal.decode().unwrap(); - - // The receipt was verified at the end of proving, but the below code is an - // example of how someone else could verify this receipt. - println!( - "committed node_key1 : {}", - hex::encode(&node_key1.to_sec1_bytes()) - ); - println!( - "committed node_key2 : {}", - hex::encode(&node_key2.to_sec1_bytes()) - ); - println!("bitcoin keys hash: {}", pk_hash); - println!("signed msg: {}", hex::encode(msg)); - println!("stump hash: {}", stump_hash); + //let (node_key1, node_key2, stump_hash, pk_hash, msg): ( + // PublicKey, + // PublicKey, + // String, + // String, + // Vec, + //) = receipt.journal.decode().unwrap(); + + //// The receipt was verified at the end of proving, but the below code is an + //// example of how someone else could verify this receipt. + //println!( + // "committed node_key1 : {}", + // hex::encode(&node_key1.to_sec1_bytes()) + //); + //println!( + // "committed node_key2 : {}", + // hex::encode(&node_key2.to_sec1_bytes()) + //); + //println!("bitcoin keys hash: {}", pk_hash); + //println!("signed msg: {}", hex::encode(msg)); + //println!("stump hash: {}", stump_hash); receipt.verify(METHOD_ID).unwrap(); println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index f10858b..6198ec7 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -20,7 +20,7 @@ fn main() { // read the input - let msg_bytes: Vec = env::read(); +// let msg_bytes: Vec = env::read(); let s: Stump = env::read(); let proof: Proof = env::read(); @@ -28,26 +28,31 @@ fn main() { let vout: u32 = env::read(); let block_height: u32 = env::read(); let block_hash: BlockHash = env::read(); - let all_pubs: Vec = env::read(); - let musig_sig_bytes: Vec = env::read(); - let mut musig_pubs = all_pubs.clone(); - sort_pubkeys(&mut musig_pubs); - - assert_eq!( - verify_musig(musig_pubs.clone(), musig_sig_bytes.clone().try_into().unwrap(), &msg_bytes), - true, - ); - - let node_key1 = all_pubs[0]; - let node_key2 = all_pubs[1]; - - // Aggregate the bitcoin keys. - let bitcoin_key1 = all_pubs[2]; - let bitcoin_key2 = all_pubs[3]; - let mut bitcoin_keys =vec![bitcoin_key1, bitcoin_key2]; - sort_pubkeys(&mut bitcoin_keys); - let tap_pub = aggregate_keys(bitcoin_keys); + // P + blinding key + let p_out: PublicKey = env::read(); + let blind: PublicKey = env::read(); + let tap_point = p_out.to_projective() + blind.to_projective(); + let tap_pub: PublicKey = tap_point.try_into().unwrap(); + // let musig_sig_bytes: Vec = env::read(); + +// let mut musig_pubs = all_pubs.clone(); +// sort_pubkeys(&mut musig_pubs); +// +// assert_eq!( +// verify_musig(musig_pubs.clone(), musig_sig_bytes.clone().try_into().unwrap(), &msg_bytes), +// true, +// ); +// +// let node_key1 = all_pubs[0]; +// let node_key2 = all_pubs[1]; +// +// // Aggregate the bitcoin keys. +// let bitcoin_key1 = all_pubs[2]; +// let bitcoin_key2 = all_pubs[3]; +// let mut bitcoin_keys =vec![bitcoin_key1, bitcoin_key2]; +// sort_pubkeys(&mut bitcoin_keys); +// let tap_pub = aggregate_keys(bitcoin_keys); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); let leaf_hash = NodeHash::from(lh); @@ -61,19 +66,19 @@ fn main() { // Assert it is in the set. assert_eq!(s.verify(&proof, &[leaf_hash]), Ok(true)); - let mut hasher = Sha512_256::new(); - hasher.update(&bitcoin_key1.to_sec1_bytes()); - hasher.update(&bitcoin_key2.to_sec1_bytes()); - let pk_hash = hex::encode(hasher.finalize()); + //let mut hasher = Sha512_256::new(); + //hasher.update(&bitcoin_key1.to_sec1_bytes()); + //hasher.update(&bitcoin_key2.to_sec1_bytes()); + //let pk_hash = hex::encode(hasher.finalize()); let mut shasher = Sha512_256::new(); s.serialize(&mut shasher).unwrap(); let stump_hash = hex::encode(shasher.finalize()); // write public output to the journal - env::commit(&node_key1); - env::commit(&node_key2); + env::commit(&p_out); + //env::commit(&node_key2); env::commit(&stump_hash); - env::commit(&pk_hash); - env::commit(&msg_bytes); + //env::commit(&pk_hash); + //env::commit(&msg_bytes); } \ No newline at end of file From 3ee4630838801ce2320e09927ae5d30521d775d3 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 18 Feb 2025 14:20:17 -0500 Subject: [PATCH 31/44] shared: fix even/odd tweak add --- shared/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 4992659..e56377c 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -101,7 +101,6 @@ fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; eng.input(&x_only_bytes); let tweak_hash = TapTweakHash::from_engine(eng); - let pub_point = internal_key.to_projective(); let tweak_bytes = &tweak_hash.to_byte_array(); let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) @@ -109,7 +108,13 @@ fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; .public_key() .to_projective(); - let tweaked_point = pub_point + tweak_point; + let pub_point = internal_key.to_projective(); + let pub_affine = internal_key.as_affine(); + let tweaked_point = if pub_affine.y_is_odd().unwrap_u8() == 1 { + tweak_point - pub_point + } else { + pub_point + tweak_point + }; let compressed = tweaked_point.to_encoded_point(true); let x_coordinate = compressed.x().unwrap(); From b01bdb7af82fffa5d191d9b55f0b1554ba059d7a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 18 Feb 2025 16:17:37 -0500 Subject: [PATCH 32/44] shared: use custom txid calc So we can use the sha256 precompile --- shared/src/lib.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index e56377c..b2ea124 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,7 +1,7 @@ -use bitcoin_hashes::{sha256, HashEngine}; +use bitcoin_hashes::HashEngine; use bitcoin_hashes::Hash as BitcoinHash; -use sha2::{Digest, Sha512_256}; +use sha2::{Digest, Sha256, Sha512_256}; use bitcoin::consensus::Encodable; use bitcoin::key::{Keypair}; @@ -26,7 +26,7 @@ pub fn get_leaf_hashes( vout: u32, height: u32, block_hash: BlockHash, -) -> sha256::Hash { +) -> [u8; 32] { let header_code = height << 1; let mut ser_utxo = Vec::new(); @@ -37,7 +37,7 @@ pub fn get_leaf_hashes( } else { header_code }; - let txid = transaction.compute_txid(); + let txid = compute_txid(&transaction); println!("txid: {txid}, block_hash: {block_hash} vout: {vout} height: {height}"); let leaf_hash = Sha512_256::new() @@ -49,7 +49,21 @@ pub fn get_leaf_hashes( .chain_update(header_code.to_le_bytes()) .chain_update(ser_utxo) .finalize(); - sha256::Hash::from_slice(leaf_hash.as_slice()).expect("parent_hash: Engines shouldn't be Err") + leaf_hash.try_into().unwrap() +} + +pub fn compute_txid(tx: &Transaction) -> Txid { + let mut enc = Vec::new(); + tx.version.consensus_encode(&mut enc).expect("engines don't error"); + tx.input.consensus_encode(&mut enc).expect("engines don't error"); + tx.output.consensus_encode(&mut enc).expect("engines don't error"); + tx.lock_time.consensus_encode(&mut enc).expect("engines don't error"); + + // Compute double SHA-256 hash + let hash_result = Sha256::digest(Sha256::digest(&enc)); + + // Convert the hash result to a Txid + Txid::from_slice(&hash_result).expect("hash should be valid Txid") } pub fn aggregate_keys(pubs: Vec) -> PublicKey { From 235d8fc5bfd0db6ef22f2436b8401a02831ef975 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Tue, 18 Feb 2025 16:45:30 -0500 Subject: [PATCH 33/44] input blind secret --- host/src/main.rs | 39 +++++++++++++++++++++++++++++---------- methods/guest/src/main.rs | 7 ++++++- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 664981f..68b7ada 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -16,7 +16,7 @@ use std::time::SystemTime; use bitcoin::consensus::deserialize; use bitcoin::key::Keypair; use bitcoin::secp256k1::{rand, Secp256k1, SecretKey, Signing, Verification}; -use bitcoin::{Address, BlockHash, Network, ScriptBuf, Transaction, XOnlyPublicKey}; +use bitcoin::{Address, BlockHash, Network, ScriptBuf, TapTweakHash, Transaction, XOnlyPublicKey}; use clap::builder::TypedValueParser; use k256::schnorr::signature::Verifier; use rustreexo::accumulator::proof::Proof; @@ -85,6 +85,9 @@ struct Args { #[arg(long)] bitcoin_key_2_priv: Option, + #[arg(long)] + blind_secret: Option, + #[arg(long)] node_key_1: Option, @@ -227,6 +230,11 @@ fn main() { None => parse_pubkey(&args.bitcoin_key_2.unwrap()), }; + let blind_str = args.blind_secret.unwrap(); + println!("blind secret: {}", blind_str); + let blind_bytes: [u8; 32] = hex::decode(blind_str).unwrap().try_into().unwrap(); + let blind = k256::SecretKey::from_bytes(&blind_bytes.into()).unwrap(); + sort_keypairs(&mut keypairs); let msg_to_sign = hex::decode(args.msg_hex.unwrap()).unwrap(); @@ -247,11 +255,14 @@ fn main() { println!("tap key : {}", hex::encode(&tap_bytes)); address(&secp, tap_key, network); - let tap_blind_point = pub_bitcoin1.to_projective() + pub_bitcoin2.to_projective(); + let blind_pub = blind.public_key(); + let tap_blind_point = pub_bitcoin1.to_projective() + blind_pub.to_projective(); let tap_blind_key: PublicKey = tap_blind_point.try_into().unwrap(); - println!("tap blind key : {}", hex::encode(&tap_blind_key.to_sec1_bytes())); + println!( + "tap blind key : {}", + hex::encode(&tap_blind_key.to_sec1_bytes()) + ); address(&secp, tap_blind_key, network); - let tap_bytes = tap_blind_key.to_sec1_bytes(); let musig_sig = match args.musig_sig { Some(musig_sig) => hex::decode(musig_sig).unwrap(), @@ -350,10 +361,18 @@ fn main() { assert_eq!(lh, leaf_hash); // We will prove inclusion in the UTXO set of the key we control. + let tap_bytes = tap_blind_key.to_sec1_bytes(); let internal_key = XOnlyPublicKey::from_slice(&tap_bytes[1..]).unwrap(); println!("xonly tap key: {}", hex::encode(internal_key.serialize())); - let script_pubkey = ScriptBuf::new_p2tr(&secp, internal_key, None); + let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, None); + println!("secp tweak hash: {}", tweak_hash); + + let script_pubkey = shared::secp_new_p2tr(&secp, internal_key, None); + let script_pub2 = shared::new_p2tr(tap_blind_key, None); + + println!("script_pubKey: {}", script_pubkey); + println!("script_pubKey2: {}", script_pub2); assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); @@ -382,12 +401,12 @@ fn main() { .write(&pub_bitcoin1) .unwrap() // Blinding key - .write(&pub_bitcoin2) + .write(&blind_bytes) .unwrap() -// .write(&all_pubs) -// .unwrap() -// .write(&musig_sig.as_slice()) -// .unwrap() + // .write(&all_pubs) + // .unwrap() + // .write(&musig_sig.as_slice()) + // .unwrap() .build() .unwrap(); diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 6198ec7..e2a4f2d 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -7,6 +7,7 @@ use rustreexo::accumulator::stump::Stump; use sha2::{Digest, Sha512_256}; use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; use k256::PublicKey; +use k256::SecretKey; use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys, new_p2tr}; @@ -31,9 +32,13 @@ fn main() { // P + blinding key let p_out: PublicKey = env::read(); - let blind: PublicKey = env::read(); + let blind_secret_bytes: [u8; 32] = env::read(); + eprintln!("blind_secret_bytes: {}", hex::encode(blind_secret_bytes)); + let blind_secret = SecretKey::from_bytes(&blind_secret_bytes.into()).unwrap(); + let blind = blind_secret.public_key(); let tap_point = p_out.to_projective() + blind.to_projective(); let tap_pub: PublicKey = tap_point.try_into().unwrap(); + eprintln!("tap blind key : {}", hex::encode(&tap_pub.to_sec1_bytes())); // let musig_sig_bytes: Vec = env::read(); // let mut musig_pubs = all_pubs.clone(); From efacfcaee60b69b9ef081266e0375f4dff67e472 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 19 Feb 2025 12:07:47 -0500 Subject: [PATCH 34/44] bump rustreexo to v0.4.0 --- host/Cargo.toml | 2 +- methods/guest/Cargo.toml | 2 +- shared/src/node_hash.rs | 335 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 shared/src/node_hash.rs diff --git a/host/Cargo.toml b/host/Cargo.toml index f6605d2..a7b5c63 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -9,7 +9,7 @@ shared = { path = "../shared" } risc0-zkvm = { version = "1.2.3" , features = ["unstable"]} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" -rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } +rustreexo = { version = "0.4.0", features = ["with-serde"] } bitcoin = { version = "0.32.5", features = ["std", "rand-std", "serde"] } bincode = "1.3.3" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 7912026..b61857e 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std', 'unstable'] } -rustreexo = { git = "https://github.com/mit-dci/rustreexo.git", rev = "071df44830139ada6cba73098b98e81a7316e4b8", features = ["with-serde"] } +rustreexo = { version = "0.4.0", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/shared/src/node_hash.rs b/shared/src/node_hash.rs new file mode 100644 index 0000000..f8af758 --- /dev/null +++ b/shared/src/node_hash.rs @@ -0,0 +1,335 @@ +//! [AccumulatorHash] is an internal type for representing Hashes in an utreexo accumulator. It's +//! just a wrapper around [[u8; 32]] but with some useful methods. +//! # Examples +//! Building from a str +//! ``` +//! use std::str::FromStr; +//! +//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; +//! let hash = BitcoinNodeHash::from_str( +//! "0000000000000000000000000000000000000000000000000000000000000000", +//! ) +//! .unwrap(); +//! assert_eq!( +//! hash.to_string().as_str(), +//! "0000000000000000000000000000000000000000000000000000000000000000" +//! ); +//! ``` +//! Building from a slice +//! ``` +//! use std::str::FromStr; +//! +//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; +//! let hash1 = BitcoinNodeHash::new([0; 32]); +//! // ... or ... +//! let hash2 = BitcoinNodeHash::from([0; 32]); +//! assert_eq!(hash1, hash2); +//! assert_eq!( +//! hash1.to_string().as_str(), +//! "0000000000000000000000000000000000000000000000000000000000000000" +//! ); +//! ``` +//! +//! Computing a parent hash (i.e a hash of two nodes concatenated) +//! ``` +//! use std::str::FromStr; +//! +//! use rustreexo::accumulator::node_hash::AccumulatorHash; +//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; +//! let left = BitcoinNodeHash::new([0; 32]); +//! let right = BitcoinNodeHash::new([1; 32]); +//! let parent = BitcoinNodeHash::parent_hash(&left, &right); +//! let expected_parent = BitcoinNodeHash::from_str( +//! "34e33ca0c40b7bd33d28932ca9e35170def7309a3bf91ecda5e1ceb067548a12", +//! ) +//! .unwrap(); +//! assert_eq!(parent, expected_parent); +//! ``` +use std::convert::TryFrom; +use std::fmt::Debug; +use std::fmt::Display; +use std::ops::Deref; +use std::str::FromStr; + +use rustreexo::accumulator::node_hash::{AccumulatorHash, NodeHash}; + +use bitcoin_hashes::hex; +use bitcoin_hashes::sha256; +use bitcoin_hashes::sha512_256; +use bitcoin_hashes::Hash; +use bitcoin_hashes::HashEngine; +use serde::Deserialize; +use serde::Serialize; + + +#[derive(Eq, PartialEq, Copy, Clone, Hash, PartialOrd, Ord, Serialize, Deserialize)] +/// AccumulatorHash is a wrapper around a 32 byte array that represents a hash of a node in the tree. +/// # Example +/// ``` +/// use rustreexo::accumulator::node_hash::BitcoinNodeHash; +/// let hash = BitcoinNodeHash::new([0; 32]); +/// assert_eq!( +/// hash.to_string().as_str(), +/// "0000000000000000000000000000000000000000000000000000000000000000" +/// ); +/// ``` +#[derive(Default)] +pub enum BitcoinNodeHash { + #[default] + Empty, + Placeholder, + Some([u8; 32]), +} + +impl Deref for BitcoinNodeHash { + type Target = [u8; 32]; + + fn deref(&self) -> &Self::Target { + match self { + BitcoinNodeHash::Some(ref inner) => inner, + _ => &[0; 32], + } + } +} + +impl Display for BitcoinNodeHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + if let BitcoinNodeHash::Some(ref inner) = self { + let mut s = String::new(); + for byte in inner.iter() { + s.push_str(&format!("{:02x}", byte)); + } + write!(f, "{}", s) + } else { + write!(f, "empty") + } + } +} + +impl Debug for BitcoinNodeHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + BitcoinNodeHash::Empty => write!(f, "empty"), + BitcoinNodeHash::Placeholder => write!(f, "placeholder"), + BitcoinNodeHash::Some(ref inner) => { + let mut s = String::new(); + for byte in inner.iter() { + s.push_str(&format!("{:02x}", byte)); + } + write!(f, "{}", s) + } + } + } +} + +impl From for BitcoinNodeHash { + fn from(hash: sha512_256::Hash) -> Self { + BitcoinNodeHash::Some(hash.to_byte_array()) + } +} + +impl From<[u8; 32]> for BitcoinNodeHash { + fn from(hash: [u8; 32]) -> Self { + BitcoinNodeHash::Some(hash) + } +} + +impl From<&[u8; 32]> for BitcoinNodeHash { + fn from(hash: &[u8; 32]) -> Self { + BitcoinNodeHash::Some(*hash) + } +} + +#[cfg(test)] +impl TryFrom<&str> for BitcoinNodeHash { + type Error = hex::HexToArrayError; + fn try_from(hash: &str) -> Result { + // This implementation is useful for testing, as it allows to create empty hashes + // from the string of 64 zeros. Without this, it would be impossible to express this + // hash in the test vectors. + if hash == "0000000000000000000000000000000000000000000000000000000000000000" { + return Ok(BitcoinNodeHash::Empty); + } + + let hash = hex::FromHex::from_hex(hash)?; + Ok(BitcoinNodeHash::Some(hash)) + } +} + +#[cfg(not(test))] +impl TryFrom<&str> for BitcoinNodeHash { + type Error = hex::HexToArrayError; + fn try_from(hash: &str) -> Result { + let inner = hex::FromHex::from_hex(hash)?; + Ok(BitcoinNodeHash::Some(inner)) + } +} + +impl From<&[u8]> for BitcoinNodeHash { + fn from(hash: &[u8]) -> Self { + let mut inner = [0; 32]; + inner.copy_from_slice(hash); + BitcoinNodeHash::Some(inner) + } +} + +impl From for BitcoinNodeHash { + fn from(hash: sha256::Hash) -> Self { + BitcoinNodeHash::Some(hash.to_byte_array()) + } +} + +impl FromStr for BitcoinNodeHash { + fn from_str(s: &str) -> Result { + BitcoinNodeHash::try_from(s) + } + + type Err = hex::HexToArrayError; +} + +impl BitcoinNodeHash { + /// Creates a new AccumulatorHash from a 32 byte array. + /// # Example + /// ``` + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// let hash = BitcoinNodeHash::new([0; 32]); + /// assert_eq!( + /// hash.to_string().as_str(), + /// "0000000000000000000000000000000000000000000000000000000000000000" + /// ); + /// ``` + pub fn new(inner: [u8; 32]) -> Self { + BitcoinNodeHash::Some(inner) + } +} + +impl AccumulatorHash for BitcoinNodeHash { + /// Tells whether this hash is empty. We use empty hashes throughout the code to represent + /// leaves we want to delete. + fn is_empty(&self) -> bool { + matches!(self, BitcoinNodeHash::Empty) + } + + /// Creates an empty hash. This is used to represent leaves we want to delete. + /// # Example + /// ``` + /// use rustreexo::accumulator::node_hash::AccumulatorHash; + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// let hash = BitcoinNodeHash::empty(); + /// assert!(hash.is_empty()); + /// ``` + fn empty() -> Self { + BitcoinNodeHash::Empty + } + + /// parent_hash return the merkle parent of the two passed in nodes. + /// # Example + /// ``` + /// use std::str::FromStr; + /// + /// use rustreexo::accumulator::node_hash::AccumulatorHash; + /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; + /// let left = BitcoinNodeHash::new([0; 32]); + /// let right = BitcoinNodeHash::new([1; 32]); + /// let parent = BitcoinNodeHash::parent_hash(&left, &right); + /// let expected_parent = BitcoinNodeHash::from_str( + /// "34e33ca0c40b7bd33d28932ca9e35170def7309a3bf91ecda5e1ceb067548a12", + /// ) + /// .unwrap(); + /// assert_eq!(parent, expected_parent); + /// ``` + fn parent_hash(left: &Self, right: &Self) -> Self { + let mut hash = sha512_256::Hash::engine(); + hash.input(&**left); + hash.input(&**right); + sha512_256::Hash::from_engine(hash).into() + } + + fn is_placeholder(&self) -> bool { + matches!(self, BitcoinNodeHash::Placeholder) + } + + /// Returns a arbitrary placeholder hash that is unlikely to collide with any other hash. + /// We use this while computing roots to destroy. Don't confuse this with an empty hash. + fn placeholder() -> Self { + BitcoinNodeHash::Placeholder + } + + /// write to buffer + fn write(&self, writer: &mut W) -> std::io::Result<()> + where + W: std::io::Write, + { + match self { + Self::Empty => writer.write_all(&[0]), + Self::Placeholder => writer.write_all(&[1]), + Self::Some(hash) => { + writer.write_all(&[2])?; + writer.write_all(hash) + } + } + } + + /// Read from buffer + fn read(reader: &mut R) -> std::io::Result + where + R: std::io::Read, + { + let mut tag = [0]; + reader.read_exact(&mut tag)?; + match tag { + [0] => Ok(Self::Empty), + [1] => Ok(Self::Placeholder), + [2] => { + let mut hash = [0; 32]; + reader.read_exact(&mut hash)?; + Ok(Self::Some(hash)) + } + [_] => { + let err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + "unexpected tag for AccumulatorHash", + ); + Err(err) + } + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::AccumulatorHash; + use crate::accumulator::node_hash::BitcoinNodeHash; + use crate::accumulator::util::hash_from_u8; + + #[test] + fn test_parent_hash() { + let hash1 = hash_from_u8(0); + let hash2 = hash_from_u8(1); + + let parent_hash = BitcoinNodeHash::parent_hash(&hash1, &hash2); + assert_eq!( + parent_hash.to_string().as_str(), + "02242b37d8e851f1e86f46790298c7097df06893d6226b7c1453c213e91717de" + ); + } + #[test] + fn test_hash_from_str() { + let hash = BitcoinNodeHash::from_str( + "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + ) + .unwrap(); + assert_eq!(hash, hash_from_u8(0)); + } + #[test] + fn test_empty_hash() { + // Only relevant for tests + let hash = BitcoinNodeHash::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + assert_eq!(hash, AccumulatorHash::empty()); + } +} From 1f91f11519ec118c827fe0c73009b6fd4bf8034e Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 19 Feb 2025 12:08:43 -0500 Subject: [PATCH 35/44] shared: commit imports --- shared/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index b2ea124..3dcc0ce 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -4,13 +4,17 @@ use bitcoin_hashes::Hash as BitcoinHash; use sha2::{Digest, Sha256, Sha512_256}; use bitcoin::consensus::Encodable; -use bitcoin::key::{Keypair}; +use bitcoin::key::{ + Keypair, Parity, Secp256k1, TweakedPublicKey, UntweakedPublicKey, Verification, +}; use bitcoin::script::{Builder, PushBytes}; use bitcoin::{ - BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, WitnessVersion, + BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, Txid, WitnessVersion, + XOnlyPublicKey, }; use k256::PublicKey; +use musig2::k256::elliptic_curve::point::AffineCoordinates; use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; use musig2::{k256, KeyAggContext}; From c85c919fc48012593f7290c0ec4162a67337e9a0 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 19 Feb 2025 12:10:22 -0500 Subject: [PATCH 36/44] shared: add secp_new_p2tr --- shared/src/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 3dcc0ce..1167cd1 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -113,6 +113,35 @@ fn new_witness_program_unchecked>( .into_script() } +pub fn secp_new_p2tr( + secp: &Secp256k1, + internal_key: UntweakedPublicKey, + merkle_root: Option, +) -> ScriptBuf { + let (output_key, _) = secp_tap_tweak(internal_key, secp, merkle_root); + // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) + new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) +} +fn secp_tap_tweak( + internal_key: UntweakedPublicKey, + secp: &Secp256k1, + merkle_root: Option, +) -> (XOnlyPublicKey, Parity) { + let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); + println!( + "secp internal key: {}", + hex::encode(internal_key.serialize()) + ); + println!("secp tweak hash: {}", tweak_hash); + let tweak = tweak_hash.to_scalar(); + + let (output_key, parity) = internal_key + .add_tweak(secp, &tweak) + .expect("Tap tweak failed"); + + (output_key, parity) +} + fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; 32] { let x_only_bytes : [u8; 32]= internal_key.to_sec1_bytes()[1..].try_into().unwrap(); let mut eng = TapTweakHash::engine(); From b8aedb9513bf607831eee9d5c29e07eee9886f76 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 19 Feb 2025 12:12:31 -0500 Subject: [PATCH 37/44] shared: remove print --- shared/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 1167cd1..916a278 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -128,10 +128,6 @@ fn secp_tap_tweak( merkle_root: Option, ) -> (XOnlyPublicKey, Parity) { let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); - println!( - "secp internal key: {}", - hex::encode(internal_key.serialize()) - ); println!("secp tweak hash: {}", tweak_hash); let tweak = tweak_hash.to_scalar(); From ce75d42f63db046cb2f916d49f6879139a562361 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 13:00:18 -0500 Subject: [PATCH 38/44] change to sha256 leaf hashes --- methods/guest/src/main.rs | 4 +- shared/src/lib.rs | 4 +- shared/src/node_hash.rs | 335 -------------------------------------- 3 files changed, 4 insertions(+), 339 deletions(-) delete mode 100644 shared/src/node_hash.rs diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index e2a4f2d..918aacd 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -4,7 +4,7 @@ use risc0_zkvm::guest::env; use rustreexo::accumulator::node_hash::NodeHash; use rustreexo::accumulator::proof::Proof; use rustreexo::accumulator::stump::Stump; -use sha2::{Digest, Sha512_256}; +use sha2::{Digest, Sha256}; use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; use k256::PublicKey; use k256::SecretKey; @@ -76,7 +76,7 @@ fn main() { //hasher.update(&bitcoin_key2.to_sec1_bytes()); //let pk_hash = hex::encode(hasher.finalize()); - let mut shasher = Sha512_256::new(); + let mut shasher = Sha256::new(); s.serialize(&mut shasher).unwrap(); let stump_hash = hex::encode(shasher.finalize()); diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 916a278..7bca401 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,7 +1,7 @@ use bitcoin_hashes::HashEngine; use bitcoin_hashes::Hash as BitcoinHash; -use sha2::{Digest, Sha256, Sha512_256}; +use sha2::{Digest, Sha256 }; use bitcoin::consensus::Encodable; use bitcoin::key::{ @@ -44,7 +44,7 @@ pub fn get_leaf_hashes( let txid = compute_txid(&transaction); println!("txid: {txid}, block_hash: {block_hash} vout: {vout} height: {height}"); - let leaf_hash = Sha512_256::new() + let leaf_hash = Sha256::new() .chain_update(UTREEXO_TAG_V1) .chain_update(UTREEXO_TAG_V1) .chain_update(block_hash) diff --git a/shared/src/node_hash.rs b/shared/src/node_hash.rs deleted file mode 100644 index f8af758..0000000 --- a/shared/src/node_hash.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! [AccumulatorHash] is an internal type for representing Hashes in an utreexo accumulator. It's -//! just a wrapper around [[u8; 32]] but with some useful methods. -//! # Examples -//! Building from a str -//! ``` -//! use std::str::FromStr; -//! -//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; -//! let hash = BitcoinNodeHash::from_str( -//! "0000000000000000000000000000000000000000000000000000000000000000", -//! ) -//! .unwrap(); -//! assert_eq!( -//! hash.to_string().as_str(), -//! "0000000000000000000000000000000000000000000000000000000000000000" -//! ); -//! ``` -//! Building from a slice -//! ``` -//! use std::str::FromStr; -//! -//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; -//! let hash1 = BitcoinNodeHash::new([0; 32]); -//! // ... or ... -//! let hash2 = BitcoinNodeHash::from([0; 32]); -//! assert_eq!(hash1, hash2); -//! assert_eq!( -//! hash1.to_string().as_str(), -//! "0000000000000000000000000000000000000000000000000000000000000000" -//! ); -//! ``` -//! -//! Computing a parent hash (i.e a hash of two nodes concatenated) -//! ``` -//! use std::str::FromStr; -//! -//! use rustreexo::accumulator::node_hash::AccumulatorHash; -//! use rustreexo::accumulator::node_hash::BitcoinNodeHash; -//! let left = BitcoinNodeHash::new([0; 32]); -//! let right = BitcoinNodeHash::new([1; 32]); -//! let parent = BitcoinNodeHash::parent_hash(&left, &right); -//! let expected_parent = BitcoinNodeHash::from_str( -//! "34e33ca0c40b7bd33d28932ca9e35170def7309a3bf91ecda5e1ceb067548a12", -//! ) -//! .unwrap(); -//! assert_eq!(parent, expected_parent); -//! ``` -use std::convert::TryFrom; -use std::fmt::Debug; -use std::fmt::Display; -use std::ops::Deref; -use std::str::FromStr; - -use rustreexo::accumulator::node_hash::{AccumulatorHash, NodeHash}; - -use bitcoin_hashes::hex; -use bitcoin_hashes::sha256; -use bitcoin_hashes::sha512_256; -use bitcoin_hashes::Hash; -use bitcoin_hashes::HashEngine; -use serde::Deserialize; -use serde::Serialize; - - -#[derive(Eq, PartialEq, Copy, Clone, Hash, PartialOrd, Ord, Serialize, Deserialize)] -/// AccumulatorHash is a wrapper around a 32 byte array that represents a hash of a node in the tree. -/// # Example -/// ``` -/// use rustreexo::accumulator::node_hash::BitcoinNodeHash; -/// let hash = BitcoinNodeHash::new([0; 32]); -/// assert_eq!( -/// hash.to_string().as_str(), -/// "0000000000000000000000000000000000000000000000000000000000000000" -/// ); -/// ``` -#[derive(Default)] -pub enum BitcoinNodeHash { - #[default] - Empty, - Placeholder, - Some([u8; 32]), -} - -impl Deref for BitcoinNodeHash { - type Target = [u8; 32]; - - fn deref(&self) -> &Self::Target { - match self { - BitcoinNodeHash::Some(ref inner) => inner, - _ => &[0; 32], - } - } -} - -impl Display for BitcoinNodeHash { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - if let BitcoinNodeHash::Some(ref inner) = self { - let mut s = String::new(); - for byte in inner.iter() { - s.push_str(&format!("{:02x}", byte)); - } - write!(f, "{}", s) - } else { - write!(f, "empty") - } - } -} - -impl Debug for BitcoinNodeHash { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match self { - BitcoinNodeHash::Empty => write!(f, "empty"), - BitcoinNodeHash::Placeholder => write!(f, "placeholder"), - BitcoinNodeHash::Some(ref inner) => { - let mut s = String::new(); - for byte in inner.iter() { - s.push_str(&format!("{:02x}", byte)); - } - write!(f, "{}", s) - } - } - } -} - -impl From for BitcoinNodeHash { - fn from(hash: sha512_256::Hash) -> Self { - BitcoinNodeHash::Some(hash.to_byte_array()) - } -} - -impl From<[u8; 32]> for BitcoinNodeHash { - fn from(hash: [u8; 32]) -> Self { - BitcoinNodeHash::Some(hash) - } -} - -impl From<&[u8; 32]> for BitcoinNodeHash { - fn from(hash: &[u8; 32]) -> Self { - BitcoinNodeHash::Some(*hash) - } -} - -#[cfg(test)] -impl TryFrom<&str> for BitcoinNodeHash { - type Error = hex::HexToArrayError; - fn try_from(hash: &str) -> Result { - // This implementation is useful for testing, as it allows to create empty hashes - // from the string of 64 zeros. Without this, it would be impossible to express this - // hash in the test vectors. - if hash == "0000000000000000000000000000000000000000000000000000000000000000" { - return Ok(BitcoinNodeHash::Empty); - } - - let hash = hex::FromHex::from_hex(hash)?; - Ok(BitcoinNodeHash::Some(hash)) - } -} - -#[cfg(not(test))] -impl TryFrom<&str> for BitcoinNodeHash { - type Error = hex::HexToArrayError; - fn try_from(hash: &str) -> Result { - let inner = hex::FromHex::from_hex(hash)?; - Ok(BitcoinNodeHash::Some(inner)) - } -} - -impl From<&[u8]> for BitcoinNodeHash { - fn from(hash: &[u8]) -> Self { - let mut inner = [0; 32]; - inner.copy_from_slice(hash); - BitcoinNodeHash::Some(inner) - } -} - -impl From for BitcoinNodeHash { - fn from(hash: sha256::Hash) -> Self { - BitcoinNodeHash::Some(hash.to_byte_array()) - } -} - -impl FromStr for BitcoinNodeHash { - fn from_str(s: &str) -> Result { - BitcoinNodeHash::try_from(s) - } - - type Err = hex::HexToArrayError; -} - -impl BitcoinNodeHash { - /// Creates a new AccumulatorHash from a 32 byte array. - /// # Example - /// ``` - /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; - /// let hash = BitcoinNodeHash::new([0; 32]); - /// assert_eq!( - /// hash.to_string().as_str(), - /// "0000000000000000000000000000000000000000000000000000000000000000" - /// ); - /// ``` - pub fn new(inner: [u8; 32]) -> Self { - BitcoinNodeHash::Some(inner) - } -} - -impl AccumulatorHash for BitcoinNodeHash { - /// Tells whether this hash is empty. We use empty hashes throughout the code to represent - /// leaves we want to delete. - fn is_empty(&self) -> bool { - matches!(self, BitcoinNodeHash::Empty) - } - - /// Creates an empty hash. This is used to represent leaves we want to delete. - /// # Example - /// ``` - /// use rustreexo::accumulator::node_hash::AccumulatorHash; - /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; - /// let hash = BitcoinNodeHash::empty(); - /// assert!(hash.is_empty()); - /// ``` - fn empty() -> Self { - BitcoinNodeHash::Empty - } - - /// parent_hash return the merkle parent of the two passed in nodes. - /// # Example - /// ``` - /// use std::str::FromStr; - /// - /// use rustreexo::accumulator::node_hash::AccumulatorHash; - /// use rustreexo::accumulator::node_hash::BitcoinNodeHash; - /// let left = BitcoinNodeHash::new([0; 32]); - /// let right = BitcoinNodeHash::new([1; 32]); - /// let parent = BitcoinNodeHash::parent_hash(&left, &right); - /// let expected_parent = BitcoinNodeHash::from_str( - /// "34e33ca0c40b7bd33d28932ca9e35170def7309a3bf91ecda5e1ceb067548a12", - /// ) - /// .unwrap(); - /// assert_eq!(parent, expected_parent); - /// ``` - fn parent_hash(left: &Self, right: &Self) -> Self { - let mut hash = sha512_256::Hash::engine(); - hash.input(&**left); - hash.input(&**right); - sha512_256::Hash::from_engine(hash).into() - } - - fn is_placeholder(&self) -> bool { - matches!(self, BitcoinNodeHash::Placeholder) - } - - /// Returns a arbitrary placeholder hash that is unlikely to collide with any other hash. - /// We use this while computing roots to destroy. Don't confuse this with an empty hash. - fn placeholder() -> Self { - BitcoinNodeHash::Placeholder - } - - /// write to buffer - fn write(&self, writer: &mut W) -> std::io::Result<()> - where - W: std::io::Write, - { - match self { - Self::Empty => writer.write_all(&[0]), - Self::Placeholder => writer.write_all(&[1]), - Self::Some(hash) => { - writer.write_all(&[2])?; - writer.write_all(hash) - } - } - } - - /// Read from buffer - fn read(reader: &mut R) -> std::io::Result - where - R: std::io::Read, - { - let mut tag = [0]; - reader.read_exact(&mut tag)?; - match tag { - [0] => Ok(Self::Empty), - [1] => Ok(Self::Placeholder), - [2] => { - let mut hash = [0; 32]; - reader.read_exact(&mut hash)?; - Ok(Self::Some(hash)) - } - [_] => { - let err = std::io::Error::new( - std::io::ErrorKind::InvalidData, - "unexpected tag for AccumulatorHash", - ); - Err(err) - } - } - } -} - -#[cfg(test)] -mod test { - use std::str::FromStr; - - use super::AccumulatorHash; - use crate::accumulator::node_hash::BitcoinNodeHash; - use crate::accumulator::util::hash_from_u8; - - #[test] - fn test_parent_hash() { - let hash1 = hash_from_u8(0); - let hash2 = hash_from_u8(1); - - let parent_hash = BitcoinNodeHash::parent_hash(&hash1, &hash2); - assert_eq!( - parent_hash.to_string().as_str(), - "02242b37d8e851f1e86f46790298c7097df06893d6226b7c1453c213e91717de" - ); - } - #[test] - fn test_hash_from_str() { - let hash = BitcoinNodeHash::from_str( - "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", - ) - .unwrap(); - assert_eq!(hash, hash_from_u8(0)); - } - #[test] - fn test_empty_hash() { - // Only relevant for tests - let hash = BitcoinNodeHash::from_str( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(); - assert_eq!(hash, AccumulatorHash::empty()); - } -} From 505744f4fde0f1c8a33ed3f4dae98e559a698a06 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 15:24:00 -0500 Subject: [PATCH 39/44] shared: split tap_tweak into reusable tweak method --- methods/guest/src/main.rs | 2 +- shared/src/lib.rs | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index 918aacd..e540475 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -25,6 +25,7 @@ fn main() { let s: Stump = env::read(); let proof: Proof = env::read(); + //let lh : [u8; 32] = env::read(); let tx: Transaction = env::read(); let vout: u32 = env::read(); let block_height: u32 = env::read(); @@ -35,7 +36,6 @@ fn main() { let blind_secret_bytes: [u8; 32] = env::read(); eprintln!("blind_secret_bytes: {}", hex::encode(blind_secret_bytes)); let blind_secret = SecretKey::from_bytes(&blind_secret_bytes.into()).unwrap(); - let blind = blind_secret.public_key(); let tap_point = p_out.to_projective() + blind.to_projective(); let tap_pub: PublicKey = tap_point.try_into().unwrap(); eprintln!("tap blind key : {}", hex::encode(&tap_pub.to_sec1_bytes())); diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 7bca401..af398ee 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -13,6 +13,7 @@ use bitcoin::{ XOnlyPublicKey, }; use k256::PublicKey; +use k256::ProjectivePoint; use musig2::k256::elliptic_curve::point::AffineCoordinates; use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; @@ -138,30 +139,37 @@ fn secp_tap_tweak( (output_key, parity) } -fn tap_tweak(internal_key: PublicKey, merkle_root: Option) -> [u8; 32] { +fn tap_tweak(internal_key: PublicKey, merkue_root: Option) -> [u8; 32] { let x_only_bytes : [u8; 32]= internal_key.to_sec1_bytes()[1..].try_into().unwrap(); let mut eng = TapTweakHash::engine(); eng.input(&x_only_bytes); let tweak_hash = TapTweakHash::from_engine(eng); - let tweak_bytes = &tweak_hash.to_byte_array(); + let tweak_bytes = tweak_hash.to_byte_array(); + + let tweaked_point = tweak_pubkey(internal_key, &tweak_bytes); + let compressed = tweaked_point.to_encoded_point(true); + let x_coordinate = compressed.x().unwrap(); + + let pubx: [u8; 32] = x_coordinate.as_slice().try_into().unwrap(); + + pubx +} + +pub fn tweak_pubkey(pubkey: PublicKey, tweak_bytes: &[u8; 32]) -> ProjectivePoint { let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) .unwrap() .public_key() .to_projective(); - let pub_point = internal_key.to_projective(); - let pub_affine = internal_key.as_affine(); - let tweaked_point = if pub_affine.y_is_odd().unwrap_u8() == 1 { + let pub_point = pubkey.to_projective(); + let pub_affine = pubkey.as_affine(); + let tweaked = if pub_affine.y_is_odd().unwrap_u8() == 1 { tweak_point - pub_point } else { pub_point + tweak_point }; - let compressed = tweaked_point.to_encoded_point(true); - let x_coordinate = compressed.x().unwrap(); - let pubx: [u8; 32] = x_coordinate.as_slice().try_into().unwrap(); - - pubx + tweaked } From c08817844f0f8192fab01de98b47da66c03cb232 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 15:25:39 -0500 Subject: [PATCH 40/44] f change to sha256 --- host/Cargo.toml | 2 +- methods/guest/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host/Cargo.toml b/host/Cargo.toml index a7b5c63..f493139 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -9,7 +9,7 @@ shared = { path = "../shared" } risc0-zkvm = { version = "1.2.3" , features = ["unstable"]} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" -rustreexo = { version = "0.4.0", features = ["with-serde"] } +rustreexo = { git = "https://github.com/halseth/rustreexo", rev = "289b27b", features = ["with-serde"] } bitcoin = { version = "0.32.5", features = ["std", "rand-std", "serde"] } bincode = "1.3.3" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index b61857e..75ccc49 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std', 'unstable'] } -rustreexo = { version = "0.4.0", features = ["with-serde"] } +rustreexo = { git = "https://github.com/halseth/rustreexo", rev = "289b27b", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } From 7f7830fc13b74546385ba209b033e923790d6194 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 15:26:11 -0500 Subject: [PATCH 41/44] change blinding to P = Q + h(r|Q) --- host/src/main.rs | 16 +++++++++++----- methods/guest/src/main.rs | 14 +++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 68b7ada..fb51950 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -24,8 +24,8 @@ use serde::{Deserialize, Serialize}; use k256::PublicKey; use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce}; -use sha2::Digest; -use shared::{aggregate_keys, get_leaf_hashes, sort_keypairs, sort_pubkeys, verify_musig}; +use sha2::{Digest, Sha256}; +use shared::{aggregate_keys, get_leaf_hashes, sort_keypairs, sort_pubkeys, tweak_pubkey, verify_musig}; fn gen_keypair(secp: &Secp256k1) -> Keypair { let sk = SecretKey::new(&mut rand::thread_rng()); @@ -233,7 +233,6 @@ fn main() { let blind_str = args.blind_secret.unwrap(); println!("blind secret: {}", blind_str); let blind_bytes: [u8; 32] = hex::decode(blind_str).unwrap().try_into().unwrap(); - let blind = k256::SecretKey::from_bytes(&blind_bytes.into()).unwrap(); sort_keypairs(&mut keypairs); @@ -255,8 +254,15 @@ fn main() { println!("tap key : {}", hex::encode(&tap_bytes)); address(&secp, tap_key, network); - let blind_pub = blind.public_key(); - let tap_blind_point = pub_bitcoin1.to_projective() + blind_pub.to_projective(); + // Blinding beta = h(r || P) + let beta: [u8; 32] = Sha256::new() + .chain_update(blind_bytes) + .chain_update(pub_bitcoin1.to_sec1_bytes()) + .finalize() + .try_into() + .unwrap(); + + let tap_blind_point = tweak_pubkey(pub_bitcoin1, &beta); let tap_blind_key: PublicKey = tap_blind_point.try_into().unwrap(); println!( "tap blind key : {}", diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index e540475..b5e3a97 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -9,7 +9,7 @@ use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; use k256::PublicKey; use k256::SecretKey; -use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys, new_p2tr}; +use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys, new_p2tr, tweak_pubkey}; fn main() { @@ -35,8 +35,16 @@ fn main() { let p_out: PublicKey = env::read(); let blind_secret_bytes: [u8; 32] = env::read(); eprintln!("blind_secret_bytes: {}", hex::encode(blind_secret_bytes)); - let blind_secret = SecretKey::from_bytes(&blind_secret_bytes.into()).unwrap(); - let tap_point = p_out.to_projective() + blind.to_projective(); + + // Blinding beta = h(r || P) + let beta: [u8; 32] = Sha256::new() + .chain_update(blind_secret_bytes) + .chain_update(p_out.to_sec1_bytes()) + .finalize() + .try_into() + .unwrap(); + + let tap_point = tweak_pubkey(p_out, &beta); let tap_pub: PublicKey = tap_point.try_into().unwrap(); eprintln!("tap blind key : {}", hex::encode(&tap_pub.to_sec1_bytes())); // let musig_sig_bytes: Vec = env::read(); From ce9997294d848c1b7166dd39902dbedb37cc6452 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 16:15:37 -0500 Subject: [PATCH 42/44] cleanup, remove musig references --- host/src/main.rs | 318 +++----------------------------------- methods/guest/src/main.rs | 37 +---- shared/src/lib.rs | 26 +--- 3 files changed, 29 insertions(+), 352 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index fb51950..062e6fd 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -14,8 +14,7 @@ use std::str::FromStr; use std::time::SystemTime; use bitcoin::consensus::deserialize; -use bitcoin::key::Keypair; -use bitcoin::secp256k1::{rand, Secp256k1, SecretKey, Signing, Verification}; +use bitcoin::secp256k1::{Secp256k1, Verification}; use bitcoin::{Address, BlockHash, Network, ScriptBuf, TapTweakHash, Transaction, XOnlyPublicKey}; use clap::builder::TypedValueParser; use k256::schnorr::signature::Verifier; @@ -23,14 +22,8 @@ use rustreexo::accumulator::proof::Proof; use serde::{Deserialize, Serialize}; use k256::PublicKey; -use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce}; use sha2::{Digest, Sha256}; -use shared::{aggregate_keys, get_leaf_hashes, sort_keypairs, sort_pubkeys, tweak_pubkey, verify_musig}; - -fn gen_keypair(secp: &Secp256k1) -> Keypair { - let sk = SecretKey::new(&mut rand::thread_rng()); - Keypair::from_secret_key(secp, &sk) -} +use shared::{get_leaf_hashes, tweak_pubkey}; /// utxozkp #[derive(Debug, Parser)] @@ -68,37 +61,10 @@ struct Args { vout: Option, #[arg(long)] - msg_hex: Option, - - #[arg(long)] - musig_sig: Option, - - #[arg(long)] - node_key_1_priv: Option, + bitcoin_key: Option, #[arg(long)] - node_key_2_priv: Option, - - #[arg(long)] - bitcoin_key_1_priv: Option, - - #[arg(long)] - bitcoin_key_2_priv: Option, - - #[arg(long)] - blind_secret: Option, - - #[arg(long)] - node_key_1: Option, - - #[arg(long)] - node_key_2: Option, - - #[arg(long)] - bitcoin_key_1: Option, - - #[arg(long)] - bitcoin_key_2: Option, + blind_secret_hex: Option, /// Network to use. #[arg(long, default_value_t = Network::Testnet)] @@ -126,37 +92,13 @@ fn parse_pubkey(pub_str: &str) -> PublicKey { pk } -fn extract_keypair( - secp: &Secp256k1, - priv_str: &str, - network: Network, -) -> Keypair { - let keypair = if priv_str == "new" { - gen_keypair(&secp) - } else { - let sk = SecretKey::from_str(&priv_str).unwrap(); - Keypair::from_secret_key(&secp, &sk) - }; - - let (internal_key, _parity) = keypair.x_only_public_key(); - let script_buf = ScriptBuf::new_p2tr(&secp, internal_key, None); - let addr = Address::from_script(script_buf.as_script(), network).unwrap(); - let pubkey = keypair.public_key(); - println!("priv: {}", hex::encode(keypair.secret_key().secret_bytes())); - println!("pubkey: {}", hex::encode(pubkey.serialize())); - println!("xonly pub: {}", internal_key); - println!("address: {}", addr); - - keypair -} - fn address(secp: &Secp256k1, pubkey: PublicKey, network: Network) { let pub_bytes: [u8; 32] = pubkey.to_sec1_bytes()[1..].try_into().unwrap(); let pubx = XOnlyPublicKey::from_slice(&pub_bytes).unwrap(); let script_buf = ScriptBuf::new_p2tr(&secp, pubx, None); let addr = Address::from_script(script_buf.as_script(), network).unwrap(); - println!("pub: {}", pubx); + println!("xonly pub: {}", pubx); println!("address: {}", addr); } @@ -188,117 +130,26 @@ fn main() { let secp = Secp256k1::new(); let network = args.network; - let mut keypairs = vec![]; - - println!("node_key_1:"); - let pub_node1 = match args.node_key_1_priv { - Some(priv_str) => { - let kp = extract_keypair(&secp, &priv_str, network); - keypairs.push(kp); - PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() - } - None => parse_pubkey(&args.node_key_1.unwrap()), - }; - - println!("node_key_2:"); - let pub_node2 = match args.node_key_2_priv { - Some(priv_str) => { - let kp = extract_keypair(&secp, &priv_str, network); - keypairs.push(kp); - PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() - } - None => parse_pubkey(&args.node_key_2.unwrap()), - }; - - println!("bitcoin_key_1:"); - let pub_bitcoin1 = match args.bitcoin_key_1_priv { - Some(priv_str) => { - let kp = extract_keypair(&secp, &priv_str, network); - keypairs.push(kp); - PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() - } - None => parse_pubkey(&args.bitcoin_key_1.unwrap()), - }; - - println!("bitcoin_key_2:"); - let pub_bitcoin2 = match args.bitcoin_key_2_priv { - Some(priv_str) => { - let kp = extract_keypair(&secp, &priv_str, network); - keypairs.push(kp); - PublicKey::from_sec1_bytes(&kp.public_key().serialize()).unwrap() - } - None => parse_pubkey(&args.bitcoin_key_2.unwrap()), - }; - - let blind_str = args.blind_secret.unwrap(); - println!("blind secret: {}", blind_str); + let pub_bitcoin = parse_pubkey(&args.bitcoin_key.unwrap()); + let blind_str = args.blind_secret_hex.unwrap(); let blind_bytes: [u8; 32] = hex::decode(blind_str).unwrap().try_into().unwrap(); - sort_keypairs(&mut keypairs); - - let msg_to_sign = hex::decode(args.msg_hex.unwrap()).unwrap(); - - let all_pubs = vec![pub_node1, pub_node2, pub_bitcoin1, pub_bitcoin2]; - let mut musig_pubs = all_pubs.clone(); - sort_pubkeys(&mut musig_pubs); - - let mut bitcoin_pubs = vec![pub_bitcoin1, pub_bitcoin2]; - sort_pubkeys(&mut bitcoin_pubs); - - for i in 0..musig_pubs.len() { - println!("key[{}]={}", i, hex::encode(musig_pubs[i].to_sec1_bytes())); - } - - let tap_key = aggregate_keys(bitcoin_pubs); - let tap_bytes = tap_key.to_sec1_bytes(); - println!("tap key : {}", hex::encode(&tap_bytes)); - address(&secp, tap_key, network); - // Blinding beta = h(r || P) let beta: [u8; 32] = Sha256::new() .chain_update(blind_bytes) - .chain_update(pub_bitcoin1.to_sec1_bytes()) + .chain_update(pub_bitcoin.to_sec1_bytes()) .finalize() .try_into() .unwrap(); - let tap_blind_point = tweak_pubkey(pub_bitcoin1, &beta); + let tap_blind_point = tweak_pubkey(pub_bitcoin, &beta); let tap_blind_key: PublicKey = tap_blind_point.try_into().unwrap(); println!( - "tap blind key : {}", + "blinded tap key : {}", hex::encode(&tap_blind_key.to_sec1_bytes()) ); address(&secp, tap_blind_key, network); - let musig_sig = match args.musig_sig { - Some(musig_sig) => hex::decode(musig_sig).unwrap(), - - // In case no signature is provided, we assume we are signing the message and private keys - // are available, - None => { - println!("signing"); - let (_, sig) = create_musig(keypairs, &msg_to_sign); - sig.to_vec() - } - }; - - let agg_key = aggregate_keys(musig_pubs.clone()); - let agg_bytes = agg_key.to_sec1_bytes(); - println!("aggregate key : {}", hex::encode(&agg_bytes)); - - println!("musig sig: {}", hex::encode(&musig_sig)); - - assert_eq!( - verify_musig( - musig_pubs.clone(), - musig_sig.clone().try_into().unwrap(), - &msg_to_sign - ), - true, - ); - - println!("musig successfully verified"); - let acc: CliStump = serde_json::from_str(&args.utreexo_acc.unwrap()).unwrap(); let acc = Stump { leaves: acc.leaves, @@ -360,8 +211,6 @@ fn main() { let block_hash: BlockHash = BlockHash::from_str(&args.block_hash.unwrap()).unwrap(); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); - println!("lh: {:?}", lh); - let lh = NodeHash::from(lh); assert_eq!(lh, leaf_hash); @@ -371,26 +220,23 @@ fn main() { let internal_key = XOnlyPublicKey::from_slice(&tap_bytes[1..]).unwrap(); println!("xonly tap key: {}", hex::encode(internal_key.serialize())); + // Assume not tap tweak. + // TODO: add support for this. let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, None); println!("secp tweak hash: {}", tweak_hash); + // Sanity check the two p2tr implementation. let script_pubkey = shared::secp_new_p2tr(&secp, internal_key, None); let script_pub2 = shared::new_p2tr(tap_blind_key, None); - - println!("script_pubKey: {}", script_pubkey); - println!("script_pubKey2: {}", script_pub2); - + assert_eq!(script_pub2, script_pubkey); assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); println!("proving {}", leaf_hash); - println!("proof: {:?}", proof); assert_eq!(acc.verify(&proof, &[leaf_hash]), Ok(true)); println!("stump proof verified"); let start_time = SystemTime::now(); let env = ExecutorEnv::builder() - //.write(&msg_to_sign) - //.unwrap() .write(&acc) .unwrap() .write(&proof) @@ -404,15 +250,11 @@ fn main() { .write(&block_hash) .unwrap() // Pubkey - .write(&pub_bitcoin1) + .write(&pub_bitcoin) .unwrap() - // Blinding key + // Blinding secret .write(&blind_bytes) .unwrap() - // .write(&all_pubs) - // .unwrap() - // .write(&musig_sig.as_slice()) - // .unwrap() .build() .unwrap(); @@ -440,27 +282,13 @@ fn main() { } fn verify_receipt(receipt: &Receipt) { - //let (node_key1, node_key2, stump_hash, pk_hash, msg): ( - // PublicKey, - // PublicKey, - // String, - // String, - // Vec, - //) = receipt.journal.decode().unwrap(); - - //// The receipt was verified at the end of proving, but the below code is an - //// example of how someone else could verify this receipt. - //println!( - // "committed node_key1 : {}", - // hex::encode(&node_key1.to_sec1_bytes()) - //); - //println!( - // "committed node_key2 : {}", - // hex::encode(&node_key2.to_sec1_bytes()) - //); - //println!("bitcoin keys hash: {}", pk_hash); - //println!("signed msg: {}", hex::encode(msg)); - //println!("stump hash: {}", stump_hash); + let (pubkey, stump_hash): (PublicKey, String) = receipt.journal.decode().unwrap(); + + println!( + "unblinded pubkey: {}", + hex::encode(&pubkey.to_sec1_bytes()) + ); + println!("stump hash: {}", stump_hash); receipt.verify(METHOD_ID).unwrap(); println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); @@ -477,103 +305,3 @@ fn to_bytes(h: [u32; 8]) -> [u8; 32] { buf } -fn create_musig(keys: Vec, message: &Vec) -> (Vec, [u8; 64]) { - let mut pubs: Vec = Vec::new(); - - for kp in keys.clone() { - let bytes = kp.secret_key().secret_bytes(); - let str = hex::encode(bytes); - let scalar: musig2::secp::Scalar = str.parse().unwrap(); - let p = scalar.base_point_mul(); - let pubkey = PublicKey::from(p); - pubs.push(pubkey.into()); - } - - let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); - - for kp in keys.clone() { - let bytes = kp.secret_key().secret_bytes(); - let str = hex::encode(bytes); - println!("priv key: {}", str); - let scalar: musig2::secp::Scalar = str.parse().unwrap(); - let p = scalar.base_point_mul(); - println!("point: {}", hex::encode(p.serialize())); - key_agg_ctx.key_coefficient(p).unwrap(); - } - - // This is the key which the group has control over. - let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); - - println!("all good {:?}", aggregated_pubkey); - - let nonce_seed = [0xACu8; 32]; - - // This is how `FirstRound` derives the nonce internally. - let mut public_nonces = Vec::new(); - let mut sec_nonces = Vec::new(); - for (i, k) in pubs.iter().enumerate() { - let secnonce = SecNonce::build(nonce_seed) - // .with_seckey(scalar) - .with_pubkey(pubs[i].clone()) - .with_message(&message) - .with_aggregated_pubkey(aggregated_pubkey) - .with_extra_input(&(i as u32).to_be_bytes()) - .build(); - - sec_nonces.push(secnonce.clone()); - let our_public_nonce = secnonce.public_nonce(); - - public_nonces.push(our_public_nonce); - } - - // We manually aggregate the nonces together and then construct our partial signature. - let aggregated_nonce: AggNonce = public_nonces.iter().sum(); - let mut partial_signatures = Vec::new(); - for (i, k) in keys.clone().iter().enumerate() { - // let sk = bitcoin::secp256k1::SecretKey::from_str(&k).unwrap(); - let b = k.secret_bytes(); - let priv_str = hex::encode(b); - let scalar: musig2::secp::Scalar = priv_str.parse().unwrap(); - - let our_partial_signature: PartialSignature = musig2::sign_partial( - &key_agg_ctx, - scalar, - sec_nonces[i].clone(), - &aggregated_nonce, - &message, - ) - .expect("error creating partial signature"); - - partial_signatures.push(our_partial_signature); - } - - /// Signatures should be verified upon receipt and invalid signatures - /// should be blamed on the signer who sent them. - for (i, partial_signature) in partial_signatures.clone().into_iter().enumerate() { - let their_pubkey: PublicKey = key_agg_ctx.get_pubkey(i).unwrap(); - let their_pubnonce = &public_nonces[i]; - - musig2::verify_partial( - &key_agg_ctx, - partial_signature, - &aggregated_nonce, - their_pubkey, - their_pubnonce, - &message, - ) - .expect("received invalid signature from a peer"); - } - - let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( - &key_agg_ctx, - &aggregated_nonce, - partial_signatures, - &message, - ) - .expect("error aggregating signatures"); - - musig2::verify_single(aggregated_pubkey, &final_signature, &message) - .expect("aggregated signature must be valid"); - - (pubs, final_signature) -} diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index b5e3a97..f4e5d26 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -9,7 +9,7 @@ use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; use k256::PublicKey; use k256::SecretKey; -use shared::{get_leaf_hashes, verify_musig, aggregate_keys, sort_pubkeys, new_p2tr, tweak_pubkey}; +use shared::{get_leaf_hashes, new_p2tr, tweak_pubkey}; fn main() { @@ -21,11 +21,9 @@ fn main() { // read the input -// let msg_bytes: Vec = env::read(); let s: Stump = env::read(); let proof: Proof = env::read(); - //let lh : [u8; 32] = env::read(); let tx: Transaction = env::read(); let vout: u32 = env::read(); let block_height: u32 = env::read(); @@ -47,28 +45,6 @@ fn main() { let tap_point = tweak_pubkey(p_out, &beta); let tap_pub: PublicKey = tap_point.try_into().unwrap(); eprintln!("tap blind key : {}", hex::encode(&tap_pub.to_sec1_bytes())); - // let musig_sig_bytes: Vec = env::read(); - -// let mut musig_pubs = all_pubs.clone(); -// sort_pubkeys(&mut musig_pubs); -// -// assert_eq!( -// verify_musig(musig_pubs.clone(), musig_sig_bytes.clone().try_into().unwrap(), &msg_bytes), -// true, -// ); -// -// let node_key1 = all_pubs[0]; -// let node_key2 = all_pubs[1]; -// -// // Aggregate the bitcoin keys. -// let bitcoin_key1 = all_pubs[2]; -// let bitcoin_key2 = all_pubs[3]; -// let mut bitcoin_keys =vec![bitcoin_key1, bitcoin_key2]; -// sort_pubkeys(&mut bitcoin_keys); -// let tap_pub = aggregate_keys(bitcoin_keys); - - let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); - let leaf_hash = NodeHash::from(lh); // We'll check that the given public key corresponds to an output in the utxo set. let script_pubkey = new_p2tr(tap_pub, None); @@ -76,22 +52,17 @@ fn main() { // assert internal key is in tx used to calc leaf hash assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); + let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); + let leaf_hash = NodeHash::from(lh); + // Assert it is in the set. assert_eq!(s.verify(&proof, &[leaf_hash]), Ok(true)); - //let mut hasher = Sha512_256::new(); - //hasher.update(&bitcoin_key1.to_sec1_bytes()); - //hasher.update(&bitcoin_key2.to_sec1_bytes()); - //let pk_hash = hex::encode(hasher.finalize()); - let mut shasher = Sha256::new(); s.serialize(&mut shasher).unwrap(); let stump_hash = hex::encode(shasher.finalize()); // write public output to the journal env::commit(&p_out); - //env::commit(&node_key2); env::commit(&stump_hash); - //env::commit(&pk_hash); - //env::commit(&msg_bytes); } \ No newline at end of file diff --git a/shared/src/lib.rs b/shared/src/lib.rs index af398ee..463754d 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -5,7 +5,7 @@ use sha2::{Digest, Sha256 }; use bitcoin::consensus::Encodable; use bitcoin::key::{ - Keypair, Parity, Secp256k1, TweakedPublicKey, UntweakedPublicKey, Verification, + Parity, Secp256k1, UntweakedPublicKey, Verification, }; use bitcoin::script::{Builder, PushBytes}; use bitcoin::{ @@ -17,7 +17,7 @@ use k256::ProjectivePoint; use musig2::k256::elliptic_curve::point::AffineCoordinates; use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; -use musig2::{k256, KeyAggContext}; +use musig2::k256; pub const UTREEXO_TAG_V1: [u8; 64] = [ 0x5b, 0x83, 0x2d, 0xb8, 0xca, 0x26, 0xc2, 0x5b, 0xe1, 0xc5, 0x42, 0xd6, 0xcc, 0xed, 0xdd, 0xa8, @@ -71,29 +71,7 @@ pub fn compute_txid(tx: &Transaction) -> Txid { Txid::from_slice(&hash_result).expect("hash should be valid Txid") } -pub fn aggregate_keys(pubs: Vec) -> PublicKey { - let key_agg_ctx = KeyAggContext::new(pubs.clone()).unwrap(); - let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); - aggregated_pubkey -} - -pub fn verify_musig(pubs: Vec, sig: [u8; 64], message: &Vec) -> bool { - let aggregated_pubkey: PublicKey = aggregate_keys(pubs); - - musig2::verify_single(aggregated_pubkey, &sig, message) - .expect("aggregated signature must be valid"); - - true -} - -pub fn sort_pubkeys(pubkeys: &mut Vec) { - pubkeys.sort_by(|a, b| a.to_sec1_bytes().cmp(&b.to_sec1_bytes())); -} - -pub fn sort_keypairs(kp: &mut Vec) { - kp.sort_by(|a, b| a.public_key().serialize().cmp(&b.public_key().serialize())); -} pub fn new_p2tr(internal_key: PublicKey, merkle_root: Option) -> ScriptBuf { let output_key = tap_tweak(internal_key, merkle_root); // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) From ed6956528ad45ff9b3f5ad1669f07ed69cca96f0 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 17:08:06 -0500 Subject: [PATCH 43/44] main: create derive option --- host/src/main.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/host/src/main.rs b/host/src/main.rs index 062e6fd..7d7cca7 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -30,7 +30,10 @@ use shared::{get_leaf_hashes, tweak_pubkey}; #[command(verbatim_doc_comment)] struct Args { #[arg(short, long, default_value_t = false)] - prove: bool, + verify: bool, + + #[arg(short, long, default_value_t = false)] + derive: bool, #[arg(long)] proof_type: Option, @@ -61,7 +64,7 @@ struct Args { vout: Option, #[arg(long)] - bitcoin_key: Option, + pubkey: Option, #[arg(long)] blind_secret_hex: Option, @@ -110,18 +113,12 @@ fn main() { let args = Args::parse(); - let receipt_file = if args.prove { - let r = File::create(args.receipt_file.unwrap()).unwrap(); - r - } else { - let r = File::open(args.receipt_file.unwrap()).unwrap(); - r - }; - // If not proving, simply verify the passed receipt using the loaded utxo set. let start_time = SystemTime::now(); - if !args.prove { - let receipt: Receipt = bincode::deserialize_from(receipt_file).unwrap(); + if args.verify{ + let receipt_file = args.receipt_file.unwrap(); + let r = File::open(receipt_file).unwrap(); + let receipt: Receipt = bincode::deserialize_from(r).unwrap(); verify_receipt(&receipt); println!("receipt verified in {:?}", start_time.elapsed().unwrap()); return; @@ -130,7 +127,7 @@ fn main() { let secp = Secp256k1::new(); let network = args.network; - let pub_bitcoin = parse_pubkey(&args.bitcoin_key.unwrap()); + let pub_bitcoin = parse_pubkey(&args.pubkey.unwrap()); let blind_str = args.blind_secret_hex.unwrap(); let blind_bytes: [u8; 32] = hex::decode(blind_str).unwrap().try_into().unwrap(); @@ -150,6 +147,10 @@ fn main() { ); address(&secp, tap_blind_key, network); + if args.derive { + return; + } + let acc: CliStump = serde_json::from_str(&args.utreexo_acc.unwrap()).unwrap(); let acc = Stump { leaves: acc.leaves, @@ -278,7 +279,9 @@ fn main() { let receipt_bytes = bincode::serialize(&receipt).unwrap(); println!("receipt ({}). seal size: {seal_size}.", receipt_bytes.len()); - bincode::serialize_into(receipt_file, &receipt).unwrap(); + let receipt_file = args.receipt_file.unwrap(); + let r = File::create(receipt_file).unwrap(); + bincode::serialize_into(r, &receipt).unwrap(); } fn verify_receipt(receipt: &Receipt) { From 5d0010a8f8c2dfd6b408d45c771ddffb6ae36941 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 20 Feb 2025 17:08:28 -0500 Subject: [PATCH 44/44] README: update to latest API --- README.md | 113 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 361a058..46b163b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # OutputZero -`OutputZero` is a proof of concept tool for proving Bitcoin UTXO set inclusion in -zero knowledge. + +`OutputZero` is a proof of concept tool for proving Bitcoin UTXO set inclusion +in zero knowledge. ## Applications -Since unspent transaction outputs is a scare resource, having a way of +Since unspent transaction outputs is a scarce resource, having a way of cryptographically prove you own one without revealing anything about the output is useful for all sorts of anti-DOS applications. @@ -20,20 +21,41 @@ The tool works with accumulators and proofs from a the [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge), which acts as a utreexo bridge to Bitcoin Core. -After being given the utreexo accumulator and proof, the prover signs a message -using the private key for the output with public key `P`, proving that he -controls the coins. +The prover starts by creating a regular Bitcoin Taproot public key `P`. This +could be any valid taproot internal key, for instance a Musig2 aggregate key. + +The prover then chooses a random blinding secret `r` that will be used to blind +the key before using it: + +``` +beta = hash(r || P) +P_out = P + beta * G +``` + +The `beta` acts as a obfuscator to the key that goes onchain, making it +impossible to derive the link between `P` and `P_out` without knowledge of +`beta`. + +Now the prover can send money to `P_out`, manifesting it as an output on-chain. + +Proving control of the UTXO now goes as follows: +- The prover can create a signature for an arbitrary message using public key + `P`, proving ownership. The prover then creates a ZK-STARK proof using the [Risc0 ZKVM](https://github.com/risc0/risc0) that proves the following: -- The prover has a valid signature for an arbitrary message for a public key - `P`, where `P = x * G`. The message and `hash(x)`is shown to the verifier. -- The prover has a proof showing that the public key P is found in the Utreexo - set. The Utreexo root is shown to the verifier. +- The prover has a secret `r` such that +``` +beta = hash(r || P) +P_out = P + beta * G +``` +- The prover has a proof showing that the public key `P_out` is found in the + Utreexo set. The Utreexo root hash is shown to the verifier. -This ZK-proof is convincing the verifier that the prover has the private key to -the output in the UTXO set. +This ZK-proof is convincing the verifier that the prover is able to sign for +the output in the UTXO set (if he can sign for `P` and knows `beta` then he can +also sign for `P_out`). ## Quick start @@ -48,7 +70,9 @@ $ bitcoind --signet --txindex Now we set ut a utreexo bridge that will index the chain and create the inclusion proofs we need: - Install the bridge according to - [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge). + [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge) + making sure using this revision: + https://github.com/halseth/rpc-utreexo-bridge/commit/4a49a589018c22da67061b5e233fe8ff45670f4a. - Set environment variables to match the bitcoind instance: ``` @@ -61,32 +85,52 @@ Start the bridge and let it index while you continue to the next step: $ bridge --network signet ``` -Now we can create an address using OutputZero, and send som signet coins to it: +Now create a public key and a random secret and pass it to OutputZero: ```bash -$ cargo run --release -- --priv-key "new" -priv: 6fc5d9e0dcd0cad79cea037a28850abe4a661d7a2c2de72311feea912acc5dbf -pub: bd70caa34056cc4bb2b66f44e038c52f1f87f4fb20703f6209617bb58a032a5d -address: tb1pnpvxrjhlwzn7rfggv2tvx508tuvha38ez3x993r865cxcn3xrexqn9t6jl +$ cargo run -- --pubkey "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --blind-secret-hex "a3b1c5d2e7f9a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4" --derive +sec1 pub: 027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +blinded tap key : 02b123a2aa4184bacf8d7879023c6dc053aeddd64ed4ca3bc6ce3519c95e1b4c75 +xonly pub: b123a2aa4184bacf8d7879023c6dc053aeddd64ed4ca3bc6ce3519c95e1b4c75 +address: tb1pu3l57s32yhmegykrq6a7pe7e05ckvdwka9yvr72hlhx8dwh5kesqudhsq7 ``` You can now fund the given address with some signetBTC, then wait for the transaction to confirm and Bitcoin Core to sync to the block (feel free to use -the above private key or deposit tx for testing, but please don't spend the coins). +the above keys or deposit tx for testing). After having the coins confirmed, we will get the utreexo accumulator and -proofs from the bridge (TODO: show how to get leaf hash): +proofs from the bridge: ``` -$ curl http://127.0.0.1:3000/prove/3baea3c5fbc3afb0ec11379416a68a1e2a64df318ea611f58213e87c50d8ccd1 | jq -c '.data' > proof.json +$ curl http://127.0.0.1:3000/leaf/ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e:1 | jq +{ + "data": { + "hash": "600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db", + "leaf_data": { + "block_hash": "000000531547656158a86c79718462f348c7c2f5d7aff509905b4c0cc9fd79c1", + "block_height": 236206, + "hash": "600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db", + "is_coinbase": false, + "prevout": "ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e:1", + "utxo": { + "script_pubkey": "5120e47f4f422a25f79412c306bbe0e7d97d316635d6e948c1f957fdcc76baf4b660", + "value": 10000 + } + } + }, + "error": null +} +$ curl http://127.0.0.1:3000/prove/600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db | jq -c '.data' > proof.json $ curl http://127.0.0.1:3000/acc | jq -c '.data' > acc.json -$ bitcoin-cli --signet getrawtransaction 48356de0a84cd6022ff84a70f805922ec7c799c1a01d683b8c906d38824e71e2 > tx.hex +$ bitcoin-cli --signet getrawtransaction ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e > tx.hex ``` -Now we can run OutputZero with these proofs, in addition to some metadata about the tx and block it confirmed in: +Now we can run OutputZero with these proofs, in addition to some metadata about +the tx and block it confirmed in: ```bash -$ cargo run --release -- --utreexo-acc "`cat acc.json`" --utreexo-proof "`cat proof.json`" --leaf-hash '3baea3c5fbc3afb0ec11379416a68a1e2a64df318ea611f58213e87c50d8ccd1' --prove --priv-key '6fc5d9e0dcd0cad79cea037a28850abe4a661d7a2c2de72311feea912acc5dbf' --receipt-file 'receipt.bin' --msg 'this is message' --tx-hex "`cat tx.hex`" --vout 1 --block-height 226735 --block-hash '00000019cfb5ef098766c4602dbfbb7351ad61a71c2f451d80feb2eb65563b63' +$ cargo run --release -- --utreexo-acc "`cat acc.json`" --utreexo-proof "`cat proof.json`" --leaf-hash '600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db' --receipt-file 'receipt.bin' --tx-hex "`cat tx.hex`" --vout 1 --block-height 236206 --block-hash '000000531547656158a86c79718462f348c7c2f5d7aff509905b4c0cc9fd79c1' --proof-type 'default' --pubkey "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --blind-secret-hex "a3b1c5d2e7f9a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4" ``` This command will create a ZK proof as detailed in the Architecture section. @@ -97,22 +141,22 @@ independently. The proof can be verified using ```bash -cargo run --release -- --utreexo-acc "`cat acc.json`" --receipt-file 'receipt.bin' --msg 'this is message' +cargo run --release -- --utreexo-acc "`cat acc.json`" --receipt-file 'receipt.bin' --verify ``` -Note that the the accumulator needed to verify the proof is the same one needed -to create it. But since utreexo accumulators are deterministic, it can be +Note that the accumulator needed to verify the proof is the same one needed to +create it. But since utreexo accumulators are deterministic, it can be independently created by the verifier as long as it is communicated which block height one is using when creating the proof. -## Benchmarks, Apple M1 Max -- Proving time is about 48 seconds. -- Verification time is ~254 ms. -- Proof size is 1.4 MB. +## Benchmarks, Apple M1 Max (succint proof type) +- Proving time is about 15 seconds. +- Verification time is ~55 ms. +- Proof size is 223 kB. ## Limitations -This is a rough first draft of how a tool like this could look like. It has -plenty of known limitations and should absolutely not be used with private keys +This is a rough draft of how a tool like this could look like. It has +plenty of known limitations and should absolutely not be used with keys controlling real (mainnet) coins. A non-exhaustive list (some of these could be relatively easy to fix): @@ -121,8 +165,5 @@ A non-exhaustive list (some of these could be relatively easy to fix): - Only supports testnet3 and signet. - Only proving existence, selectively revealing more about the output is not supported. -- Proving time is not optimized. -- Proof size is not attempted optimized. -- Private key must be hot. - ... and many more.