From 5a9abf6cadd58b83228141bb0f4a87a4e647d2d1 Mon Sep 17 00:00:00 2001 From: vanhger Date: Wed, 17 Jun 2026 10:23:45 +0700 Subject: [PATCH 1/9] build and broadcast operator commit pubin in handler. --- node/src/handle.rs | 65 ++++- .../graph_maintenance_tasks.rs | 263 ++++++++++++++++++ node/src/utils.rs | 189 +++++++++++-- 3 files changed, 500 insertions(+), 17 deletions(-) diff --git a/node/src/handle.rs b/node/src/handle.rs index eb2f385c..9d8ffff6 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -13,6 +13,7 @@ use crate::utils::*; use anyhow::{Context, Result, anyhow, bail}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; +use bitcoin::hashes::{Hash, sha256}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ @@ -37,7 +38,7 @@ use goat::transactions::pre_signed_musig2::verify_public_nonce; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; use std::sync::Arc; -use store::GraphStatus; +use store::{GraphStatus, SerializableTxid}; use store::localdb::LocalDB; use uuid::Uuid; @@ -3634,6 +3635,66 @@ async fn handle_operator_commit_pubin_timeout_verifier( Ok(()) } +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_commit_pubin_ready_operator( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<()> { + let message = GOATMessage::new( + Actor::Operator, + GOATMessageContent::CommitPubinReady(CommitPubinReady { instance_id, graph_id }), + ); + + let (graph, _graph_status, _graph_sub_status) = + match refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge).await? + { + Some(v) => v, + None => return Ok(()), + }; + + // compute the 96-byte guest pubin + let watchtower_challenge_init_txid = + SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); + let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); + let (challenge_txids, included_watchtowers_bits) = + get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) + .await?; + let (btc_best_block_hash, included_watchtowers_bitmap) = + compute_operator_pubin_blockhash_and_bitmap( + ctx.btc_client, + &challenge_txids, + &included_watchtowers_bits, + ) + .await?; + let guest_pubin = build_operator_guest_pubin( + &btc_best_block_hash, + &graph.parameters.pubin_disprove_constant, + &included_watchtowers_bitmap, + ); + + // sign and broadcast + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let commit_pubin_wots_sk = operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id).0; + let signed_input = operator_sign_commit_pubin(&graph, &commit_pubin_wots_sk, &guest_pubin)?; + let connector_e_amount = graph + .watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow!("failed to get connector-e input: {e}"))? + .amount; + let operator_keypair = operator_master_key.master_keypair(); + build_sign_and_broadcast_tx( + ctx.btc_client, + operator_keypair, + vec![signed_input], + connector_e_amount, + vec![], + ) + .await?; + + Ok(()) +} + // after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_ready_operator( @@ -3674,6 +3735,8 @@ async fn handle_assert_ready_operator( }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; + // TODO: replace with build_assert_witness_with_pubin_commitment passing the real + // x_d = sha256(guest_pubin) recomputed from on-chain watchtower challenge data. let assert_witness = build_assert_witness(&operator_wrapper_proof.proof, &assert_secret_key)?; let assert_message = assert_wots_message(&assert_witness)?; let mut asserted_wrapper_proof = Vec::new(); diff --git a/node/src/scheduled_tasks/graph_maintenance_tasks.rs b/node/src/scheduled_tasks/graph_maintenance_tasks.rs index dfed5b02..739701dd 100644 --- a/node/src/scheduled_tasks/graph_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/graph_maintenance_tasks.rs @@ -371,6 +371,25 @@ pub async fn process_graph_challenge( ); upsert_detected_messages(local_db, graph.graph_id, assert_sent_messages).await?; } + // Todo: vanhger + if let Some((actor, message_content)) = + detect_operator_commit_pubin(btc_client, local_db, &graph).await? + { + info!("process_graph_challenge detect operator commit pubin confirmed on-chain"); + let mut storage_processor = local_db.acquire().await?; + upsert_message( + &mut storage_processor, + false, + graph.graph_id, + None, + SELF_SENDER.to_string(), + actor, + message_content, + 0, + 0, + ) + .await?; + } if let Some((actor, message_content, sub_type)) = detect_assert_disprove_ready(btc_client, local_db, &graph, current_height).await? @@ -688,6 +707,64 @@ async fn detect_assert_sent_flow( Ok(messages) } +/// may trigger: AssertReady +async fn detect_operator_commit_pubin( + btc_client: &BTCClient, + local_db: &LocalDB, + graph: &Graph, +) -> anyhow::Result> { + let watchtower_challenge_init_txid: Txid = match graph.watchtower_challenge_init_txid.clone() { + Some(txid) => txid.into(), + None => { + warn!( + "detect_operator_commit_pubin graph_id:{} watchtower_challenge_init_txid is none", + graph.graph_id + ); + return Ok(None); + } + }; + + // vout_len is stored in graph_btc_tx_vout_monitor by detect_watchtower_challenge + let vout_len = { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .find_graph_btc_tx_vout_monitor( + &graph.graph_id, + &SerializableTxid::from(watchtower_challenge_init_txid), + ) + .await? + .map(|m| m.vout_len) + .unwrap_or(0) + }; + if vout_len < 3 { + return Ok(None); + } + + // ConnectorE is the 3rd-to-last output + // (layout: ..., connector_e, connector_f, anchor) + let connector_e_vout = (vout_len - 3) as u64; + let Some(spend_txid) = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, connector_e_vout).await? + else { + return Ok(None); + }; + + // if ConnectorE is spent by operator_commit_timeout — skip + let commit_timeout_txid: Option = + graph.operator_commit_timeout_txid.clone().map(Txid::from); + if commit_timeout_txid == Some(spend_txid) { + return Ok(None); + } + + Ok(Some(( + Actor::Operator, + GOATMessageContent::AssertReady(AssertReady { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + ))) +} + // may trigger disprove ready async fn detect_assert_disprove_ready( btc_client: &BTCClient, @@ -1191,3 +1268,189 @@ async fn check_pre_kickoff_sent( } Ok(pre_sents) } + +#[cfg(test)] +mod detect_commit_pubin_tests { + use bitcoin::hashes::Hash; + use super::*; + use client::btc_chain::BTCClient; + use esplora_client::OutputStatus; + use store::create_local_db; + + fn make_txid(byte: u8) -> Txid { + Txid::from_slice(&[byte; 32]).unwrap() + } + + fn make_dummy_graph( + graph_id: Uuid, + instance_id: Uuid, + watchtower_challenge_init_txid: Option, + operator_commit_timeout_txid: Option, + ) -> Graph { + Graph { + graph_id, + instance_id, + kickoff_index: 0, + from_addr: "".into(), + to_addr: "".into(), + amount: 0, + challenge_amount: 0, + status: "Challenge".into(), + sub_status: "".into(), + operator_pubkey: "".into(), + next_prekickoff: None, + cur_prekickoff_txid: None, + force_skip_kickoff_txid: None, + quick_challenge_txid: None, + challenge_incomplete_kickoff_txid: None, + pegin_txid: None, + kickoff_txid: None, + take1_txid: None, + challenge_txid: None, + take2_txid: None, + watchtower_challenge_init_txid, + operator_assert_txid: None, + verifier_assert_txids: vec![], + disprove_txids: vec![], + watchtower_challenge_timeout_txids: vec![], + operator_challenge_nack_txids: vec![], + operator_commit_timeout_txid, + init_withdraw_tx_hash: None, + bridge_out_start_at: 0, + status_updated_at: 0, + proceed_withdraw_height: 0, + created_at: 0, + updated_at: 0, + } + } + + async fn insert_vout_for_watchtower(db: &LocalDB, graph_id: &Uuid, txid: Txid, vout_len: i64) { + let mut s = db.acquire().await.unwrap(); + s.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { + graph_id: *graph_id, + tx_name: "watchtower_init".into(), + txid: SerializableTxid::from(txid), + height: 100, + vout_len, + monitor_data: "".into(), + created_at: 0, + updated_at: 0, + }) + .await + .unwrap(); + } + + #[tokio::test] + async fn returns_none_when_init_txid_missing() { + let db = create_local_db("sqlite::memory:").await; + let (btc_client, _) = BTCClient::new_mock_client(); + + let graph = make_dummy_graph(Uuid::new_v4(), Uuid::new_v4(), None, None); + let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn returns_none_when_vout_monitor_missing_from_db() { + let db = create_local_db("sqlite::memory:").await; + let (btc_client, _) = BTCClient::new_mock_client(); + + let init_txid = make_txid(0x01); + let graph = make_dummy_graph( + Uuid::new_v4(), + Uuid::new_v4(), + Some(SerializableTxid::from(init_txid)), + None, + ); + // vout_monitor not inserted → vout_len defaults to 0 → returns None + let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn returns_none_when_connector_e_not_spent() { + let db = create_local_db("sqlite::memory:").await; + let (btc_client, _) = BTCClient::new_mock_client(); + + let graph_id = Uuid::new_v4(); + let init_txid = make_txid(0x01); + insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; + + let graph = make_dummy_graph( + graph_id, + Uuid::new_v4(), + Some(SerializableTxid::from(init_txid)), + None, + ); + let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn returns_none_when_connector_e_spent_by_commit_timeout() { + let db = create_local_db("sqlite::memory:").await; + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let graph_id = Uuid::new_v4(); + let init_txid = make_txid(0x01); + let timeout_txid = make_txid(0x02); + let connector_e_vout = 2u64; + insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; + mock_adaptor.set_output_status( + init_txid, + connector_e_vout, + OutputStatus { spent: true, txid: Some(timeout_txid), vin: Some(0), status: None }, + ); + + let graph = make_dummy_graph( + graph_id, + Uuid::new_v4(), + Some(SerializableTxid::from(init_txid)), + Some(SerializableTxid::from(timeout_txid)), + ); + let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn returns_assert_ready_when_connector_e_spent_by_commit_pubin() { + let db = create_local_db("sqlite::memory:").await; + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let graph_id = Uuid::new_v4(); + let instance_id = Uuid::new_v4(); + let init_txid = make_txid(0x01); + let commit_pubin_txid = make_txid(0x03); + let timeout_txid = make_txid(0x02); + let connector_e_vout = 2u64; + insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; + mock_adaptor.set_output_status( + init_txid, + connector_e_vout, + OutputStatus { + spent: true, + txid: Some(commit_pubin_txid), // spend by commit txn + vin: Some(0), + status: None, + }, + ); + + let graph = make_dummy_graph( + graph_id, + instance_id, + Some(SerializableTxid::from(init_txid)), + Some(SerializableTxid::from(timeout_txid)), + ); + let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); + + let (actor, content) = result.expect("should detect commit-pubin"); + assert_eq!(actor, Actor::Operator); + match content { + GOATMessageContent::AssertReady(msg) => { + assert_eq!(msg.instance_id, instance_id); + assert_eq!(msg.graph_id, graph_id); + } + _ => panic!("expected AssertReady"), + } + } +} diff --git a/node/src/utils.rs b/node/src/utils.rs index 9d9f6a7b..ae1425fa 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -1957,6 +1957,54 @@ pub async fn get_watchtower_challenge_info( } Ok((challenge_txids, included_watchtowers)) } + +/// Returns `(btc_best_block_hash, included_watchtowers_bitmap)` from already-fetched +/// `get_watchtower_challenge_info` output. +pub async fn compute_operator_pubin_blockhash_and_bitmap( + btc_client: &BTCClient, + challenge_txids: &[Option], + included_watchtowers_bits: &[bool], +) -> Result<([u8; 32], [u8; 32])> { + let btc_best_block_hash = { + let mut largest: Option<(u32, BlockHash)> = None; + for txid in challenge_txids.iter().flatten() { + let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; + let (height, hash) = match (status.block_height, status.block_hash) { + (Some(height), Some(hash)) => (height, hash), + _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), + }; + if largest.is_none_or(|(h, _)| height > h) { + largest = Some((height, hash)); + } + } + largest + .map(|(_, hash)| hash.to_byte_array()) + .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx available"))? + }; + + let mut included_watchtowers = [0u8; 32]; + for (i, &included) in included_watchtowers_bits.iter().enumerate() { + if included && i < 256 { + included_watchtowers[i / 8] |= 1 << (i % 8); + } + } + + Ok((btc_best_block_hash, included_watchtowers)) +} + +/// Assembles the 96-byte guest pubin: +pub fn build_operator_guest_pubin( + btc_best_block_hash: &[u8; 32], + pubin_disprove_constant: &[u8; 32], + included_watchtowers: &[u8; 32], +) -> [u8; 96] { + let mut pubin = [0u8; 96]; + pubin[0..32].copy_from_slice(btc_best_block_hash); + pubin[32..64].copy_from_slice(pubin_disprove_constant); + pubin[64..96].copy_from_slice(included_watchtowers); + pubin +} + fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) @@ -2043,22 +2091,13 @@ pub async fn get_operator_wrapper_proof( return Ok((None, get_operator_proof_wait_secs())); } }; - let operator_committed_blockhash = { - let mut largest: Option<(u32, BlockHash)> = None; - for txid in watchtower_challenge_txids.iter().flatten() { - let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; - let (height, hash) = match (status.block_height, status.block_hash) { - (Some(height), Some(hash)) => (height, hash), - _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), - }; - if largest.is_none_or(|(largest_height, _)| height > largest_height) { - largest = Some((height, hash)); - } - } - largest - .map(|(_, hash)| hash.to_string()) - .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx is available"))? - }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); let base_url = Url::parse( &get_proof_build_rpc_host() @@ -4773,3 +4812,121 @@ pub struct OperatorBabeSetupState { #[serde(default)] pub asserted_wrapper_proof: Option>, } + +#[cfg(test)] +mod commit_pubin_tests { + use super::*; + use bitcoin::BlockHash; + use client::btc_chain::BTCClient; + use esplora_client::{Tx, TxStatus, Vin}; + use store::SerializableTxid; + + fn make_txid(byte: u8) -> Txid { + Txid::from_slice(&[byte; 32]).unwrap() + } + + fn make_block_hash(byte: u8) -> BlockHash { + BlockHash::from_slice(&[byte; 32]).unwrap() + } + + fn create_confirmed_tx( + txid: Txid, + spends: &[(Txid, u32)], + height: u32, + block_hash: BlockHash, + ) -> Tx { + Tx { + txid, + version: 2, + locktime: 0, + vin: spends + .iter() + .map(|(prev_txid, vout)| Vin { + txid: *prev_txid, + vout: *vout, + prevout: None, + scriptsig: ScriptBuf::default(), + witness: vec![], + sequence: 1, + is_coinbase: false, + }) + .collect(), + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(height), + block_hash: Some(block_hash), + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn test_get_watchtower_challenge_info_partial_inclusion() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let watchtower_init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let block_hash = make_block_hash(0xAA); + + // watchtower 0 spent vout 0 of the init tx; watchtower 1 did not spend vout 1 + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(watchtower_init_txid, 0)], 100, block_hash), + ); + + let (txids, bits) = get_watchtower_challenge_info( + &btc_client, + &SerializableTxid::from(watchtower_init_txid), + 2, // 2 watchtowers + ) + .await + .unwrap(); + + assert_eq!(txids, vec![Some(challenge_txid_wt0.to_string()), None]); + assert_eq!(bits, vec![true, false]); + } + + #[tokio::test] + async fn test_compute_operator_pubin_blockhash_and_bitmap() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let challenge_txid_wt2 = make_txid(0x02); + let block_hash_low = make_block_hash(0x10); + let block_hash_high = make_block_hash(0x20); + + // wt0 confirmed at height 100, wt2 at height 200 (highest) + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(init_txid, 0)], 100, block_hash_low), + ); + mock_adaptor.set_tx( + challenge_txid_wt2, + create_confirmed_tx(challenge_txid_wt2, &[(init_txid, 2)], 200, block_hash_high), + ); + + let challenge_txids = vec![ + Some(challenge_txid_wt0.to_string()), + None, + Some(challenge_txid_wt2.to_string()), + ]; + let bits = vec![true, false, true]; + + let (best_hash, bitmap) = + compute_operator_pubin_blockhash_and_bitmap(&btc_client, &challenge_txids, &bits) + .await + .unwrap(); + + // block at height 200 wins + assert_eq!(best_hash, block_hash_high.to_byte_array()); + + // bits 0 and 2 set → byte 0 = 0b0000_0101 + assert_eq!(bitmap[0], 0b0000_0101u8); + assert_eq!(&bitmap[1..], &[0u8; 31]); + } +} From 8f87426f7e1f072da468186609781996d172c3e2 Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 18 Jun 2026 09:56:12 +0700 Subject: [PATCH 2/9] change babe_adapter to adapt bitvm3-gc --- Cargo.lock | 8 +- Cargo.toml | 10 +- crates/bitvm-gc/src/babe_adapter.rs | 1188 ++++++++++++++------------- node/src/handle.rs | 8 +- 4 files changed, 614 insertions(+), 600 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f6a4613..6932f89d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5576,7 +5576,6 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12225,8 +12224,7 @@ dependencies = [ [[package]] name = "soldering-host" -version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +version = "0.0.1" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12236,6 +12234,7 @@ dependencies = [ "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", "garbled-snark-verifier", "rand 0.8.5", + "rand_chacha 0.3.1", "tracing", "verifiable-circuit-babe", "zkm-build", @@ -13911,7 +13910,6 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" dependencies = [ "aes", "ark-bn254", @@ -13930,9 +13928,11 @@ dependencies = [ "p3-maybe-rayon", "rand 0.8.5", "rand_chacha 0.3.1", + "rayon", "ripemd 0.2.0", "serde", "sha2 0.10.9", + "tracing", "zkm-sdk", "zkm-zkvm", ] diff --git a/Cargo.toml b/Cargo.toml index f8aab0ce..eb146875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,9 +107,13 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } #zkm-verifier = { path = "../Ziren/crates/verifier" } bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } -verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } +#verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +#garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +#soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +# +verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"} +garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"} +soldering-host = { path = "../bitvm2-gc/babe-programs/soldering/host" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" } diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index ccce0658..541d487a 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use anyhow::{Result, bail}; use ark_bn254::{Bn254, Fq, Fr}; +use ark_bn254::g1::G1Affine; use ark_groth16::VerifyingKey as Groth16VerifyingKey; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use garbled_snark_verifier::bag::S; @@ -16,25 +17,18 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; -use verifiable_circuit_babe::babe::{ - LAMPORT_N, ProverSetupState, WeKnownPi1SetupCt as RealSetupCt, babe_verifier_presign, -}; -use verifiable_circuit_babe::cac::{ - CACSetupPackage as RealCACSetupPackage, FinalizedInstanceData as RealFinalizedInstanceData, - cac_finalize_indices, -}; -use verifiable_circuit_babe::gc::{ - SparseAdaptorEntry as RealSparseAdaptorEntry, SparseAdaptorRow as RealSparseAdaptorRow, - SparseAdaptorTable as RealSparseAdaptorTable, -}; -use verifiable_circuit_babe::instance::BABEInstance; -use verifiable_circuit_babe::instance::commit::CACInstanceCommit as RealCACInstanceCommit; +use verifiable_circuit_babe::babe::{ProverSetupState, WeKnownPi1SetupCt, babe_verifier_presign, GC_INPUT_WIRES, interleave_dummy_positions, build_challenge_assert_witness_raw, deinterleave_dummy_positions}; +pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +use verifiable_circuit_babe::cac::cac_finalize_indices; +use verifiable_circuit_babe::dre::N; +use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; +use verifiable_circuit_babe::instance::CACInstance; +use verifiable_circuit_babe::instance::commit::{CACInstanceCommit as RealCACInstanceCommit, CACInstanceCommit}; use verifiable_circuit_babe::prover::BABEProver; -use verifiable_circuit_babe::soldering::{ - SolderingData as RealSolderingData, SolderingProof as RealSolderingProof, -}; -use verifiable_circuit_babe::verifier::BABEVerifier; - +use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; +use verifiable_circuit_babe::transactions::{ChallengeAssertWitnessRaw, TxAssertWitness}; +use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; +use verifiable_circuit_babe::verifier::{BABEVerifier, InstanceLightSecrets}; use crate::types::BitvmGcCircuitData; /// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. @@ -47,47 +41,51 @@ pub type OpenedInstanceSeeds = Vec<(usize, u64)>; pub type FinalizedInstances = Vec; pub type SetupAndSolderingData = (OpenedInstanceSeeds, FinalizedInstances, SolderingData); -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CACSetupPackage { - pub commits: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CACInstanceCommit { - pub epk: Vec<[[u8; 20]; 2]>, - pub wots_padding_epk: [[[u8; 20]; 2]; 4], - pub constant_commits: [[[u8; 32]; 2]; 2], - pub h_msg: [u8; 20], - pub h_ct_setup: [u8; 32], - pub com_adaptor: [u8; 32], - pub com_gc: [u8; 32], -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FinalizedInstanceData { - pub index: usize, - pub final_msg_hash: [u8; 20], - pub wire_hashes: Vec, - pub gc_commitment: [u8; 32], - pub adaptor_commitment: [u8; 32], - pub ct_setup_commitment: [u8; 32], - pub real_data: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RealFinalizedPayload { - pub gc_ciphertexts: Vec>, - pub adaptor_table: SerializableSparseAdaptorTable, - pub ct_setup: SerializableSetupCt, - pub constant_labels: [[u8; 16]; 2], - pub wots_padding_zero_labels: [[u8; 16]; 4], -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompactFinalizedInstanceData { - pub index: usize, - pub real_data: RealFinalizedPayload, -} +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct CACSetupPackage { +// pub commits: Vec, +// } +// +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct CACInstanceCommit { +// pub epk: Vec<[[u8; 20]; 2]>, +// pub wots_padding_epk: [[[u8; 20]; 2]; 4], +// pub constant_commits: [[[u8; 32]; 2]; 2], +// pub h_msg: [u8; 20], +// pub h_ct_setup: [u8; 32], +// pub com_adaptor: [u8; 32], +// pub com_gc: [u8; 32], +// } + +// #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct FinalizedInstanceData { +// pub index: usize, +// pub final_msg_hash: [u8; 20], +// // todo: we maybe dont need this, it's in the CACInstanceCommit::epk +// pub wire_hashes: Vec, +// // todo: we already has this in the package before. +// pub gc_commitment: [u8; 32], +// // todo: we already has this in the package before. +// pub adaptor_commitment: [u8; 32], +// // todo: we already has this in the package before. +// pub ct_setup_commitment: [u8; 32], +// pub real_data: Option, +// } +// +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct RealFinalizedPayload { +// pub gc_ciphertexts: Vec>, +// pub adaptor_table: SerializableSparseAdaptorTable, +// pub ct_setup: SerializableSetupCt, +// pub constant_labels: [[u8; 16]; 2], +// pub wots_padding_zero_labels: [[u8; 16]; 4], +// } + +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct CompactFinalizedInstanceData { // Replaced by FinalizedInstanceData +// pub index: usize, +// pub real_data: RealFinalizedPayload, +// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingData { @@ -98,33 +96,33 @@ pub struct CompactSolderingData { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingProofPayload { pub opened: OpenedInstanceSeeds, - pub finalized: Vec, + pub finalized: Vec, pub soldering: CompactSolderingData, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSetupCt { - pub ct2_r_delta_g2: Vec, - pub ct3_masked_msg: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorTable { - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorEntry { - pub x: SerializableSparseAdaptorRow, - pub y: SerializableSparseAdaptorRow, - pub z: SerializableSparseAdaptorRow, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorRow { - pub cts: Vec<[u8; 32]>, - pub offset: Vec, -} +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct SerializableSetupCt { replaced by WeKnownPi1SetupCt +// pub ct2_r_delta_g2: Vec, +// pub ct3_masked_msg: Vec, +// } + +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct SerializableSparseAdaptorTable { replaced by SparseAdaptorTable +// pub entries: Vec, +// } +// +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct SerializableSparseAdaptorEntry { replaced by SparseAdaptorEntry +// pub x: SerializableSparseAdaptorRow, +// pub y: SerializableSparseAdaptorRow, +// pub z: SerializableSparseAdaptorRow, +// } +// +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct SerializableSparseAdaptorRow { replaced by SparseAdaptorRow +// pub cts: Vec<[u8; 32]>, +// pub offset: Vec, +// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SolderingData { @@ -133,17 +131,16 @@ pub struct SolderingData { pub proof: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SolderedLabelsData { - pub base_commitment: Vec<([u8; 32], [u8; 32])>, - pub deltas: Vec>, - pub commitments: Vec>, -} +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +// pub struct SolderedLabelsData { +// pub base_commitment: Vec<([u8; 32], [u8; 32])>, +// pub deltas: Vec>, +// pub commitments: Vec>, +// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeVerifierPrivateState { pub instance_seeds: Vec, - pub temp_val: [u8; 32], pub statement_digest: [u8; 32], } @@ -162,79 +159,94 @@ pub struct BabeProverState { pub h_msgs: Vec<[u8; 20]>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BabeAssertWitness { - pub pi1: Vec, - #[serde(default)] - pub pubin_commitment: [u8; 32], - pub wots_sig: Vec<[u8; 21]>, -} +// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// pub struct BabeAssertWitness { // replace by TxAssertWitness +// // pub pi1: Vec, +// // #[serde(default)] +// // pub pubin_commitment: [u8; 32], +// pub wots_sig: Vec<[u8; 21]>, +// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeChallengeAssertWitness { pub verifier_index: usize, - pub input_labels: Vec<[u8; 16]>, - pub wots_sig: Vec<[u8; 21]>, + pub witness: ChallengeAssertWitnessRaw, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeWronglyChallengedWitness { pub verifier_index: usize, - pub final_msgs: Vec>, + // just need to contain one valid msg + pub final_msg: Vec, } -impl CACInstanceCommit { - /// Builds deterministic placeholder setup commitments for tests and wiring. - pub fn sample(seed: u8) -> Self { - let epk = (0..LAMPORT_N) - .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) - .collect(); - Self { - epk, - wots_padding_epk: padding_wire_hashes(), - constant_commits: [ - [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], - [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], - ], - h_msg: hash20(&[seed, 0xa0]), - h_ct_setup: hash32(&[seed, 0xa1]), - com_adaptor: hash32(&[seed, 0xa2]), - com_gc: hash32(&[seed, 0xa3]), - } + +/// Builds deterministic placeholder setup commitments for tests and wiring. +pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { + let epk = (0..GC_INPUT_WIRES) + .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) + .collect(); + + let constant_commit_sgc: Vec<_> = (0..SGC_PART1_CONSTANT_SIZE) + .map(|_| { + [ + hash32(&[seed, 0xf0, 0]), + hash32(&[seed, 0xf0, 1]), + ] + }) + .collect(); + + CACInstanceCommit { + epk, + constant_commits_fgc: [ + [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], + [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], + ], + constant_commits_sgc: constant_commit_sgc, + b_blind_commit: hash32(&[seed, 0xa0]), + h_msg: hash20(&[seed, 0xa1]), + h_ct_setup: hash32(&[seed, 0xa2]), + com_adaptor: [hash32(&[seed, 0xa3]), hash32(&[seed, 0xa4])], + com_gc: [hash32(&[seed, 0xa5]), hash32(&[seed, 0xa6]), hash32(&[seed, 0xa7])], } } -impl FinalizedInstanceData { - /// Builds deterministic placeholder finalized data for tests and graph wiring. - pub fn sample(index: usize) -> Self { - let seed = index as u8; - let wire_hashes = (0..INPUT_WIRE_NUM) - .map(|wire| WireHash { - true_label_hash: hash20(&[seed, wire as u8, 1]), - false_label_hash: hash20(&[seed, wire as u8, 0]), - }) - .collect(); - Self { - index, - final_msg_hash: hash20(&[seed, 0xb0]), - wire_hashes, - gc_commitment: hash32(&[seed, 0xb1]), - adaptor_commitment: hash32(&[seed, 0xb2]), - ct_setup_commitment: hash32(&[seed, 0xb3]), - real_data: None, +pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { + let seed = index as u8; + let adaptor_tables = [ + SparseAdaptorTable { + entries: vec![] + }, + SparseAdaptorTable { + entries: vec![] } - } -} + ]; + let ct_setup = WeKnownPi1SetupCt { + ct2_r_delta_g2: vec![], + ct3_masked_msg: vec![], + }; -impl CompactFinalizedInstanceData { - pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { - let real_data = finalized.real_data.clone().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(Self { index: finalized.index, real_data }) + FinalizedInstanceData { + index, + ciphertext_sets: [vec![], vec![], vec![]], + adaptor_tables, + ct_setup, + constant_labels_0: [S([0; 16]), S([0; 16])], + constant_labels_1: vec![], + b: G1Affine::identity(), } } + +// impl CompactFinalizedInstanceData { +// pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { +// let real_data = finalized.real_data.clone().ok_or_else(|| { +// anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) +// })?; +// Ok(Self { index: finalized.index, real_data }) +// } +// } + impl SolderingData { /// Builds deterministic placeholder soldering data for the selected finalized indices. pub fn sample(finalized_indices: Vec) -> Self { @@ -248,7 +260,7 @@ pub fn build_setup_package(n_cc: usize) -> Result { bail!("n_cc must be greater than zero"); } Ok(CACSetupPackage { - commits: (0..n_cc).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + commits: (0..n_cc).map(|index| sample_cac_instance_commit(index as u8)).collect(), }) } @@ -256,20 +268,19 @@ pub fn build_setup_package(n_cc: usize) -> Result { pub fn build_real_setup_package( n_cc: usize, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result<(CACSetupPackage, BabeVerifierPrivateState)> { if n_cc == 0 { bail!("n_cc must be greater than zero"); } ensure_real_gc_assets_configured()?; - let verifier = catch_unwind(AssertUnwindSafe(|| BABEVerifier::new(n_cc, vk, public_inputs))) + let verifier = catch_unwind(AssertUnwindSafe(|| BABEVerifier::new(n_cc, vk, static_inputs))) .map_err(|_| anyhow::anyhow!("real BABE verifier setup panicked while loading GC assets"))? .map_err(anyhow::Error::msg)?; - let package = from_real_package(&verifier.commit()); + let package = verifier.commit(); let private_state = BabeVerifierPrivateState { - instance_seeds: verifier.instances.iter().map(|instance| instance.seed).collect(), - temp_val: verifier.temp_val, - statement_digest: statement_digest(vk, public_inputs)?, + instance_seeds: verifier.get_seeds(), + statement_digest: statement_digest(vk, static_inputs)?, }; Ok((package, private_state)) @@ -282,15 +293,15 @@ pub fn open_real_setup_and_solder( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { ensure_real_gc_assets_configured()?; - if private_state.statement_digest != statement_digest(vk, public_inputs)? { + if private_state.statement_digest != statement_digest(vk, static_inputs)? { bail!("BABE setup statement does not match persisted verifier state"); } validate_finalized_indices(package, finalized_indices)?; - let verifier = restore_real_verifier(private_state, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { bail!("persisted BABE verifier state does not reproduce setup package"); } let bundle = soldering_builder @@ -298,11 +309,7 @@ pub fn open_real_setup_and_solder( .map_err(anyhow::Error::msg)?; Ok(( bundle.opened, - bundle - .finalized - .iter() - .map(|data| from_real_finalized(data, package)) - .collect::>>()?, + bundle.finalized, from_real_soldering(&bundle.soldering)?, )) } @@ -315,22 +322,15 @@ pub fn verify_real_setup( finalized: &[FinalizedInstanceData], soldering: &SolderingData, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_public_inputs: Fr, ) -> Result<()> { - let real_package = to_real_package(package); - let real_finalized = finalized - .iter() - .map(to_real_finalized) - .collect::>>()?; - let bundle = BabeBundle { opened: opened.to_vec(), - finalized: real_finalized, + finalized: finalized.to_vec(), soldering: to_real_soldering(soldering)?, - temp_hashlock: [0u8; 20], }; soldering_builder - .babe_prover_verify_setup(&real_package, &bundle, vk, public_inputs) + .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs) .map_err(anyhow::Error::msg) } @@ -342,10 +342,10 @@ pub fn compact_soldering_proof_payload( ) -> Result { Ok(CompactSolderingProofPayload { opened: opened.to_vec(), - finalized: finalized - .iter() - .map(CompactFinalizedInstanceData::try_from_finalized) - .collect::>>()?, + finalized: finalized.to_vec(), + // .iter() + // .map(CompactFinalizedInstanceData::try_from_finalized) + // .collect::>>()?, soldering: CompactSolderingData { finalized_indices: soldering.finalized_indices.clone(), proof: soldering.proof.clone(), @@ -358,11 +358,7 @@ pub fn expand_compact_soldering_proof_payload( package: &CACSetupPackage, payload: CompactSolderingProofPayload, ) -> Result { - let finalized = payload - .finalized - .into_iter() - .map(|data| expand_compact_finalized_instance(package, data)) - .collect::>>()?; + let finalized = payload.finalized.clone(); Ok((payload.opened, finalized, expand_compact_soldering_data(payload.soldering)?)) } @@ -372,7 +368,7 @@ pub fn derive_finalized_indices(package: &CACSetupPackage, m_cc: usize) -> Resul if m_cc == 0 || m_cc > n_cc { bail!("invalid m_cc {m_cc} for n_cc {n_cc}"); } - Ok(cac_finalize_indices(&to_real_package(package), m_cc)) + Ok(cac_finalize_indices(n_cc, m_cc)) } /// Opens non-finalized placeholder instances and returns finalized data plus soldering data. @@ -392,7 +388,7 @@ pub fn open_and_solder( .map(|index| (index, deterministic_seed(index))) .collect::>(); let finalized = - finalized_indices.iter().map(|index| FinalizedInstanceData::sample(*index)).collect(); + finalized_indices.iter().map(|index| sample_finalized_instance_data(*index)).collect(); let soldering = SolderingData::sample(finalized_indices.to_vec()); Ok((opened, finalized, soldering)) } @@ -427,9 +423,9 @@ pub fn verify_setup( if data.index >= n_cc { bail!("finalized index {} out of range", data.index); } - if data.wire_hashes.len() != INPUT_WIRE_NUM { - bail!("finalized index {} has invalid wire hash count", data.index); - } + // if data.wire_hashes.len() != INPUT_WIRE_NUM { + // bail!("finalized index {} has invalid wire hash count", data.index); + // } } Ok(()) } @@ -439,6 +435,8 @@ pub fn extract_gc_circuit_data( finalized: &[FinalizedInstanceData], soldering: &SolderingData, verifier_pubkey: bitcoin::PublicKey, + epk: &[[[u8; 20]; 2]], + h_msgs: &[[u8; 20]], ) -> Result { if finalized.len() != BABE_M_CC { bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); @@ -447,16 +445,23 @@ pub fn extract_gc_circuit_data( bail!("soldering finalized indices mismatch"); } let data = &finalized[0]; - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = - data.wire_hashes.clone().try_into().map_err(|wire_hashes: Vec| { + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = epk + .iter() + .map(|labels| WireHash { + false_label_hash: labels[0], + true_label_hash: labels[1], + }) + .collect::>() + .try_into() + .map_err(|wire_hashes: Vec| { anyhow::anyhow!( - "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", - wire_hashes.len() - ) + "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", + wire_hashes.len() + ) })?; Ok(BitvmGcCircuitData { verifier_pubkey, - final_msg_hashlocks: finalized.iter().map(|data| data.final_msg_hash).collect(), + final_msg_hashlocks: h_msgs.to_vec(), wire_hashes, }) } @@ -465,54 +470,63 @@ pub fn extract_gc_circuit_data( pub fn build_assert_witness( proof: &ark_groth16::Proof, assert_secret_key: &OperatorAssertSecretKey, -) -> Result { - build_assert_witness_with_pubin_commitment(proof, &[0u8; 32], assert_secret_key) -} - -pub fn build_assert_witness_with_pubin_commitment( - proof: &ark_groth16::Proof, - pubin_commitment: &[u8; 32], - assert_secret_key: &OperatorAssertSecretKey, -) -> Result { - if assert_secret_key.is_empty() { - bail!("operator WOTS secret key must not be empty"); - } - let pi1 = proof.a; - let mut pi1_bytes = Vec::new(); - pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); - let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); + dynamic_input: ark_bn254::Fr, +) -> Result { + let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); let wots_sig = Wots96::sign(assert_secret_key, &msg); - Ok(BabeAssertWitness { - pi1: pi1_bytes, - pubin_commitment: *pubin_commitment, - wots_sig: wots_sig.to_vec(), - }) -} - -pub fn assert_wots_message(assert_witness: &BabeAssertWitness) -> Result<[u8; 96]> { - let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; - Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) + Ok(TxAssertWitness { wots_sig: wots_sig.to_vec() }) +} + +// pub fn build_assert_witness_with_pubin_commitment( +// proof: &ark_groth16::Proof, +// pubin_commitment: &[u8; 32], +// assert_secret_key: &OperatorAssertSecretKey, +// ) -> Result { +// if assert_secret_key.is_empty() { +// bail!("operator WOTS secret key must not be empty"); +// } +// let pi1 = proof.a; +// let mut pi1_bytes = Vec::new(); +// pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); +// let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); +// let wots_sig = Wots96::sign(assert_secret_key, &msg); +// Ok(BabeAssertWitness { +// pi1: pi1_bytes, +// pubin_commitment: *pubin_commitment, +// wots_sig: wots_sig.to_vec(), +// }) +// } + +pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { + // let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) + // .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + // Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) } /// Builds a placeholder verifier challenge witness from an assert witness. pub fn build_challenge_assert_witness( verifier_state: &BabeVerifierState, - assert_witness: &BabeAssertWitness, + assert_witness: &TxAssertWitness, verifier_index: usize, ) -> Result { if verifier_state.finalized_indices.len() != BABE_M_CC { bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); } - if assert_witness.pi1.is_empty() || assert_witness.wots_sig.is_empty() { - bail!("invalid assert witness"); - } + let bytes = assert_wots_message(assert_witness)?; Ok(BabeChallengeAssertWitness { verifier_index, - input_labels: (0usize..INPUT_WIRE_NUM) - .map(|index| hash16_with_index(&assert_witness.pi1, index)) - .collect(), - wots_sig: assert_witness.wots_sig.clone(), + witness: ChallengeAssertWitnessRaw { + input_labels: (0usize..INPUT_WIRE_NUM) + .map(|index| hash16_with_index(&bytes, index)) + .collect(), + wots_sig: assert_witness.wots_sig.clone(), + } }) } @@ -523,110 +537,116 @@ pub fn build_real_challenge_assert_witness( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, operator_wots_pubkey: &OperatorAssertPublicKey, - assert_witness: &BabeAssertWitness, + assert_witness: &TxAssertWitness, verifier_index: usize, ) -> Result { if finalized_indices.len() != BABE_M_CC { bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); } - let verifier = restore_real_verifier(private_state, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { bail!("persisted BABE verifier state does not reproduce setup package"); } - let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + let expected_message = pi1_xd_to_wots96_msg(&pi1, x_d); let wots_sig = to_real_wots_sig(&assert_witness.wots_sig)?; let signed_message = Wots96::signature_to_message(&wots_sig); - let expected_message = pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment); if signed_message != expected_message { bail!("operator BABE assertion WOTS signature message does not match pi1/pubin"); } let base_idx = finalized_indices[0]; - let base_inst = verifier - .instances - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - let mut input_labels = base_inst - .compute_pi1_labels_based_on_value(pi1) - .into_iter() - .skip(2) - .map(|label| label.0) - .collect::>(); - let commit = package - .commits - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - input_labels.extend(pubin_input_labels(commit, &assert_witness.pubin_commitment)); - if input_labels.len() != INPUT_WIRE_NUM { - bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", input_labels.len()); + + let witness = build_challenge_assert_witness_raw( + &verifier, + assert_witness, + operator_wots_pubkey, + base_idx, + ); + if witness.is_none() { + bail!("cannot generate challenge assert witness"); } - let _ = operator_wots_pubkey; - Ok(BabeChallengeAssertWitness { verifier_index, input_labels, wots_sig: wots_sig.to_vec() }) + let witness = witness.unwrap(); + + if witness.input_labels.len() != INPUT_WIRE_NUM { + bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", witness.input_labels.len()); + } + Ok(BabeChallengeAssertWitness { + verifier_index, + witness, + }) } -/// Builds a wrongly-challenged witness from every recovered finalized-message preimage. +/// Builds a wrongly-challenged witness from a valid recovered finalized-message preimage. pub fn build_wrongly_challenged_witness( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, - final_msgs: Vec>, + final_msg: Vec, ) -> Result { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, challenge_witness, - final_msgs, + final_msg, ) } -/// Evaluates the native BABE garbled circuit and returns all finalized hashlock preimages. +/// Evaluates the native BABE garbled circuit and returns a finalized hashlock preimages. pub fn recover_real_wrongly_challenged_witness( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, proof: &ark_groth16::Proof, + vk: Groth16VerifyingKey, + dyn_pubin: ark_bn254::Fr, ) -> Result { let real_state = ProverSetupState { wots_sk_p: vec![], finalized: prover_state - .finalized - .iter() - .map(to_real_finalized) - .collect::>>()?, + .finalized.clone(), + // .iter() + // .map(to_real_finalized) + // .collect::>>()?, soldering: to_real_soldering(&prover_state.soldering)?, h_msgs: prover_state.h_msgs.clone(), presigs_v: babe_verifier_presign(), }; - recover_all_finalized_messages(prover_state, challenge_witness, proof, &real_state) + let mut prover = BABEProver::new(vk, proof.clone(), dyn_pubin); + recover_a_valid_finalized_messages(prover_state, challenge_witness, &mut prover, &real_state) } /// Builds a wrongly-challenged witness after validating all finalized preimages. pub fn build_wrongly_challenged_witness_from_preimages( h_msgs: &[[u8; 20]], challenge_witness: &BabeChallengeAssertWitness, - final_msgs: Vec>, + final_msg: Vec, ) -> Result { if h_msgs.len() != BABE_M_CC { bail!("wrongly challenged setup must contain exactly {BABE_M_CC} finalized hashlocks"); } - if final_msgs.len() != h_msgs.len() { - bail!("wrongly challenged witness must provide every finalized preimage"); - } - for (position, (final_msg, expected_hash)) in final_msgs.iter().zip(h_msgs).enumerate() { - if label_hash(final_msg) != *expected_hash { - bail!("message at finalized position {position} is not a valid preimage"); - } + + if !h_msgs.contains(&label_hash(&final_msg)) { + bail!("message is not a valid preimage"); } + Ok(BabeWronglyChallengedWitness { verifier_index: challenge_witness.verifier_index, - final_msgs, + final_msg, }) } fn ensure_real_gc_assets_configured() -> Result<()> { - for name in ["GC_GATES_PATH", "GC_INDICES_PATH"] { + for name in [ + "FGC_GATES_PATH", "FGC_OUT_INDICES_PATH", "SGC_GATES_PATH", "SGC_OUT_INDICES_PATH", + "FGC_COMPACT_GATES_PATH", "FGC_COMPACT_OUT_INDICES_PATH", "SGC_COMPACT_GATES_PATH", "SGC_COMPACT_OUT_INDICES_PATH" + ] { let path = PathBuf::from( std::env::var(name) - .map_err(|_| anyhow::anyhow!("{name} is required for real BABE setup"))?, + .map_err(|_| anyhow::anyhow!("{name} is required for real CAC setup"))?, ); if !path.is_file() { bail!("{name} does not point to a readable file: {}", path.display()); @@ -635,30 +655,27 @@ fn ensure_real_gc_assets_configured() -> Result<()> { Ok(()) } -fn statement_digest(vk: &Groth16VerifyingKey, public_inputs: &[Fr]) -> Result<[u8; 32]> { +fn statement_digest(vk: &Groth16VerifyingKey, static_inputs: Fr) -> Result<[u8; 32]> { let mut bytes = Vec::new(); vk.serialize_compressed(&mut bytes)?; - for public_input in public_inputs { - public_input.serialize_compressed(&mut bytes)?; - } + static_inputs.serialize_compressed(&mut bytes)?; Ok(hash32(&bytes)) } fn restore_real_verifier( state: &BabeVerifierPrivateState, + package: &CACSetupPackage, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { - let instances = state - .instance_seeds - .par_iter() - .map(|seed| { - let mut instance = BABEInstance::new_from_seed(*seed); - instance.enc_setup(vk, public_inputs).map_err(anyhow::Error::msg)?; - Ok(instance) - }) - .collect::>>()?; - Ok(BABEVerifier { instances, temp_val: state.temp_val }) + let verifier = BABEVerifier::from_state( + &state.instance_seeds, package, vk, static_inputs + ); + if verifier.is_none() { + Err(anyhow::anyhow!("Cannot restore real verifier")) + } else { + Ok(verifier.unwrap()) + } } fn validate_finalized_indices( @@ -678,153 +695,153 @@ fn validate_finalized_indices( Ok(()) } -fn from_real_package(package: &RealCACSetupPackage) -> CACSetupPackage { - CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } -} - -fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { - CACInstanceCommit { - epk: commit.epk.clone(), - wots_padding_epk: padding_wire_hashes(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - } -} - -fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { - RealCACSetupPackage { - commits: package - .commits - .iter() - .map(|commit| RealCACInstanceCommit { - epk: commit.epk.clone(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - }) - .collect(), - } -} - -fn from_real_finalized( - finalized: &RealFinalizedInstanceData, - package: &CACSetupPackage, -) -> Result { - let real_data = RealFinalizedPayload { - gc_ciphertexts: finalized - .gc_ciphertexts - .iter() - .map(|ciphertext| ciphertext.map(|label| label.0)) - .collect(), - adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), - ct_setup: SerializableSetupCt { - ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], - wots_padding_zero_labels: [[0u8; 16]; 4], - }; - expand_compact_finalized_instance( - package, - CompactFinalizedInstanceData { index: finalized.index, real_data }, - ) -} - -fn expand_compact_finalized_instance( - package: &CACSetupPackage, - finalized: CompactFinalizedInstanceData, -) -> Result { - let commit = package - .commits - .get(finalized.index) - .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; - if commit.epk.len() != LAMPORT_N { - bail!( - "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", - finalized.index, - commit.epk.len() - ); - } - let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); - wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); - wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); - wire_hashes.extend(pubin_wire_hashes(commit)); - Ok(FinalizedInstanceData { - index: finalized.index, - final_msg_hash: commit.h_msg, - wire_hashes, - gc_commitment: commit.com_gc, - adaptor_commitment: commit.com_adaptor, - ct_setup_commitment: commit.h_ct_setup, - real_data: Some(finalized.real_data), - }) -} - -fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { - let payload = finalized.real_data.as_ref().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(RealFinalizedInstanceData { - index: finalized.index, - gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), - adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, - ct_setup: RealSetupCt { - ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], - }) -} - -fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { - SerializableSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| SerializableSparseAdaptorEntry { - x: from_real_adaptor_row(&entry.x), - y: from_real_adaptor_row(&entry.y), - z: from_real_adaptor_row(&entry.z), - }) - .collect(), - } -} - -fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { - let mut offset = Vec::new(); - row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); - SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } -} - -fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { - Ok(RealSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| { - Ok(RealSparseAdaptorEntry { - x: to_real_adaptor_row(&entry.x)?, - y: to_real_adaptor_row(&entry.y)?, - z: to_real_adaptor_row(&entry.z)?, - }) - }) - .collect::>>()?, - }) -} - -fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { - Ok(RealSparseAdaptorRow { - cts: row.cts.clone(), - offset: Fq::deserialize_compressed(row.offset.as_slice())?, - }) -} +// fn from_real_package(package: &RealCACSetupPackage) -> CACSetupPackage { +// CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } +// } +// +// fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { +// CACInstanceCommit { +// epk: commit.epk.clone(), +// wots_padding_epk: padding_wire_hashes(), +// constant_commits: commit.constant_commits, +// h_msg: commit.h_msg, +// h_ct_setup: commit.h_ct_setup, +// com_adaptor: commit.com_adaptor, +// com_gc: commit.com_gc, +// } +// } + +// fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { +// RealCACSetupPackage { +// commits: package +// .commits +// .iter() +// .map(|commit| RealCACInstanceCommit { +// epk: commit.epk.clone(), +// constant_commits: commit.constant_commits, +// h_msg: commit.h_msg, +// h_ct_setup: commit.h_ct_setup, +// com_adaptor: commit.com_adaptor, +// com_gc: commit.com_gc, +// }) +// .collect(), +// } +// } + +// fn from_real_finalized( +// finalized: &RealFinalizedInstanceData, +// package: &CACSetupPackage, +// ) -> Result { +// let real_data = RealFinalizedPayload { +// gc_ciphertexts: finalized +// .gc_ciphertexts +// .iter() +// .map(|ciphertext| ciphertext.map(|label| label.0)) +// .collect(), +// adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), +// ct_setup: SerializableSetupCt { +// ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), +// ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), +// }, +// constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], +// wots_padding_zero_labels: [[0u8; 16]; 4], +// }; +// expand_compact_finalized_instance( +// package, +// CompactFinalizedInstanceData { index: finalized.index, real_data }, +// ) +// } + +// fn expand_compact_finalized_instance( +// package: &CACSetupPackage, +// finalized: FinalizedInstanceData, +// ) -> Result { +// let commit = package +// .commits +// .get(finalized.index) +// .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; +// if commit.epk.len() != LAMPORT_N { +// bail!( +// "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", +// finalized.index, +// commit.epk.len() +// ); +// } +// let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); +// wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); +// wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); +// wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); +// wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); +// wire_hashes.extend(pubin_wire_hashes(commit)); +// Ok(FinalizedInstanceData { +// index: finalized.index, +// final_msg_hash: commit.h_msg, +// wire_hashes, +// gc_commitment: commit.com_gc, +// adaptor_commitment: commit.com_adaptor, +// ct_setup_commitment: commit.h_ct_setup, +// real_data: Some(finalized.real_data), +// }) +// } + +// fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { +// let payload = finalized.real_data.as_ref().ok_or_else(|| { +// anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) +// })?; +// Ok(RealFinalizedInstanceData { +// index: finalized.index, +// gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), +// adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, +// ct_setup: RealSetupCt { +// ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), +// ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), +// }, +// constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], +// }) +// } + +// fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { +// SerializableSparseAdaptorTable { +// entries: table +// .entries +// .iter() +// .map(|entry| SerializableSparseAdaptorEntry { +// x: from_real_adaptor_row(&entry.x), +// y: from_real_adaptor_row(&entry.y), +// z: from_real_adaptor_row(&entry.z), +// }) +// .collect(), +// } +// } +// +// fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { +// let mut offset = Vec::new(); +// row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); +// SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } +// } + +// fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { +// Ok(RealSparseAdaptorTable { +// entries: table +// .entries +// .iter() +// .map(|entry| { +// Ok(RealSparseAdaptorEntry { +// x: to_real_adaptor_row(&entry.x)?, +// y: to_real_adaptor_row(&entry.y)?, +// z: to_real_adaptor_row(&entry.z)?, +// }) +// }) +// .collect::>>()?, +// }) +// } + +// fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { +// Ok(RealSparseAdaptorRow { +// cts: row.cts.clone(), +// offset: Fq::deserialize_compressed(row.offset.as_slice())?, +// }) +// } fn from_real_soldering(soldering: &RealSolderingData) -> Result { let output = soldering.soldering_proof.output().map_err(anyhow::Error::msg)?; @@ -866,10 +883,10 @@ fn to_real_soldering(soldering: &SolderingData) -> Result { }) } -fn recover_all_finalized_messages( +fn recover_a_valid_finalized_messages( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, - proof: &ark_groth16::Proof, + prover: &mut BABEProver, real_state: &ProverSetupState, ) -> Result { if real_state.finalized.len() != prover_state.h_msgs.len() { @@ -879,7 +896,13 @@ fn recover_all_finalized_messages( bail!("BABE prover state has no finalized instances"); } - let base_input_labels = pi1_labels_from_challenge(&challenge_witness.input_labels)?; + let base_input_labels: Vec = challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); + // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). + let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); + let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).collect(); + let soldered_output = &prover_state.soldering.soldered_output; + let real_soldering = to_real_soldering(&prover_state.soldering)?; + let soldered_output = &prover_state.soldering.soldered_output; if soldered_output.base_commitment.len() != base_input_labels.len() { bail!( @@ -889,109 +912,102 @@ fn recover_all_finalized_messages( ); } - let prover = BABEProver::new(proof.clone()); - let final_msgs = real_state - .finalized - .iter() - .zip(&prover_state.h_msgs) - .enumerate() - .map(|(position, (finalized, expected_hash))| { - let input_labels = - soldered_input_labels(&base_input_labels, soldered_output, position)?; - recover_finalized_message( - &prover, - proof, - finalized, - &input_labels, - *expected_hash, - position, - ) - }) - .collect::>>()?; - - Ok(BabeWronglyChallengedWitness { - verifier_index: challenge_witness.verifier_index, - final_msgs, - }) -} + let found = prover.check_compute_msg( + &prover_state.finalized, + &pi1_labels, + &x_d_labels, + &real_soldering, + &prover_state.h_msgs, + ); -fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { - if input_labels.len() == LAMPORT_N { - return Ok(input_labels.iter().copied().map(S).collect()); - } - if input_labels.len() != INPUT_WIRE_NUM { - bail!( - "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", - input_labels.len() - ); - } - Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) -} -fn soldered_input_labels( - base_input_labels: &[S], - soldered_output: &SolderedLabelsData, - finalized_position: usize, -) -> Result> { - if finalized_position == 0 { - return Ok(base_input_labels.to_vec()); - } - let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { - anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") - })?; - if deltas.len() != base_input_labels.len() { - bail!( - "soldering delta count {} does not match BABE input label count {}", - deltas.len(), - base_input_labels.len() - ); + if !found { + bail!("Cannot find any valid msg"); } - Ok(base_input_labels - .iter() - .enumerate() - .map(|(wire, &base_label)| { - let (delta_false, delta_true) = deltas[wire]; - if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { - base_label ^ S(delta_false) - } else { - base_label ^ S(delta_true) - } - }) - .collect()) + Ok(BabeWronglyChallengedWitness { + verifier_index: challenge_witness.verifier_index, + final_msg: prover.valid_msg.unwrap().to_vec(), + }) } -fn recover_finalized_message( - prover: &BABEProver, - proof: &ark_groth16::Proof, - finalized: &RealFinalizedInstanceData, - input_labels: &[S], - expected_hash: [u8; 20], - finalized_position: usize, -) -> Result> { - let mut full_labels = Vec::with_capacity(2 + input_labels.len()); - full_labels.push(finalized.constant_labels[0]); - full_labels.push(finalized.constant_labels[1]); - full_labels.extend_from_slice(input_labels); - - let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); - let ct_prove = prover.compute_ct_prove( - &mut circuit, - &gc_output_indices, - &full_labels, - &finalized.gc_ciphertexts, - &finalized.adaptor_table, - ); - let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) - .map_err(anyhow::Error::msg)?; - let final_msg = msg.to_vec(); - if label_hash(&final_msg) != expected_hash { - bail!( - "recovered message at finalized position {finalized_position} does not match hashlock" - ); - } - Ok(final_msg) -} +// fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { +// if input_labels.len() == LAMPORT_N { +// return Ok(input_labels.iter().copied().map(S).collect()); +// } +// if input_labels.len() != INPUT_WIRE_NUM { +// bail!( +// "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", +// input_labels.len() +// ); +// } +// Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) +// } + +// fn soldered_input_labels( +// base_input_labels: &[S], +// soldered_output: &SolderedLabelsData, +// finalized_position: usize, +// ) -> Result> { +// if finalized_position == 0 { +// return Ok(base_input_labels.to_vec()); +// } +// let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { +// anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") +// })?; +// if deltas.len() != base_input_labels.len() { +// bail!( +// "soldering delta count {} does not match BABE input label count {}", +// deltas.len(), +// base_input_labels.len() +// ); +// } +// +// Ok(base_input_labels +// .iter() +// .enumerate() +// .map(|(wire, &base_label)| { +// let (delta_false, delta_true) = deltas[wire]; +// if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { +// base_label ^ S(delta_false) +// } else { +// base_label ^ S(delta_true) +// } +// }) +// .collect()) +// } + +// fn recover_finalized_message( +// prover: &BABEProver, +// proof: &ark_groth16::Proof, +// finalized: &RealFinalizedInstanceData, +// input_labels: &[S], +// expected_hash: [u8; 20], +// finalized_position: usize, +// ) -> Result> { +// let mut full_labels = Vec::with_capacity(2 + input_labels.len()); +// full_labels.push(finalized.constant_labels[0]); +// full_labels.push(finalized.constant_labels[1]); +// full_labels.extend_from_slice(input_labels); +// +// let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); +// let ct_prove = prover.compute_ct_prove( +// &mut circuit, +// &gc_output_indices, +// &full_labels, +// &finalized.gc_ciphertexts, +// &finalized.adaptor_table, +// ); +// let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) +// .map_err(anyhow::Error::msg)?; +// let final_msg = msg.to_vec(); +// if label_hash(&final_msg) != expected_hash { +// bail!( +// "recovered message at finalized position {finalized_position} does not match hashlock" +// ); +// } +// Ok(final_msg) +// } fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { wots_sig.try_into().map_err(|_| { @@ -1002,64 +1018,64 @@ fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature }) } -fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { - let false_hash = label_hash(&vec![0u8; 16]); - let true_hash = label_hash(&vec![1u8; 16]); - [[false_hash, true_hash]; 4] -} +// fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { +// let false_hash = label_hash(&vec![0u8; 16]); +// let true_hash = label_hash(&vec![1u8; 16]); +// [[false_hash, true_hash]; 4] +// } fn deterministic_seed(index: usize) -> u64 { u64::from_le_bytes(hash32(&(index as u64).to_le_bytes())[0..8].try_into().expect("8 bytes")) } -fn to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { - WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } -} - -fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { - let mut msg = [0u8; 96]; - let mut tmp = Vec::new(); - - pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); - msg[..32].copy_from_slice(&tmp); - - tmp.clear(); - pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); - msg[32..64].copy_from_slice(&tmp); - - msg[64..96].copy_from_slice(pubin_commitment); - msg -} - -fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { - (0..256) - .map(|index| WireHash { - false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), - true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), - }) - .collect() -} - -fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { - (0..256) - .map(|index| { - let byte = pubin_commitment[index / 8]; - let bit = ((byte >> (index % 8)) & 1) == 1; - pubin_label(commit, index, bit) - }) - .collect() -} - -fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { - let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); - bytes.extend_from_slice(&commit.h_ct_setup); - bytes.extend_from_slice(&commit.com_adaptor); - bytes.extend_from_slice(&commit.com_gc); - bytes.extend_from_slice(&commit.h_msg); - bytes.extend_from_slice(&(index as u64).to_le_bytes()); - bytes.push(u8::from(bit)); - hash16(&bytes) -} +// fn to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { +// WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } +// } + +// fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { +// let mut msg = [0u8; 96]; +// let mut tmp = Vec::new(); +// +// pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); +// msg[..32].copy_from_slice(&tmp); +// +// tmp.clear(); +// pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); +// msg[32..64].copy_from_slice(&tmp); +// +// msg[64..96].copy_from_slice(pubin_commitment); +// msg +// } + +// fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { +// (0..256) +// .map(|index| WireHash { +// false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), +// true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), +// }) +// .collect() +// } +// +// fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { +// (0..256) +// .map(|index| { +// let byte = pubin_commitment[index / 8]; +// let bit = ((byte >> (index % 8)) & 1) == 1; +// pubin_label(commit, index, bit) +// }) +// .collect() +// } + +// fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { +// let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); +// bytes.extend_from_slice(&commit.h_ct_setup); +// bytes.extend_from_slice(&commit.com_adaptor); +// bytes.extend_from_slice(&commit.com_gc); +// bytes.extend_from_slice(&commit.h_msg); +// bytes.extend_from_slice(&(index as u64).to_le_bytes()); +// bytes.push(u8::from(bit)); +// hash16(&bytes) +// } fn hash20(data: &[u8]) -> [u8; 20] { let hash = hash32(data); diff --git a/node/src/handle.rs b/node/src/handle.rs index 9d8ffff6..44f1ac31 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -16,13 +16,7 @@ use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::hashes::{Hash, sha256}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{ - BABE_M_CC, BABE_N_CC, BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, - BabeProverState, CACSetupPackage, CompactSolderingProofPayload, assert_wots_message, - build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, - derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, - open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, -}; +use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, CompactSolderingProofPayload, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; From 63e9837d627c83bba723c0bb121e558f0227351d Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 18 Jun 2026 17:14:41 +0700 Subject: [PATCH 3/9] use operator proof & adapt handler to bitvm3 partial binding. --- crates/bitvm-gc/src/babe_adapter.rs | 12 +- node/src/action.rs | 4 +- node/src/env.rs | 56 ++++++--- node/src/handle.rs | 72 +++++++---- node/src/utils.rs | 177 +++++++++++++++++++++++++++- 5 files changed, 267 insertions(+), 54 deletions(-) diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 541d487a..0c43a75d 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -26,7 +26,8 @@ use verifiable_circuit_babe::instance::CACInstance; use verifiable_circuit_babe::instance::commit::{CACInstanceCommit as RealCACInstanceCommit, CACInstanceCommit}; use verifiable_circuit_babe::prover::BABEProver; use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; -use verifiable_circuit_babe::transactions::{ChallengeAssertWitnessRaw, TxAssertWitness}; +pub use verifiable_circuit_babe::transactions::TxAssertWitness; +use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; use verifiable_circuit_babe::verifier::{BABEVerifier, InstanceLightSecrets}; use crate::types::BitvmGcCircuitData; @@ -432,19 +433,10 @@ pub fn verify_setup( /// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. pub fn extract_gc_circuit_data( - finalized: &[FinalizedInstanceData], - soldering: &SolderingData, verifier_pubkey: bitcoin::PublicKey, epk: &[[[u8; 20]; 2]], h_msgs: &[[u8; 20]], ) -> Result { - if finalized.len() != BABE_M_CC { - bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); - } - if soldering.finalized_indices != finalized.iter().map(|data| data.index).collect::>() { - bail!("soldering finalized indices mismatch"); - } - let data = &finalized[0]; let wire_hashes: [WireHash; INPUT_WIRE_NUM] = epk .iter() .map(|labels| WireHash { diff --git a/node/src/action.rs b/node/src/action.rs index 08e22cbf..16b87a3d 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result, anyhow}; use bitcoin::{PublicKey, Txid}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ - BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, + BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, TxAssertWitness, CACSetupPackage, }; use bitvm_lib::committee::*; @@ -245,7 +245,7 @@ pub struct AssertSent { pub instance_id: Uuid, pub graph_id: Uuid, pub assert_txid: Txid, - pub assert_witness: Option, + pub assert_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct ChallengeAssertSent { diff --git a/node/src/env.rs b/node/src/env.rs index b923552d..d79a46af 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -79,12 +79,19 @@ pub const ENV_GOAT_NETWORK: &str = "GOAT_NETWORK"; pub const ENV_WATCHTOWER_PROOF_WAIT_SECS: &str = "WATCHTOWER_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_PROOF_WAIT_SECS: &str = "OPERATOR_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_VK_HASH: &str = "OPERATOR_VK_HASH"; +pub const ENV_OPERATOR_ZKM_VERSION: &str = "OPERATOR_ZKM_VERSION"; pub const ENV_OPERATOR_WRAPPER_VK_HASH: &str = "OPERATOR_WRAPPER_VK_HASH"; pub const ENV_OPERATOR_WRAPPER_ZKM_VERSION: &str = "OPERATOR_WRAPPER_ZKM_VERSION"; pub const DEFAULT_WATCHTOWER_PROOF_WAIT_SECS: usize = 60; pub const DEFAULT_OPERATOR_PROOF_WAIT_SECS: usize = 60; -pub const ENV_GC_GATES_PATH: &str = "GC_GATES_PATH"; -pub const ENV_GC_INDICES_PATH: &str = "GC_INDICES_PATH"; +pub const ENV_FGC_GATES_PATH: &str = "FGC_GATES_PATH"; +pub const ENV_FGC_OUT_INDICES_PATH: &str = "FGC_OUT_INDICES_PATH"; +pub const ENV_SGC_GATES_PATH: &str = "SGC_GATES_PATH"; +pub const ENV_SGC_OUT_INDICES_PATH: &str = "SGC_OUT_INDICES_PATH"; +pub const ENV_FGC_COMPACT_GATES_PATH: &str = "FGC_COMPACT_GATES_PATH"; +pub const ENV_FGC_COMPACT_OUT_INDICES_PATH: &str = "FGC_COMPACT_OUT_INDICES_PATH"; +pub const ENV_SGC_COMPACT_GATES_PATH: &str = "SGC_COMPACT_GATES_PATH"; +pub const ENV_SGC_COMPACT_OUT_INDICES_PATH: &str = "SGC_COMPACT_OUT_INDICES_PATH"; pub const ENV_BABE_SETUP_STATE_DIR: &str = "BABE_SETUP_STATE_DIR"; pub const ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH: &str = "SOLDERING_PROOF_PAYLOAD_STORE_PATH"; @@ -601,6 +608,11 @@ pub fn get_operator_vk_hash() -> anyhow::Result<[u8; 32]> { hex_parse::<32>(&value).map_err(|err| anyhow::anyhow!("invalid {ENV_OPERATOR_VK_HASH}: {err}")) } +pub fn get_operator_zkm_version() -> anyhow::Result { + std::env::var(ENV_OPERATOR_ZKM_VERSION) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_ZKM_VERSION} needs to be set")) +} + pub fn get_operator_wrapper_vk_hash() -> anyhow::Result { std::env::var(ENV_OPERATOR_WRAPPER_VK_HASH) .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_VK_HASH} needs to be set")) @@ -611,23 +623,37 @@ pub fn get_operator_wrapper_zkm_version() -> anyhow::Result { .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_ZKM_VERSION} needs to be set")) } -/// Returns the configured GC asset paths after checking that they are readable files. -/// TODO: maybe multi files -pub fn get_babe_gc_asset_paths() -> anyhow::Result<(PathBuf, PathBuf)> { - let gates_path = PathBuf::from( - std::env::var(ENV_GC_GATES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_GATES_PATH} is missing"))?, - ); - let indices_path = PathBuf::from( - std::env::var(ENV_GC_INDICES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_INDICES_PATH} is missing"))?, - ); - for (name, path) in [(ENV_GC_GATES_PATH, &gates_path), (ENV_GC_INDICES_PATH, &indices_path)] { +pub struct BabeGcAssetPaths { + pub fgc_gates: PathBuf, + pub fgc_out_indices: PathBuf, + pub sgc_gates: PathBuf, + pub sgc_out_indices: PathBuf, + pub fgc_compact_gates: PathBuf, + pub fgc_compact_out_indices: PathBuf, + pub sgc_compact_gates: PathBuf, + pub sgc_compact_out_indices: PathBuf, +} + +pub fn get_babe_gc_asset_paths() -> anyhow::Result { + fn read_path(name: &str) -> anyhow::Result { + let path = PathBuf::from( + std::env::var(name).map_err(|_| anyhow::anyhow!("{name} is missing"))?, + ); if !path.is_file() { anyhow::bail!("{name} does not point to a readable file: {}", path.display()); } + Ok(path) } - Ok((gates_path, indices_path)) + Ok(BabeGcAssetPaths { + fgc_gates: read_path(ENV_FGC_GATES_PATH)?, + fgc_out_indices: read_path(ENV_FGC_OUT_INDICES_PATH)?, + sgc_gates: read_path(ENV_SGC_GATES_PATH)?, + sgc_out_indices: read_path(ENV_SGC_OUT_INDICES_PATH)?, + fgc_compact_gates: read_path(ENV_FGC_COMPACT_GATES_PATH)?, + fgc_compact_out_indices: read_path(ENV_FGC_COMPACT_OUT_INDICES_PATH)?, + sgc_compact_gates: read_path(ENV_SGC_COMPACT_GATES_PATH)?, + sgc_compact_out_indices: read_path(ENV_SGC_COMPACT_OUT_INDICES_PATH)?, + }) } pub fn get_instance_maintenance_batch_size() -> u32 { diff --git a/node/src/handle.rs b/node/src/handle.rs index 44f1ac31..2450f563 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -16,7 +16,7 @@ use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::hashes::{Hash, sha256}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, CompactSolderingProofPayload, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage}; +use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; @@ -1203,9 +1203,9 @@ async fn handle_init_graph_verifier( } else { get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE setup")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let (setup_package, private_state) = tokio::task::spawn_blocking(move || { - build_real_setup_package(BABE_N_CC, &vk, &public_inputs) + build_real_setup_package(BABE_N_CC, &vk, static_input) }) .await .context("real BABE setup task failed")??; @@ -1402,7 +1402,7 @@ async fn handle_cut_circuits_verifier( get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE opening")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let private_state = verifier_state.private_state.clone(); let selected_indices = selected_circuit_indexes.clone(); let package_for_opening = setup_package.clone(); @@ -1418,7 +1418,7 @@ async fn handle_cut_circuits_verifier( &package_for_opening, &selected_indices, &vk, - &public_inputs, + static_input, ) }) .await @@ -1658,7 +1658,7 @@ async fn handle_compact_soldering_proof_operator( .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let package_for_validation = setup_package.clone(); let opened_for_validation = opened.clone(); let finalized_for_validation = finalized.clone(); @@ -1677,13 +1677,18 @@ async fn handle_compact_soldering_proof_operator( &finalized_for_validation, &soldering_for_validation, &vk, - &public_inputs, + static_input, ) }) .await .context("real BABE setup verification task failed")??; - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey)?; + if finalized.len() != BABE_M_CC { + bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + } + let epk = &setup_package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &h_msgs)?; let prover_state = BabeProverState { package: setup_package.clone(), finalized, @@ -3706,7 +3711,7 @@ async fn handle_assert_ready_operator( Some(v) => v, None => return Ok(()), }; - let (operator_wrapper_proof, wait_secs) = get_operator_wrapper_proof( + let (operator_proof, wait_secs) = get_operator_proof( ctx.local_db, ctx.http_client, &graph, @@ -3724,17 +3729,16 @@ async fn handle_assert_ready_operator( return Ok(()); } - let Some(operator_wrapper_proof) = operator_wrapper_proof else { + let Some(operator_proof) = operator_proof else { return Ok(()); }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; - // TODO: replace with build_assert_witness_with_pubin_commitment passing the real - // x_d = sha256(guest_pubin) recomputed from on-chain watchtower challenge data. - let assert_witness = build_assert_witness(&operator_wrapper_proof.proof, &assert_secret_key)?; + let dynamic_input = operator_proof.public_inputs[1]; + let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; let mut asserted_wrapper_proof = Vec::new(); - operator_wrapper_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; + operator_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; let operator_state = setup_state @@ -3770,7 +3774,7 @@ async fn handle_assert_sent_verifier( instance_id: Uuid, graph_id: Uuid, assert_txid: Txid, - assert_witness: &Option, + assert_witness: &Option, ) -> Result<()> { // TODO: check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction let (graph, _graph_status, _graph_sub_status) = @@ -3821,18 +3825,19 @@ async fn handle_assert_sent_verifier( return Ok(()); } let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, &saved_verifier_state.setup_package, &saved_verifier_state.finalized_indices, &vk, - &public_inputs, + static_input, &graph.parameters.operator_assert_wots_pubkey, assert_witness, verifier_index, )?; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3906,6 +3911,7 @@ async fn handle_challenge_assert_sent_operator( }; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3961,15 +3967,27 @@ async fn handle_challenge_assert_sent_operator( .ok_or_else(|| anyhow!("missing asserted wrapper proof for graph {graph_id}"))?; let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) .context("deserialize asserted wrapper proof")?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE wrongly challenged")?; + let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } + .recover_pi1_xd_without_verify() + .ok_or_else(|| anyhow!("cannot recover dynamic input from challenge witness WOTS signature"))?; let wrongly_challenged_witness = - recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; - // TODO: The wrongly-challenged script accepts any one finalized-message preimage. - let final_msg = wrongly_challenged_witness - .final_msgs - .first() - .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; - let (wrongly_challenged_input, _amount) = - operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; + recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof, vk, dyn_pubin)?; + let (wrongly_challenged_input, _amount) = operator_sign_wrongly_challenged( + &graph, + verifier_index, + &wrongly_challenged_witness.final_msg, + )?; + + // let wrongly_challenged_witness = + // recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; + // // TODO: The wrongly-challenged script accepts any one finalized-message preimage. + // let final_msg = wrongly_challenged_witness + // .final_msgs + // .first() + // .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; + // let (wrongly_challenged_input, _amount) = + // operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; let wrongly_challenged_tx = bitcoin::Transaction { version: bitcoin::transaction::Version(2), lock_time: bitcoin::absolute::LockTime::ZERO, @@ -4619,7 +4637,9 @@ mod tests { let package = build_setup_package(BABE_M_CC + 1).unwrap(); let selected = (0..BABE_M_CC).collect::>(); let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()).unwrap(); + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs).unwrap(); let prover_state = BabeProverState { package: package.clone(), finalized, diff --git a/node/src/utils.rs b/node/src/utils.rs index ae1425fa..08c6c0f0 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -25,7 +25,8 @@ use bitcoin::{ XOnlyPublicKey, }; use bitcoin_light_client_circuit::{ - VK_HASH_SIZE, build_watchtower_commitment, wrapper_public_values, + VK_HASH_SIZE, build_watchtower_commitment, decode_operator_public_outputs, + wrapper_public_values, }; use bitvm::treepp::*; use bitvm_lib::actors::Actor; @@ -116,6 +117,16 @@ pub type VerifyingKey = ark_groth16::VerifyingKey; pub type Groth16Proof = ark_groth16::Proof; pub type PublicInputs = Vec; +#[derive(Clone)] +pub struct ValidatedOperatorProof { + pub proof: Groth16Proof, + pub public_inputs: PublicInputs, + pub verifying_key: VerifyingKey, + pub public_values: Vec, + pub vk_hash: String, + pub zkm_version: String, +} + #[derive(Clone)] pub struct ValidatedOperatorWrapperProof { pub proof: Groth16Proof, @@ -126,6 +137,14 @@ pub struct ValidatedOperatorWrapperProof { pub zkm_version: String, } +#[derive(Clone)] +pub struct OperatorStatement { + pub static_input: ark_bn254::Fr, + pub vk_hash: [u8; 32], + pub zkm_version: String, + pub constant: [u8; 32], +} + #[derive(Clone)] pub struct OperatorWrapperStatement { pub public_values: Vec, @@ -2010,6 +2029,30 @@ fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) } +fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { + if !operator_vk_hash.starts_with("0x") { + bail!("configured operator vk hash must use 0x-prefixed Ziren encoding"); + } + let raw_vk_hash = decode_zkm_vkey_hash(operator_vk_hash) + .map_err(|e| anyhow!("invalid configured operator vk hash: {e:?}"))?; + let part_vk: PartStarkVerifyingKey = + bincode::deserialize(&load_part_stark_vk_for_zkm_version(zkm_version)?) + .context("deserialize operator partial STARK verifying key")?; + let base = Bn254Fr::from_canonical_u32(256); + let mut field_hash = Bn254Fr::ZERO; + for byte in raw_vk_hash { + field_hash = field_hash * base + Bn254Fr::from_canonical_u32(byte as u32); + } + let combined = zkm_recursion_core::hash_vkey_with_part_vk(&part_vk, field_hash); + let bytes = combined.as_canonical_biguint().to_bytes_be(); + if bytes.len() > 32 { + bail!("combined operator verifying key hash exceeds BN254 field encoding"); + } + let mut encoded = [0u8; 32]; + encoded[32 - bytes.len()..].copy_from_slice(&bytes); + Ok(encoded) +} + fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { if !wrapper_vk_hash.starts_with("0x") { bail!("configured wrapper vk hash must use 0x-prefixed Ziren encoding"); @@ -2034,6 +2077,15 @@ fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result< Ok(encoded) } +pub fn derive_operator_statement(graph_id: Uuid) -> Result { + let vk_hash = get_operator_vk_hash()?; + let zkm_version = get_operator_zkm_version()?; + let combined_hash = combined_operator_vk_hash(&format!("0x{}", hex::encode(vk_hash)), &zkm_version)?; + let static_input = load_ark_public_inputs_from_bytes(&combined_hash, &[0u8; 32])[0]; + let constant = hash_operator_constant(*graph_id.as_bytes(), get_genesis_sequencer_commit_id()); + Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) +} + pub fn derive_operator_wrapper_statement(graph_id: Uuid) -> Result { let public_values = wrapper_public_values( get_operator_vk_hash()?, @@ -2188,6 +2240,129 @@ pub async fn get_operator_wrapper_proof( 0, )) } + +/// Returns: +/// - `Ok(Some(OperatorProof), _)` if operator proof is available and valid +/// - `Ok(None, wait_secs)` if operator proof is not yet available +pub async fn get_operator_proof( + local_db: &LocalDB, + http_client: &HttpAsyncClient, + bitvm_graph: &BitvmGcGraph, + btc_client: &BTCClient, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<(Option, usize)> { + let mut storage_processor = local_db.acquire().await?; + let Some(graph) = storage_processor.find_graph(&graph_id).await? else { + warn!("graph:{graph_id} not found"); + bail!("No graph in db"); + }; + drop(storage_processor); + + if graph.proceed_withdraw_height <= 0 { + warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); + return Ok((None, get_operator_proof_wait_secs())); + } + + let watchtower_challenge_init_txid = graph + .watchtower_challenge_init_txid + .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; + let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); + let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( + btc_client, + &watchtower_challenge_init_txid, + num_challenger, + ) + .await + { + Ok(info) => info, + Err(e) => { + warn!("Failed to get watchtower challenge info: {e}"); + return Ok((None, get_operator_proof_wait_secs())); + } + }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); + + let base_url = Url::parse( + &get_proof_build_rpc_host() + .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, + )?; + let operator_url = base_url.join(NODES_OPERATOR_BASE)?; + let operator_response = http_client + .post_response_json::( + operator_url.as_str(), + &OperatorProofRequest { + instance_id: instance_id.to_string(), + graph_id: graph_id.to_string(), + operator_committed_blockhash, + execution_layer_block_number: graph.proceed_withdraw_height, + watchtower_challenge_txids, + included_watchtowers, + watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), + watchtower_challenge_pubkeys: bitvm_graph + .parameters + .watchtower_pubkeys + .iter() + .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) + .collect(), + }, + ) + .await?; + + let Some(proof_data) = operator_response.proof_data else { + if let Some(error) = operator_response.error { + info!("operator proof is not ready for graph_id:{graph_id}: {error}"); + } + return Ok((None, get_operator_proof_wait_secs())); + }; + + let statement = derive_operator_statement(graph_id)?; + + let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) + .map_err(|err| anyhow!("failed to deserialize operator proof: {err}"))?; + + let operator_vk_hash_raw = decode_zkm_vkey_hash(&proof_data.vk) + .map_err(|e| anyhow!("invalid operator proof vk hash: {e:?}"))?; + if operator_vk_hash_raw != statement.vk_hash { + bail!("operator proof vk hash does not match configured operator identity"); + } + if proof.zkm_version != statement.zkm_version || proof_data.zkm_version != statement.zkm_version { + bail!("operator proof Ziren version does not match configured operator identity"); + } + + let outputs = decode_operator_public_outputs(&proof.public_values.to_vec(), statement.vk_hash) + .map_err(|e| anyhow!("invalid operator public outputs: {e}"))?; + if outputs.constant != statement.constant { + bail!("operator proof constant does not match graph setup"); + } + + let part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; + let ark_proof = + convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) + .map_err(|e| anyhow!("failed to convert operator proof to ark format: {e}"))?; + if ark_proof.public_inputs[0] != statement.static_input { + bail!("operator proof static public input does not match graph setup statement"); + } + + Ok(( + Some(ValidatedOperatorProof { + proof: ark_proof.proof, + public_inputs: ark_proof.public_inputs.into(), + verifying_key: ark_proof.groth16_vk.into(), + public_values: proof.public_values.to_vec(), + vk_hash: proof_data.vk, + zkm_version: proof.zkm_version, + }), + 0, + )) +} + pub async fn verifier_force_skip_kickoff(client: &BTCClient, graph: &BitvmGcGraph) -> Result { let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_master_keypair = verifier_master_key.master_keypair(); From 61a3463bd00002e88c69ec6ce9c4457bf5b1455c Mon Sep 17 00:00:00 2001 From: vanhger Date: Fri, 19 Jun 2026 11:29:21 +0700 Subject: [PATCH 4/9] adjust code after rebase --- crates/bitvm-gc/src/babe_adapter.rs | 492 ++---------------- node/src/handle.rs | 123 ++--- .../graph_maintenance_tasks.rs | 263 ---------- 3 files changed, 92 insertions(+), 786 deletions(-) diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 0c43a75d..8a18e805 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -1,13 +1,12 @@ -use rayon::prelude::*; use std::collections::HashSet; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; use anyhow::{Result, bail}; -use ark_bn254::{Bn254, Fq, Fr}; +use ark_bn254::{Bn254, Fr}; use ark_bn254::g1::G1Affine; use ark_groth16::VerifyingKey as Groth16VerifyingKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::CanonicalSerialize; use garbled_snark_verifier::bag::S; use goat::assert_scripts::{ INPUT_WIRE_NUM, OperatorAssertPublicKey, OperatorAssertSecretKey, WireHash, label_hash, @@ -17,19 +16,17 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; -use verifiable_circuit_babe::babe::{ProverSetupState, WeKnownPi1SetupCt, babe_verifier_presign, GC_INPUT_WIRES, interleave_dummy_positions, build_challenge_assert_witness_raw, deinterleave_dummy_positions}; +use verifiable_circuit_babe::babe::{WeKnownPi1SetupCt, GC_INPUT_WIRES, build_challenge_assert_witness_raw, deinterleave_dummy_positions, interleave_dummy_positions}; pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; use verifiable_circuit_babe::cac::cac_finalize_indices; -use verifiable_circuit_babe::dre::N; use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; -use verifiable_circuit_babe::instance::CACInstance; -use verifiable_circuit_babe::instance::commit::{CACInstanceCommit as RealCACInstanceCommit, CACInstanceCommit}; +use verifiable_circuit_babe::instance::commit::CACInstanceCommit; use verifiable_circuit_babe::prover::BABEProver; use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; pub use verifiable_circuit_babe::transactions::TxAssertWitness; use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; -use verifiable_circuit_babe::verifier::{BABEVerifier, InstanceLightSecrets}; +use verifiable_circuit_babe::verifier::BABEVerifier; use crate::types::BitvmGcCircuitData; /// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. @@ -42,52 +39,6 @@ pub type OpenedInstanceSeeds = Vec<(usize, u64)>; pub type FinalizedInstances = Vec; pub type SetupAndSolderingData = (OpenedInstanceSeeds, FinalizedInstances, SolderingData); -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct CACSetupPackage { -// pub commits: Vec, -// } -// -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct CACInstanceCommit { -// pub epk: Vec<[[u8; 20]; 2]>, -// pub wots_padding_epk: [[[u8; 20]; 2]; 4], -// pub constant_commits: [[[u8; 32]; 2]; 2], -// pub h_msg: [u8; 20], -// pub h_ct_setup: [u8; 32], -// pub com_adaptor: [u8; 32], -// pub com_gc: [u8; 32], -// } - -// #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct FinalizedInstanceData { -// pub index: usize, -// pub final_msg_hash: [u8; 20], -// // todo: we maybe dont need this, it's in the CACInstanceCommit::epk -// pub wire_hashes: Vec, -// // todo: we already has this in the package before. -// pub gc_commitment: [u8; 32], -// // todo: we already has this in the package before. -// pub adaptor_commitment: [u8; 32], -// // todo: we already has this in the package before. -// pub ct_setup_commitment: [u8; 32], -// pub real_data: Option, -// } -// -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct RealFinalizedPayload { -// pub gc_ciphertexts: Vec>, -// pub adaptor_table: SerializableSparseAdaptorTable, -// pub ct_setup: SerializableSetupCt, -// pub constant_labels: [[u8; 16]; 2], -// pub wots_padding_zero_labels: [[u8; 16]; 4], -// } - -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct CompactFinalizedInstanceData { // Replaced by FinalizedInstanceData -// pub index: usize, -// pub real_data: RealFinalizedPayload, -// } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingData { pub finalized_indices: Vec, @@ -101,29 +52,6 @@ pub struct CompactSolderingProofPayload { pub soldering: CompactSolderingData, } -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct SerializableSetupCt { replaced by WeKnownPi1SetupCt -// pub ct2_r_delta_g2: Vec, -// pub ct3_masked_msg: Vec, -// } - -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct SerializableSparseAdaptorTable { replaced by SparseAdaptorTable -// pub entries: Vec, -// } -// -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct SerializableSparseAdaptorEntry { replaced by SparseAdaptorEntry -// pub x: SerializableSparseAdaptorRow, -// pub y: SerializableSparseAdaptorRow, -// pub z: SerializableSparseAdaptorRow, -// } -// -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct SerializableSparseAdaptorRow { replaced by SparseAdaptorRow -// pub cts: Vec<[u8; 32]>, -// pub offset: Vec, -// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SolderingData { @@ -132,12 +60,6 @@ pub struct SolderingData { pub proof: Vec, } -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -// pub struct SolderedLabelsData { -// pub base_commitment: Vec<([u8; 32], [u8; 32])>, -// pub deltas: Vec>, -// pub commitments: Vec>, -// } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeVerifierPrivateState { @@ -160,14 +82,6 @@ pub struct BabeProverState { pub h_msgs: Vec<[u8; 20]>, } -// #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// pub struct BabeAssertWitness { // replace by TxAssertWitness -// // pub pi1: Vec, -// // #[serde(default)] -// // pub pubin_commitment: [u8; 32], -// pub wots_sig: Vec<[u8; 21]>, -// } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeChallengeAssertWitness { pub verifier_index: usize, @@ -185,7 +99,10 @@ pub struct BabeWronglyChallengedWitness { /// Builds deterministic placeholder setup commitments for tests and wiring. pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { let epk = (0..GC_INPUT_WIRES) - .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) + .map(|wire| { + let w = (wire as u16).to_le_bytes(); + [hash20(&[seed, w[0], w[1], 0]), hash20(&[seed, w[0], w[1], 1])] + }) .collect(); let constant_commit_sgc: Vec<_> = (0..SGC_PART1_CONSTANT_SIZE) @@ -213,7 +130,6 @@ pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { } pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { - let seed = index as u8; let adaptor_tables = [ SparseAdaptorTable { entries: vec![] @@ -239,15 +155,6 @@ pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { } -// impl CompactFinalizedInstanceData { -// pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { -// let real_data = finalized.real_data.clone().ok_or_else(|| { -// anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) -// })?; -// Ok(Self { index: finalized.index, real_data }) -// } -// } - impl SolderingData { /// Builds deterministic placeholder soldering data for the selected finalized indices. pub fn sample(finalized_indices: Vec) -> Self { @@ -344,9 +251,6 @@ pub fn compact_soldering_proof_payload( Ok(CompactSolderingProofPayload { opened: opened.to_vec(), finalized: finalized.to_vec(), - // .iter() - // .map(CompactFinalizedInstanceData::try_from_finalized) - // .collect::>>()?, soldering: CompactSolderingData { finalized_indices: soldering.finalized_indices.clone(), proof: soldering.proof.clone(), @@ -356,7 +260,6 @@ pub fn compact_soldering_proof_payload( /// Reconstructs the full BABE setup data using the locally trusted setup package. pub fn expand_compact_soldering_proof_payload( - package: &CACSetupPackage, payload: CompactSolderingProofPayload, ) -> Result { let finalized = payload.finalized.clone(); @@ -424,33 +327,41 @@ pub fn verify_setup( if data.index >= n_cc { bail!("finalized index {} out of range", data.index); } - // if data.wire_hashes.len() != INPUT_WIRE_NUM { - // bail!("finalized index {} has invalid wire hash count", data.index); - // } + } Ok(()) } /// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. +/// +/// `epk` must have exactly `GC_INPUT_WIRES` (762) entries pub fn extract_gc_circuit_data( verifier_pubkey: bitcoin::PublicKey, epk: &[[[u8; 20]; 2]], h_msgs: &[[u8; 20]], ) -> Result { - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = epk - .iter() - .map(|labels| WireHash { - false_label_hash: labels[0], - true_label_hash: labels[1], - }) - .collect::>() - .try_into() - .map_err(|wire_hashes: Vec| { - anyhow::anyhow!( - "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", - wire_hashes.len() - ) - })?; + if epk.len() != GC_INPUT_WIRES { + bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); + } + let n = GC_INPUT_WIRES / 3; + let to_wire_hash = |pair: &[[u8; 20]; 2]| WireHash { + false_label_hash: pair[0], + true_label_hash: pair[1], + }; + let dummy_hash = label_hash(&vec![0u8; 16]); + let dummy = WireHash { false_label_hash: dummy_hash, true_label_hash: dummy_hash }; + + let pi1_x: Vec = epk[..n].iter().map(to_wire_hash).collect(); + let pi1_y: Vec = epk[n..2 * n].iter().map(to_wire_hash).collect(); + let x_d: Vec = epk[2 * n..].iter().map(to_wire_hash).collect(); + let wire_hashes_vec = interleave_dummy_positions(&pi1_x, &pi1_y, &x_d, dummy); + + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = wire_hashes_vec.try_into().map_err( + |v: Vec| anyhow::anyhow!( + "wire hash count {} does not match expected {INPUT_WIRE_NUM}", + v.len() + ), + )?; Ok(BitvmGcCircuitData { verifier_pubkey, final_msg_hashlocks: h_msgs.to_vec(), @@ -464,35 +375,15 @@ pub fn build_assert_witness( assert_secret_key: &OperatorAssertSecretKey, dynamic_input: ark_bn254::Fr, ) -> Result { + if assert_secret_key.is_empty() { + bail!("operator WOTS secret key must not be empty"); + } let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); let wots_sig = Wots96::sign(assert_secret_key, &msg); Ok(TxAssertWitness { wots_sig: wots_sig.to_vec() }) } -// pub fn build_assert_witness_with_pubin_commitment( -// proof: &ark_groth16::Proof, -// pubin_commitment: &[u8; 32], -// assert_secret_key: &OperatorAssertSecretKey, -// ) -> Result { -// if assert_secret_key.is_empty() { -// bail!("operator WOTS secret key must not be empty"); -// } -// let pi1 = proof.a; -// let mut pi1_bytes = Vec::new(); -// pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); -// let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); -// let wots_sig = Wots96::sign(assert_secret_key, &msg); -// Ok(BabeAssertWitness { -// pi1: pi1_bytes, -// pubin_commitment: *pubin_commitment, -// wots_sig: wots_sig.to_vec(), -// }) -// } - pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { - // let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - // .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; - // Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) let recover = assert_witness.recover_pi1_xd_without_verify(); if recover.is_none() { return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) @@ -596,19 +487,8 @@ pub fn recover_real_wrongly_challenged_witness( vk: Groth16VerifyingKey, dyn_pubin: ark_bn254::Fr, ) -> Result { - let real_state = ProverSetupState { - wots_sk_p: vec![], - finalized: prover_state - .finalized.clone(), - // .iter() - // .map(to_real_finalized) - // .collect::>>()?, - soldering: to_real_soldering(&prover_state.soldering)?, - h_msgs: prover_state.h_msgs.clone(), - presigs_v: babe_verifier_presign(), - }; let mut prover = BABEProver::new(vk, proof.clone(), dyn_pubin); - recover_a_valid_finalized_messages(prover_state, challenge_witness, &mut prover, &real_state) + recover_a_valid_finalized_messages(prover_state, challenge_witness, &mut prover) } /// Builds a wrongly-challenged witness after validating all finalized preimages. @@ -687,154 +567,6 @@ fn validate_finalized_indices( Ok(()) } -// fn from_real_package(package: &RealCACSetupPackage) -> CACSetupPackage { -// CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } -// } -// -// fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { -// CACInstanceCommit { -// epk: commit.epk.clone(), -// wots_padding_epk: padding_wire_hashes(), -// constant_commits: commit.constant_commits, -// h_msg: commit.h_msg, -// h_ct_setup: commit.h_ct_setup, -// com_adaptor: commit.com_adaptor, -// com_gc: commit.com_gc, -// } -// } - -// fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { -// RealCACSetupPackage { -// commits: package -// .commits -// .iter() -// .map(|commit| RealCACInstanceCommit { -// epk: commit.epk.clone(), -// constant_commits: commit.constant_commits, -// h_msg: commit.h_msg, -// h_ct_setup: commit.h_ct_setup, -// com_adaptor: commit.com_adaptor, -// com_gc: commit.com_gc, -// }) -// .collect(), -// } -// } - -// fn from_real_finalized( -// finalized: &RealFinalizedInstanceData, -// package: &CACSetupPackage, -// ) -> Result { -// let real_data = RealFinalizedPayload { -// gc_ciphertexts: finalized -// .gc_ciphertexts -// .iter() -// .map(|ciphertext| ciphertext.map(|label| label.0)) -// .collect(), -// adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), -// ct_setup: SerializableSetupCt { -// ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), -// ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), -// }, -// constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], -// wots_padding_zero_labels: [[0u8; 16]; 4], -// }; -// expand_compact_finalized_instance( -// package, -// CompactFinalizedInstanceData { index: finalized.index, real_data }, -// ) -// } - -// fn expand_compact_finalized_instance( -// package: &CACSetupPackage, -// finalized: FinalizedInstanceData, -// ) -> Result { -// let commit = package -// .commits -// .get(finalized.index) -// .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; -// if commit.epk.len() != LAMPORT_N { -// bail!( -// "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", -// finalized.index, -// commit.epk.len() -// ); -// } -// let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); -// wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); -// wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); -// wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); -// wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); -// wire_hashes.extend(pubin_wire_hashes(commit)); -// Ok(FinalizedInstanceData { -// index: finalized.index, -// final_msg_hash: commit.h_msg, -// wire_hashes, -// gc_commitment: commit.com_gc, -// adaptor_commitment: commit.com_adaptor, -// ct_setup_commitment: commit.h_ct_setup, -// real_data: Some(finalized.real_data), -// }) -// } - -// fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { -// let payload = finalized.real_data.as_ref().ok_or_else(|| { -// anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) -// })?; -// Ok(RealFinalizedInstanceData { -// index: finalized.index, -// gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), -// adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, -// ct_setup: RealSetupCt { -// ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), -// ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), -// }, -// constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], -// }) -// } - -// fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { -// SerializableSparseAdaptorTable { -// entries: table -// .entries -// .iter() -// .map(|entry| SerializableSparseAdaptorEntry { -// x: from_real_adaptor_row(&entry.x), -// y: from_real_adaptor_row(&entry.y), -// z: from_real_adaptor_row(&entry.z), -// }) -// .collect(), -// } -// } -// -// fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { -// let mut offset = Vec::new(); -// row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); -// SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } -// } - -// fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { -// Ok(RealSparseAdaptorTable { -// entries: table -// .entries -// .iter() -// .map(|entry| { -// Ok(RealSparseAdaptorEntry { -// x: to_real_adaptor_row(&entry.x)?, -// y: to_real_adaptor_row(&entry.y)?, -// z: to_real_adaptor_row(&entry.z)?, -// }) -// }) -// .collect::>>()?, -// }) -// } - -// fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { -// Ok(RealSparseAdaptorRow { -// cts: row.cts.clone(), -// offset: Fq::deserialize_compressed(row.offset.as_slice())?, -// }) -// } - fn from_real_soldering(soldering: &RealSolderingData) -> Result { let output = soldering.soldering_proof.output().map_err(anyhow::Error::msg)?; Ok(SolderingData { @@ -879,12 +611,11 @@ fn recover_a_valid_finalized_messages( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, prover: &mut BABEProver, - real_state: &ProverSetupState, ) -> Result { - if real_state.finalized.len() != prover_state.h_msgs.len() { + if prover_state.finalized.len() != prover_state.h_msgs.len() { bail!("BABE prover state finalized data and hash count differ"); } - if real_state.finalized.is_empty() { + if prover_state.finalized.is_empty() { bail!("BABE prover state has no finalized instances"); } @@ -892,15 +623,13 @@ fn recover_a_valid_finalized_messages( // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).collect(); - let soldered_output = &prover_state.soldering.soldered_output; let real_soldering = to_real_soldering(&prover_state.soldering)?; let soldered_output = &prover_state.soldering.soldered_output; - if soldered_output.base_commitment.len() != base_input_labels.len() { + if soldered_output.base_commitment.len() != GC_INPUT_WIRES { bail!( - "soldering base commitment count {} does not match BABE input label count {}", - soldered_output.base_commitment.len(), - base_input_labels.len() + "soldering base commitment count {} does not match expected GC input wire count {GC_INPUT_WIRES}", + soldered_output.base_commitment.len() ); } @@ -919,88 +648,10 @@ fn recover_a_valid_finalized_messages( Ok(BabeWronglyChallengedWitness { verifier_index: challenge_witness.verifier_index, - final_msg: prover.valid_msg.unwrap().to_vec(), + final_msg: prover.valid_msg.ok_or_else(|| anyhow::anyhow!("check_compute_msg returned true but valid_msg is not set"))?.to_vec(), }) } -// fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { -// if input_labels.len() == LAMPORT_N { -// return Ok(input_labels.iter().copied().map(S).collect()); -// } -// if input_labels.len() != INPUT_WIRE_NUM { -// bail!( -// "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", -// input_labels.len() -// ); -// } -// Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) -// } - -// fn soldered_input_labels( -// base_input_labels: &[S], -// soldered_output: &SolderedLabelsData, -// finalized_position: usize, -// ) -> Result> { -// if finalized_position == 0 { -// return Ok(base_input_labels.to_vec()); -// } -// let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { -// anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") -// })?; -// if deltas.len() != base_input_labels.len() { -// bail!( -// "soldering delta count {} does not match BABE input label count {}", -// deltas.len(), -// base_input_labels.len() -// ); -// } -// -// Ok(base_input_labels -// .iter() -// .enumerate() -// .map(|(wire, &base_label)| { -// let (delta_false, delta_true) = deltas[wire]; -// if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { -// base_label ^ S(delta_false) -// } else { -// base_label ^ S(delta_true) -// } -// }) -// .collect()) -// } - -// fn recover_finalized_message( -// prover: &BABEProver, -// proof: &ark_groth16::Proof, -// finalized: &RealFinalizedInstanceData, -// input_labels: &[S], -// expected_hash: [u8; 20], -// finalized_position: usize, -// ) -> Result> { -// let mut full_labels = Vec::with_capacity(2 + input_labels.len()); -// full_labels.push(finalized.constant_labels[0]); -// full_labels.push(finalized.constant_labels[1]); -// full_labels.extend_from_slice(input_labels); -// -// let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); -// let ct_prove = prover.compute_ct_prove( -// &mut circuit, -// &gc_output_indices, -// &full_labels, -// &finalized.gc_ciphertexts, -// &finalized.adaptor_table, -// ); -// let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) -// .map_err(anyhow::Error::msg)?; -// let final_msg = msg.to_vec(); -// if label_hash(&final_msg) != expected_hash { -// bail!( -// "recovered message at finalized position {finalized_position} does not match hashlock" -// ); -// } -// Ok(final_msg) -// } - fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { wots_sig.try_into().map_err(|_| { anyhow::anyhow!( @@ -1010,65 +661,10 @@ fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature }) } -// fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { -// let false_hash = label_hash(&vec![0u8; 16]); -// let true_hash = label_hash(&vec![1u8; 16]); -// [[false_hash, true_hash]; 4] -// } - fn deterministic_seed(index: usize) -> u64 { u64::from_le_bytes(hash32(&(index as u64).to_le_bytes())[0..8].try_into().expect("8 bytes")) } -// fn to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { -// WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } -// } - -// fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { -// let mut msg = [0u8; 96]; -// let mut tmp = Vec::new(); -// -// pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); -// msg[..32].copy_from_slice(&tmp); -// -// tmp.clear(); -// pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); -// msg[32..64].copy_from_slice(&tmp); -// -// msg[64..96].copy_from_slice(pubin_commitment); -// msg -// } - -// fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { -// (0..256) -// .map(|index| WireHash { -// false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), -// true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), -// }) -// .collect() -// } -// -// fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { -// (0..256) -// .map(|index| { -// let byte = pubin_commitment[index / 8]; -// let bit = ((byte >> (index % 8)) & 1) == 1; -// pubin_label(commit, index, bit) -// }) -// .collect() -// } - -// fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { -// let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); -// bytes.extend_from_slice(&commit.h_ct_setup); -// bytes.extend_from_slice(&commit.com_adaptor); -// bytes.extend_from_slice(&commit.com_gc); -// bytes.extend_from_slice(&commit.h_msg); -// bytes.extend_from_slice(&(index as u64).to_le_bytes()); -// bytes.push(u8::from(bit)); -// hash16(&bytes) -// } - fn hash20(data: &[u8]) -> [u8; 20] { let hash = hash32(data); hash[0..20].try_into().expect("20 bytes") diff --git a/node/src/handle.rs b/node/src/handle.rs index 2450f563..21736139 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -13,7 +13,6 @@ use crate::utils::*; use anyhow::{Context, Result, anyhow, bail}; use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; -use bitcoin::hashes::{Hash, sha256}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage}; @@ -1654,7 +1653,7 @@ async fn handle_compact_soldering_proof_operator( } let setup_package = candidate.setup_package.clone(); let (opened, finalized, soldering) = - expand_compact_soldering_proof_payload(&setup_package, payload) + expand_compact_soldering_proof_payload(payload) .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; @@ -3560,17 +3559,52 @@ async fn handle_operator_commit_pubin_ready_operator( content: &GOATMessageContent, ) -> Result<()> { let message = make_message(ctx, content); - let Some((_graph, graph_status, _graph_sub_status)) = + let Some((graph, _graph_status, _graph_sub_status)) = refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge) .await? else { return Ok(()); }; - // TODO(gc-v2): build operator-commit-pubin after pubin commitment data is wired. - // It is not pre-signed, so add a funding input for fees and broadcast it normally. - tracing::warn!( - "OperatorCommitPubinReady for {instance_id}:{graph_id} is not implemented yet; graph status is {graph_status:?}" + + // compute the 96-byte guest pubin + let watchtower_challenge_init_txid = + SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); + let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); + let (challenge_txids, included_watchtowers_bits) = + get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) + .await?; + let (btc_best_block_hash, included_watchtowers_bitmap) = + compute_operator_pubin_blockhash_and_bitmap( + ctx.btc_client, + &challenge_txids, + &included_watchtowers_bits, + ) + .await?; + let guest_pubin = build_operator_guest_pubin( + &btc_best_block_hash, + &graph.parameters.pubin_disprove_constant, + &included_watchtowers_bitmap, ); + + // sign and broadcast + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let commit_pubin_wots_sk = operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id).0; + let signed_input = operator_sign_commit_pubin(&graph, &commit_pubin_wots_sk, &guest_pubin)?; + let connector_e_amount = graph + .watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow!("failed to get connector-e input: {e}"))? + .amount; + let operator_keypair = operator_master_key.master_keypair(); + build_sign_and_broadcast_tx( + ctx.btc_client, + operator_keypair, + vec![signed_input], + connector_e_amount, + vec![], + ) + .await?; + Ok(()) } @@ -3634,65 +3668,6 @@ async fn handle_operator_commit_pubin_timeout_verifier( Ok(()) } -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_commit_pubin_ready_operator( - ctx: &mut HandlerContext<'_>, - instance_id: Uuid, - graph_id: Uuid, -) -> Result<()> { - let message = GOATMessage::new( - Actor::Operator, - GOATMessageContent::CommitPubinReady(CommitPubinReady { instance_id, graph_id }), - ); - - let (graph, _graph_status, _graph_sub_status) = - match refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge).await? - { - Some(v) => v, - None => return Ok(()), - }; - - // compute the 96-byte guest pubin - let watchtower_challenge_init_txid = - SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); - let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); - let (challenge_txids, included_watchtowers_bits) = - get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) - .await?; - let (btc_best_block_hash, included_watchtowers_bitmap) = - compute_operator_pubin_blockhash_and_bitmap( - ctx.btc_client, - &challenge_txids, - &included_watchtowers_bits, - ) - .await?; - let guest_pubin = build_operator_guest_pubin( - &btc_best_block_hash, - &graph.parameters.pubin_disprove_constant, - &included_watchtowers_bitmap, - ); - - // sign and broadcast - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let commit_pubin_wots_sk = operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id).0; - let signed_input = operator_sign_commit_pubin(&graph, &commit_pubin_wots_sk, &guest_pubin)?; - let connector_e_amount = graph - .watchtower_challenge_init - .connector_e_input() - .map_err(|e| anyhow!("failed to get connector-e input: {e}"))? - .amount; - let operator_keypair = operator_master_key.master_keypair(); - build_sign_and_broadcast_tx( - ctx.btc_client, - operator_keypair, - vec![signed_input], - connector_e_amount, - vec![], - ) - .await?; - - Ok(()) -} // after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] @@ -3734,6 +3709,13 @@ async fn handle_assert_ready_operator( }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; + + if operator_proof.public_inputs.len() != 2 { + bail!( + "operator proof has {} public inputs; expected 2", + operator_proof.public_inputs.len() + ); + } let dynamic_input = operator_proof.public_inputs[1]; let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; @@ -3979,15 +3961,6 @@ async fn handle_challenge_assert_sent_operator( &wrongly_challenged_witness.final_msg, )?; - // let wrongly_challenged_witness = - // recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; - // // TODO: The wrongly-challenged script accepts any one finalized-message preimage. - // let final_msg = wrongly_challenged_witness - // .final_msgs - // .first() - // .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; - // let (wrongly_challenged_input, _amount) = - // operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; let wrongly_challenged_tx = bitcoin::Transaction { version: bitcoin::transaction::Version(2), lock_time: bitcoin::absolute::LockTime::ZERO, diff --git a/node/src/scheduled_tasks/graph_maintenance_tasks.rs b/node/src/scheduled_tasks/graph_maintenance_tasks.rs index 739701dd..dfed5b02 100644 --- a/node/src/scheduled_tasks/graph_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/graph_maintenance_tasks.rs @@ -371,25 +371,6 @@ pub async fn process_graph_challenge( ); upsert_detected_messages(local_db, graph.graph_id, assert_sent_messages).await?; } - // Todo: vanhger - if let Some((actor, message_content)) = - detect_operator_commit_pubin(btc_client, local_db, &graph).await? - { - info!("process_graph_challenge detect operator commit pubin confirmed on-chain"); - let mut storage_processor = local_db.acquire().await?; - upsert_message( - &mut storage_processor, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), - actor, - message_content, - 0, - 0, - ) - .await?; - } if let Some((actor, message_content, sub_type)) = detect_assert_disprove_ready(btc_client, local_db, &graph, current_height).await? @@ -707,64 +688,6 @@ async fn detect_assert_sent_flow( Ok(messages) } -/// may trigger: AssertReady -async fn detect_operator_commit_pubin( - btc_client: &BTCClient, - local_db: &LocalDB, - graph: &Graph, -) -> anyhow::Result> { - let watchtower_challenge_init_txid: Txid = match graph.watchtower_challenge_init_txid.clone() { - Some(txid) => txid.into(), - None => { - warn!( - "detect_operator_commit_pubin graph_id:{} watchtower_challenge_init_txid is none", - graph.graph_id - ); - return Ok(None); - } - }; - - // vout_len is stored in graph_btc_tx_vout_monitor by detect_watchtower_challenge - let vout_len = { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .find_graph_btc_tx_vout_monitor( - &graph.graph_id, - &SerializableTxid::from(watchtower_challenge_init_txid), - ) - .await? - .map(|m| m.vout_len) - .unwrap_or(0) - }; - if vout_len < 3 { - return Ok(None); - } - - // ConnectorE is the 3rd-to-last output - // (layout: ..., connector_e, connector_f, anchor) - let connector_e_vout = (vout_len - 3) as u64; - let Some(spend_txid) = - outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, connector_e_vout).await? - else { - return Ok(None); - }; - - // if ConnectorE is spent by operator_commit_timeout — skip - let commit_timeout_txid: Option = - graph.operator_commit_timeout_txid.clone().map(Txid::from); - if commit_timeout_txid == Some(spend_txid) { - return Ok(None); - } - - Ok(Some(( - Actor::Operator, - GOATMessageContent::AssertReady(AssertReady { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), - ))) -} - // may trigger disprove ready async fn detect_assert_disprove_ready( btc_client: &BTCClient, @@ -1268,189 +1191,3 @@ async fn check_pre_kickoff_sent( } Ok(pre_sents) } - -#[cfg(test)] -mod detect_commit_pubin_tests { - use bitcoin::hashes::Hash; - use super::*; - use client::btc_chain::BTCClient; - use esplora_client::OutputStatus; - use store::create_local_db; - - fn make_txid(byte: u8) -> Txid { - Txid::from_slice(&[byte; 32]).unwrap() - } - - fn make_dummy_graph( - graph_id: Uuid, - instance_id: Uuid, - watchtower_challenge_init_txid: Option, - operator_commit_timeout_txid: Option, - ) -> Graph { - Graph { - graph_id, - instance_id, - kickoff_index: 0, - from_addr: "".into(), - to_addr: "".into(), - amount: 0, - challenge_amount: 0, - status: "Challenge".into(), - sub_status: "".into(), - operator_pubkey: "".into(), - next_prekickoff: None, - cur_prekickoff_txid: None, - force_skip_kickoff_txid: None, - quick_challenge_txid: None, - challenge_incomplete_kickoff_txid: None, - pegin_txid: None, - kickoff_txid: None, - take1_txid: None, - challenge_txid: None, - take2_txid: None, - watchtower_challenge_init_txid, - operator_assert_txid: None, - verifier_assert_txids: vec![], - disprove_txids: vec![], - watchtower_challenge_timeout_txids: vec![], - operator_challenge_nack_txids: vec![], - operator_commit_timeout_txid, - init_withdraw_tx_hash: None, - bridge_out_start_at: 0, - status_updated_at: 0, - proceed_withdraw_height: 0, - created_at: 0, - updated_at: 0, - } - } - - async fn insert_vout_for_watchtower(db: &LocalDB, graph_id: &Uuid, txid: Txid, vout_len: i64) { - let mut s = db.acquire().await.unwrap(); - s.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id: *graph_id, - tx_name: "watchtower_init".into(), - txid: SerializableTxid::from(txid), - height: 100, - vout_len, - monitor_data: "".into(), - created_at: 0, - updated_at: 0, - }) - .await - .unwrap(); - } - - #[tokio::test] - async fn returns_none_when_init_txid_missing() { - let db = create_local_db("sqlite::memory:").await; - let (btc_client, _) = BTCClient::new_mock_client(); - - let graph = make_dummy_graph(Uuid::new_v4(), Uuid::new_v4(), None, None); - let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn returns_none_when_vout_monitor_missing_from_db() { - let db = create_local_db("sqlite::memory:").await; - let (btc_client, _) = BTCClient::new_mock_client(); - - let init_txid = make_txid(0x01); - let graph = make_dummy_graph( - Uuid::new_v4(), - Uuid::new_v4(), - Some(SerializableTxid::from(init_txid)), - None, - ); - // vout_monitor not inserted → vout_len defaults to 0 → returns None - let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn returns_none_when_connector_e_not_spent() { - let db = create_local_db("sqlite::memory:").await; - let (btc_client, _) = BTCClient::new_mock_client(); - - let graph_id = Uuid::new_v4(); - let init_txid = make_txid(0x01); - insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; - - let graph = make_dummy_graph( - graph_id, - Uuid::new_v4(), - Some(SerializableTxid::from(init_txid)), - None, - ); - let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn returns_none_when_connector_e_spent_by_commit_timeout() { - let db = create_local_db("sqlite::memory:").await; - let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); - - let graph_id = Uuid::new_v4(); - let init_txid = make_txid(0x01); - let timeout_txid = make_txid(0x02); - let connector_e_vout = 2u64; - insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; - mock_adaptor.set_output_status( - init_txid, - connector_e_vout, - OutputStatus { spent: true, txid: Some(timeout_txid), vin: Some(0), status: None }, - ); - - let graph = make_dummy_graph( - graph_id, - Uuid::new_v4(), - Some(SerializableTxid::from(init_txid)), - Some(SerializableTxid::from(timeout_txid)), - ); - let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); - assert!(result.is_none()); - } - - #[tokio::test] - async fn returns_assert_ready_when_connector_e_spent_by_commit_pubin() { - let db = create_local_db("sqlite::memory:").await; - let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); - - let graph_id = Uuid::new_v4(); - let instance_id = Uuid::new_v4(); - let init_txid = make_txid(0x01); - let commit_pubin_txid = make_txid(0x03); - let timeout_txid = make_txid(0x02); - let connector_e_vout = 2u64; - insert_vout_for_watchtower(&db, &graph_id, init_txid, 5).await; - mock_adaptor.set_output_status( - init_txid, - connector_e_vout, - OutputStatus { - spent: true, - txid: Some(commit_pubin_txid), // spend by commit txn - vin: Some(0), - status: None, - }, - ); - - let graph = make_dummy_graph( - graph_id, - instance_id, - Some(SerializableTxid::from(init_txid)), - Some(SerializableTxid::from(timeout_txid)), - ); - let result = detect_operator_commit_pubin(&btc_client, &db, &graph).await.unwrap(); - - let (actor, content) = result.expect("should detect commit-pubin"); - assert_eq!(actor, Actor::Operator); - match content { - GOATMessageContent::AssertReady(msg) => { - assert_eq!(msg.instance_id, instance_id); - assert_eq!(msg.graph_id, graph_id); - } - _ => panic!("expected AssertReady"), - } - } -} From 489bd4b5ac0bd83b5398042d5adcd9a22a98765e Mon Sep 17 00:00:00 2001 From: vanhger Date: Mon, 22 Jun 2026 11:12:32 +0700 Subject: [PATCH 5/9] add tests for babe adapter --- crates/bitvm-gc/src/babe_adapter.rs | 4 +- crates/bitvm-gc/tests/babe_adapter.rs | 237 ++++++++++++++++++-------- 2 files changed, 164 insertions(+), 77 deletions(-) diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 8a18e805..0f34a868 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -20,11 +20,11 @@ use verifiable_circuit_babe::babe::{WeKnownPi1SetupCt, GC_INPUT_WIRES, build_cha pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; use verifiable_circuit_babe::cac::cac_finalize_indices; use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; -use verifiable_circuit_babe::instance::commit::CACInstanceCommit; +pub use verifiable_circuit_babe::instance::commit::CACInstanceCommit; use verifiable_circuit_babe::prover::BABEProver; use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; pub use verifiable_circuit_babe::transactions::TxAssertWitness; -use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; +pub use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; use verifiable_circuit_babe::verifier::BABEVerifier; use crate::types::BitvmGcCircuitData; diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index 19af1c2a..c8bf0798 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -4,14 +4,12 @@ use ark_groth16::Groth16; use bitvm_gc::assert_scripts::{INPUT_WIRE_NUM, label_hash}; use bitvm_gc::babe_adapter::{ BABE_M_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, BabeVerifierState, - CACInstanceCommit, CACSetupPackage, FinalizedInstanceData, SolderingData, WOTS_SIG_COUNT, - build_assert_witness, build_challenge_assert_witness, build_real_setup_package, - build_setup_package, build_wrongly_challenged_witness, - build_wrongly_challenged_witness_from_preimages, derive_finalized_indices, - extract_gc_circuit_data, open_and_solder, open_real_setup_and_solder, verify_real_setup, - verify_setup, + CACSetupPackage, ChallengeAssertWitnessRaw, FinalizedInstanceData, SolderingData, WOTS_SIG_COUNT, + build_assert_witness, build_challenge_assert_witness, build_real_setup_package, build_setup_package, + build_wrongly_challenged_witness, build_wrongly_challenged_witness_from_preimages, derive_finalized_indices, + extract_gc_circuit_data, open_and_solder, open_real_setup_and_solder, verify_real_setup, verify_setup, + sample_cac_instance_commit, sample_finalized_instance_data }; -use bitvm_gc::operator::generate_wots_key; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; use std::collections::HashSet; @@ -26,7 +24,7 @@ fn verifier_pubkey() -> bitcoin::PublicKey { } #[test] -#[ignore = "requires GC_GATES_PATH and GC_INDICES_PATH runtime assets"] +#[ignore = "requires FGC_GATES_PATH, FGC_OUT_INDICES_PATH, SGC_GATES_PATH, SGC_OUT_INDICES_PATH, FGC_COMPACT_GATES_PATH, FGC_COMPACT_OUT_INDICES_PATH, SGC_COMPACT_GATES_PATH, SGC_COMPACT_OUT_INDICES_PATH runtime assets"] fn real_setup_restores_private_state_and_verifies_soldering_proof() { let mut rng = ChaCha12Rng::seed_from_u64(42); let a = Fr::from(3_u64); @@ -36,10 +34,11 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &mut rng, ) .expect("groth16 setup"); - let public_inputs = vec![a * b]; + let static_public_inputs = a * b; + let _dynamic_public_inputs = a * a; let (package, private_state) = - build_real_setup_package(BABE_M_CC, &vk, &public_inputs).expect("real setup"); + build_real_setup_package(BABE_M_CC, &vk, static_public_inputs).expect("real setup"); let restored = serde_json::from_slice(&serde_json::to_vec(&private_state).expect("serialize state")) .expect("deserialize state"); @@ -51,7 +50,7 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &package, &finalized_indices, &vk, - &public_inputs, + static_public_inputs, ) .expect("open real setup"); @@ -64,17 +63,19 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &finalized, &soldering, &vk, - &public_inputs, + static_public_inputs, ) .expect("verify soldering proof"); - extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("extract native 508-wire graph data"); } #[test] fn babe_setup_payload_round_trips_and_derives_gc_data() { let package = CACSetupPackage { - commits: (0..BABE_M_CC).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + commits: (0..BABE_M_CC).map(|index| sample_cac_instance_commit(index as u8)).collect(), }; let encoded = serde_json::to_vec(&package).expect("serialize package"); @@ -87,10 +88,11 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { let finalized = finalized_indices .iter() - .map(|index| FinalizedInstanceData::sample(*index)) + .map(|index| sample_finalized_instance_data(*index)) .collect::>(); - let soldering = SolderingData::sample(finalized_indices); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("extract gc data"); assert_eq!(gc_data.verifier_pubkey, verifier_pubkey()); @@ -99,43 +101,45 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { #[test] fn protocol_finalized_instances_contribute_one_base_wire_slot() { - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..BABE_M_CC).collect()); + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let package = CACSetupPackage { + commits: (0..BABE_M_CC).map(|i| sample_cac_instance_commit(i as u8)).collect(), + }; + let finalized: Vec = + (0..BABE_M_CC).map(sample_finalized_instance_data).collect(); + let h_msgs: Vec<[u8; 20]> = + finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let epk = &package.commits[finalized[0].index].epk; - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("one verifier graph slot"); - assert_eq!( - gc_data.final_msg_hashlocks, - finalized.iter().map(|data| data.final_msg_hash).collect::>() - ); - assert!(gc_data.wire_hashes.as_slice() == finalized[0].wire_hashes.as_slice()); + // M finalized instances each contribute one hashlock … + assert_eq!(gc_data.final_msg_hashlocks, h_msgs); + let n = GC_INPUT_WIRES / 3; // 254 real wires per group + // pi1_x: positions 0..n + assert_eq!(gc_data.wire_hashes[0].false_label_hash, epk[0][0]); + assert_eq!(gc_data.wire_hashes[0].true_label_hash, epk[0][1]); + assert_eq!(gc_data.wire_hashes[n - 1].false_label_hash, epk[n - 1][0]); + assert_eq!(gc_data.wire_hashes[n + 2].false_label_hash, epk[n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2)].false_label_hash, epk[2 * n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2) + n - 1].false_label_hash, epk[3 * n - 1][0]); } #[test] -fn gc_slot_rejects_invalid_finalized_counts_and_soldering_order() { - for count in [1, 3, 5, 8] { - let finalized = (0..count).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..count).collect()); - let error = match extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) { - Ok(_) => panic!("invalid finalized count"), - Err(error) => error, - }; - assert!(error.to_string().contains(&format!("exactly {BABE_M_CC} finalized"))); +fn gc_slot_rejects_invalid_epk_length() { + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let h_msgs = vec![[0u8; 20]; BABE_M_CC]; + for bad_len in [0, 1, GC_INPUT_WIRES - 1, GC_INPUT_WIRES + 1, INPUT_WIRE_NUM] { + let bad_epk = vec![[[0u8; 20]; 2]; bad_len]; + match extract_gc_circuit_data(verifier_pubkey(), &bad_epk, &h_msgs) { + Ok(_) => panic!("epk length {bad_len} should be rejected"), + Err(err) => assert!( + err.to_string().contains("epk has"), + "unexpected error for len {bad_len}: {err}" + ), + } } - - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let mut indices = (0..BABE_M_CC).collect::>(); - indices.swap(0, 1); - let error = match extract_gc_circuit_data( - &finalized, - &SolderingData::sample(indices), - verifier_pubkey(), - ) { - Ok(_) => panic!("mismatched soldering order"), - Err(error) => error, - }; - assert!(error.to_string().contains("soldering finalized indices mismatch")); } #[test] @@ -146,8 +150,6 @@ fn finalized_indices_reject_invalid_counts_and_cover_full_cut() { assert!(derive_finalized_indices(&package, 5).is_err()); let first = derive_finalized_indices(&package, 4).expect("derive full cut"); - let second = derive_finalized_indices(&package, 4).expect("derive full cut again"); - assert_eq!(first, second); assert_eq!(first.len(), 4); assert_eq!(first.iter().copied().collect::>().len(), 4); assert_eq!(first.iter().copied().collect::>(), HashSet::from([0, 1, 2, 3])); @@ -184,12 +186,24 @@ fn verify_setup_accepts_valid_opening_and_rejects_invalid_shapes() { #[test] fn witness_builders_validate_inputs_and_indices() { - let (assert_secret_key, _) = generate_wots_key("graph-scoped-key"); - let proof = ark_groth16::Proof::::default(); - let assert_witness = build_assert_witness(&proof, &assert_secret_key).expect("assert witness"); + use bitvm_gc::operator::generate_assert_wots_key; + let (assert_secret_key, _) = generate_assert_wots_key("test-graph-scoped-key"); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::default(), + c: ark_bn254::G1Affine::default(), + }; + let dynamic_input = Fr::from(0u64); + + let assert_witness = build_assert_witness(&proof, &assert_secret_key, dynamic_input) + .expect("assert witness"); assert_eq!(assert_witness.wots_sig.len(), WOTS_SIG_COUNT); - assert!(!assert_witness.pi1.is_empty()); - assert!(build_assert_witness(&proof, &Vec::new()).is_err()); + assert!(assert_witness.recover_pi1_xd_without_verify().is_some()); + assert!(build_assert_witness(&proof, &Vec::new(), dynamic_input).is_err()); let verifier_state = BabeVerifierState { package: build_setup_package(BABE_M_CC).expect("setup package"), @@ -199,61 +213,63 @@ fn witness_builders_validate_inputs_and_indices() { let challenge_witness = build_challenge_assert_witness(&verifier_state, &assert_witness, 12) .expect("challenge witness"); assert_eq!(challenge_witness.verifier_index, 12); - assert_eq!(challenge_witness.input_labels.len(), INPUT_WIRE_NUM); + assert_eq!(challenge_witness.witness.input_labels.len(), INPUT_WIRE_NUM); - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); + let final_msg = b"finalized-preimage-0".to_vec(); + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); let prover_state = BabeProverState { package: build_setup_package(BABE_M_CC).expect("setup package"), - finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), soldering: SolderingData::sample((0..BABE_M_CC).collect()), h_msgs, }; let wrongly_challenged = - build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msgs.clone()) + build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msg.clone()) .expect("wrongly challenged witness"); assert_eq!(wrongly_challenged.verifier_index, 12); - assert_eq!(wrongly_challenged.final_msgs, final_msgs); + assert_eq!(wrongly_challenged.final_msg, final_msg); assert!( build_wrongly_challenged_witness( &prover_state, &challenge_witness, - vec![b"missing-preimage".to_vec(); BABE_M_CC], + b"missing-preimage".to_vec(), ) .is_err() ); } #[test] -fn wrongly_challenged_witness_requires_all_finalized_preimages() { - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); - let challenge_witness = - BabeChallengeAssertWitness { verifier_index: 0, input_labels: vec![], wots_sig: vec![] }; +fn wrongly_challenged_witness_accepts_valid_preimage_and_rejects_invalid() { + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); + let valid_msg = b"finalized-preimage-0".to_vec(); + let challenge_witness = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; let from_preimages = build_wrongly_challenged_witness_from_preimages( &h_msgs, &challenge_witness, - final_msgs.clone(), + valid_msg.clone(), ) .expect("wrongly challenged witness"); assert_eq!(from_preimages.verifier_index, 0); - assert_eq!(from_preimages.final_msgs, final_msgs); + assert_eq!(from_preimages.final_msg, valid_msg); let prover_state = BabeProverState { package: build_setup_package(BABE_M_CC).expect("setup package"), - finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), soldering: SolderingData::sample((0..BABE_M_CC).collect()), h_msgs, }; let delegated = build_wrongly_challenged_witness( &prover_state, &challenge_witness, - from_preimages.final_msgs.clone(), + from_preimages.final_msg.clone(), ) .expect("delegated wrongly challenged witness"); assert_eq!(delegated, from_preimages); @@ -262,8 +278,79 @@ fn wrongly_challenged_witness_requires_all_finalized_preimages() { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, &challenge_witness, - vec![from_preimages.final_msgs[0].clone(); BABE_M_CC], + b"not-a-valid-preimage".to_vec(), ) .is_err() ); } + +#[test] +fn setup_package_and_open_reject_degenerate_inputs() { + assert!(build_setup_package(0).is_err()); + + let pkg = build_setup_package(4).expect("setup package"); + assert!(open_and_solder(&pkg, &[0, 0]).is_err()); // duplicate finalized index + assert!(open_and_solder(&pkg, &[99]).is_err()); // out-of-range finalized index +} + +#[test] +fn witness_builders_reject_wrong_finalized_count() { + use bitvm_gc::babe_adapter::TxAssertWitness; + + // build_challenge_assert_witness rejects finalized_indices.len() != BABE_M_CC + let bad_state = BabeVerifierState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized_indices: vec![0], // 1, not BABE_M_CC + verifier_pubkey: verifier_pubkey(), + }; + let dummy_assert = TxAssertWitness { wots_sig: vec![] }; + assert!(build_challenge_assert_witness(&bad_state, &dummy_assert, 0).is_err()); + + // build_wrongly_challenged_witness_from_preimages rejects h_msgs.len() != BABE_M_CC + let challenge = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; + assert!(build_wrongly_challenged_witness_from_preimages( + &[], + &challenge, + b"msg".to_vec(), + ).is_err()); + let too_many = vec![[0u8; 20]; BABE_M_CC + 1]; + assert!(build_wrongly_challenged_witness_from_preimages( + &too_many, + &challenge, + b"msg".to_vec(), + ).is_err()); +} + +#[test] +fn assert_witness_preserves_pi1_and_dynamic_input() { + use bitvm_gc::babe_adapter::assert_wots_message; + use bitvm_gc::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("round-trip-test"); + let dynamic_input = Fr::from(42u64); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::default(), + c: ark_bn254::G1Affine::default(), + }; + + let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); + + let (recovered_pi1, recovered_xd) = assert_witness + .recover_pi1_xd_without_verify() + .expect("recover pi1 and xd"); + assert_eq!(recovered_pi1, proof.a); + assert_eq!(recovered_xd, dynamic_input); + + // assert_wots_message must be deterministic + let msg = assert_wots_message(&assert_witness).expect("wots message"); + let msg2 = assert_wots_message(&assert_witness).expect("wots message 2"); + assert_eq!(msg, msg2); +} From 9d011eea6756fbc83ae3363e1fa4b0a831f71c83 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:02:03 +0800 Subject: [PATCH 6/9] update validate_pubin_disprove (#405) Co-authored-by: ethan --- crates/bitvm-gc/src/verifier/api.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 5895c0aa..489d1915 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -9,8 +9,9 @@ use goat::{ scripts::{generate_opreturn_script, p2a_output}, transactions::{ assert::{pubin_disprove, validate_pubin}, - base::DUST_AMOUNT, + base::{DUST_AMOUNT, output_topology}, pre_signed::PreSignedTransaction, + watchtower_challenge::extract_operator_preimage_from_ack_txin, }, utils::num_blocks_per_network, }; @@ -179,8 +180,26 @@ pub fn validate_pubin_disprove( graph: &BitvmGcGraph, operator_commit_pubin_txin: &TxIn, operator_assert_txin: &TxIn, - ack_preimages: Vec>, + operator_ack_txins: &[TxIn], ) -> Result> { + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let mut ack_preimages = vec![vec![]; watchtower_num]; + for txin in operator_ack_txins { + let vout = txin.previous_output.vout as usize; + if vout % 2 != 1 { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let watchtower_index = vout / 2; + if watchtower_index >= watchtower_num + || vout != output_topology::watchtower_challenge_init::ack_connector(watchtower_index) + { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let preimage = extract_operator_preimage_from_ack_txin(txin) + .map_err(|e| anyhow::anyhow!("failed to extract preimage from ack txin: {e}"))?; + ack_preimages[watchtower_index] = preimage; + } + let connector_e = graph.connector_e(); let connector_c = graph.connector_c(); let operator_commit_pubin_witness = connector_e From 4850b371ee1385a8baba4c3e2fc23c8f814a4c23 Mon Sep 17 00:00:00 2001 From: vanhger Date: Mon, 22 Jun 2026 15:31:24 +0700 Subject: [PATCH 7/9] update assert sent verifier --- node/src/handle.rs | 172 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 9 deletions(-) diff --git a/node/src/handle.rs b/node/src/handle.rs index 21736139..df992958 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -28,6 +28,7 @@ use goat::connectors::connector_z::ConnectorZ; use goat::transactions::base::output_topology; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::pre_signed_musig2::verify_public_nonce; +use goat::wots::{Wots, Wots96}; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; use std::sync::Arc; @@ -3749,6 +3750,27 @@ async fn handle_assert_ready_operator( Ok(()) } +/// Collects the on-chain ACK TxIns from all watchtowers that submitted a challenge_ack. +/// Only connectors that are already spent are included; unspent ones are skipped. +async fn collect_ack_txins( + btc_client: &BTCClient, + wci_txid: &Txid, + num_watchtowers: usize, +) -> Result> { + let mut ack_txins = Vec::new(); + for i in 0..num_watchtowers { + let ack_vout = output_topology::watchtower_challenge_init::ack_connector(i) as u64; + if let Some(ack_txid) = outpoint_spent_txid(btc_client, wci_txid, ack_vout).await? { + if let Some(ack_tx) = btc_client.get_tx(&ack_txid).await? { + if let Some(txin) = ack_tx.input.first().cloned() { + ack_txins.push(txin); + } + } + } + } + Ok(ack_txins) +} + // verify Operator DynamicPublicInput and Proof; broadcast PubinDisprove or ChallengeAssert as needed. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_sent_verifier( @@ -3758,7 +3780,6 @@ async fn handle_assert_sent_verifier( assert_txid: Txid, assert_witness: &Option, ) -> Result<()> { - // TODO: check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? { @@ -3772,13 +3793,6 @@ async fn handle_assert_sent_verifier( ); return Ok(()); }; - let Some(assert_witness) = assert_witness else { - // TODO(gc-v2): recover BabeAssertWitness from the assert tx witness and graph. - tracing::warn!( - "Ignore AssertSent for {instance_id}:{graph_id}: assert witness is not available in message" - ); - return Ok(()); - }; let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); @@ -3792,6 +3806,86 @@ async fn handle_assert_sent_verifier( }; validate_verifier_slot(&graph, verifier_index)?; + // check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction + if let Ok(connector_e_input) = graph.watchtower_challenge_init.connector_e_input() { + let outpoint = connector_e_input.outpoint; + if let Some(commit_pubin_txid) = + outpoint_spent_txid(ctx.btc_client, &outpoint.txid, outpoint.vout as u64).await? + { + if let Some(commit_pubin_tx) = ctx.btc_client.get_tx(&commit_pubin_txid).await? { + if let Some(commit_pubin_txin) = commit_pubin_tx.input.first() { + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); + let num_watchtowers = graph.parameters.watchtower_ack_hashlocks.len(); + let ack_txins = collect_ack_txins(ctx.btc_client, &wci_txid, num_watchtowers).await?; + match validate_pubin_disprove(&graph, commit_pubin_txin, assert_txin, &ack_txins) { + Ok(Some((witness_data, _))) => { + // build pubin disprove Tx + let pubin_disprove_txin = + build_pubin_disprove_txin(&graph, witness_data)?; + let pubin_disprove_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![pubin_disprove_txin], + output: vec![goat::scripts::p2a_output()], + }; + broadcast_nonstandard_tx(ctx.btc_client, &pubin_disprove_tx).await?; + let message_content = + GOATMessageContent::DisproveSent(DisproveSent { + instance_id, + graph_id, + disprove_type: DisproveTxType::PubinDisprove, + index: verifier_index, + challenge_start_txid: None, + challenge_finish_txid: pubin_disprove_tx.compute_txid(), + }); + send_to_peer( + ctx.swarm, + GOATMessage::new(Actor::Committee, message_content), + ) + .await?; + return Ok(()); + } + Ok(None) => tracing::debug!( + "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" + ), + Err(e) => tracing::warn!( + "PubinDisprove check failed for {instance_id}:{graph_id}: {e}, proceeding to ChallengeAssert" + ), + } + } + } + } + } + + // recover TxAssertWitness from the assert tx witness and graph. + let assert_witness_recovered: TxAssertWitness; + let assert_witness: &TxAssertWitness = match assert_witness { + Some(w) => w, + None => { + let raw_witness = graph + .connector_c() + .extract_leaf_1_raw_witness( + assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, + ) + .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &raw_witness { + bitcoin_witness.push(item); + } + assert_witness_recovered = TxAssertWitness { + wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(), + }; + &assert_witness_recovered + } + }; + let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .and_then(|state| state.verifier) else { @@ -4594,7 +4688,7 @@ async fn handle_response_node_info( #[cfg(test)] mod tests { use std::str::FromStr; - + use bitcoin::hashes::Hash; use bitvm_lib::babe_adapter::{ BABE_M_CC, BabeProverState, build_setup_package, open_and_solder, }; @@ -4715,4 +4809,64 @@ mod tests { assert!(restored.candidates[0].prover_state.is_none()); } + + // ── collect_ack_txins ───────────────────────────────────────────────────── + + fn make_txid(byte: u8) -> Txid { + Txid::from_byte_array([byte; 32]) + } + + fn make_confirmed_ack_tx(ack_txid: Txid, wci_txid: Txid, ack_vout: u32) -> esplora_client::Tx { + use esplora_client::{TxStatus, Vin}; + let preimage = b"test-ack-preimage".to_vec(); + esplora_client::Tx { + txid: ack_txid, + version: 2, + locktime: 0, + vin: vec![Vin { + txid: wci_txid, + vout: ack_vout, + prevout: None, + scriptsig: bitcoin::ScriptBuf::default(), + witness: vec![preimage], + sequence: 1, + is_coinbase: false, + }], + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(800_000), + block_hash: None, + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn collect_ack_txins_returns_only_spent_connectors() { + let (btc_client, mock) = client::btc_chain::BTCClient::new_mock_client(); + let wci_txid = make_txid(0x00); + let ack_txid_0 = make_txid(0x01); + let num_watchtowers = 3; + + // Watchtower 0 ACKed + let vout_0 = output_topology::watchtower_challenge_init::ack_connector(0) as u32; + mock.set_tx(ack_txid_0, make_confirmed_ack_tx(ack_txid_0, wci_txid, vout_0)); + + // Watchtower 1 did NOT ACK (connector unspent — nothing registered) + + // Watchtower 2 ACKed + let ack_txid_2 = make_txid(0x02); + let vout_2 = output_topology::watchtower_challenge_init::ack_connector(2) as u32; + mock.set_tx(ack_txid_2, make_confirmed_ack_tx(ack_txid_2, wci_txid, vout_2)); + + let txins = collect_ack_txins(&btc_client, &wci_txid, num_watchtowers).await.unwrap(); + + assert_eq!(txins.len(), 2); + assert_eq!(txins[0].previous_output.vout, vout_0); + assert_eq!(txins[1].previous_output.vout, vout_2); + } } From e9a137c8e66e754bd7ec700bca878ad9ea4b2b42 Mon Sep 17 00:00:00 2001 From: vanhger Date: Mon, 22 Jun 2026 16:06:06 +0700 Subject: [PATCH 8/9] update handle_challenge_assert_sent_operator --- node/src/handle.rs | 136 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 126 insertions(+), 10 deletions(-) diff --git a/node/src/handle.rs b/node/src/handle.rs index df992958..e67785c8 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -15,7 +15,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage}; +use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage, WOTS_SIG_COUNT}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; @@ -3750,6 +3750,40 @@ async fn handle_assert_ready_operator( Ok(()) } +/// Recovers a `BabeChallengeAssertWitness` from the on-chain challenge assert tx. +fn recover_challenge_assert_witness( + challenge_assert_tx: &bitcoin::Transaction, + verifier_index: usize, +) -> Result { + let txin = challenge_assert_tx + .input + .first() + .ok_or_else(|| anyhow!("challenge assert tx has no input"))?; + let items: Vec> = txin.witness.to_vec(); + let expected_len = 2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM + 2; + if items.len() != expected_len { + bail!( + "unexpected challenge assert witness length: {} (expected {expected_len})", + items.len() + ); + } + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &items[..2 * WOTS_SIG_COUNT] { + bitcoin_witness.push(item); + } + let wots_sig = Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(); + let input_labels: Vec<[u8; 16]> = items + [2 * WOTS_SIG_COUNT..2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM] + .iter() + .map(|item| item.as_slice().try_into()) + .collect::>() + .map_err(|_| anyhow!("challenge assert label item is not 16 bytes"))?; + Ok(BabeChallengeAssertWitness { + verifier_index, + witness: ChallengeAssertWitnessRaw { input_labels, wots_sig }, + }) +} + /// Collects the on-chain ACK TxIns from all watchtowers that submitted a challenge_ack. /// Only connectors that are already spent are included; unspent ones are skipped. async fn collect_ack_txins( @@ -3963,16 +3997,7 @@ async fn handle_challenge_assert_sent_operator( Some(v) => v, None => return Ok(()), }; - let Some(challenge_witness) = challenge_witness else { - // TODO(gc-v2): recover BabeChallengeAssertWitness from the challenge tx witness and graph. - tracing::warn!( - "Ignore ChallengeAssertSent for {instance_id}:{graph_id}: challenge witness is not available in message" - ); - return Ok(()); - }; - validate_verifier_slot(&graph, verifier_index)?; - validate_challenge_witness_index(verifier_index, challenge_witness)?; validate_expected_challenge_assert_txid(&graph, verifier_index, challenge_assert_txid)?; let Some(challenge_assert_tx) = ctx.btc_client.get_tx(&challenge_assert_txid).await? else { @@ -3986,6 +4011,17 @@ async fn handle_challenge_assert_sent_operator( return Ok(()); }; + let challenge_witness_recovered: BabeChallengeAssertWitness; + let challenge_witness: &BabeChallengeAssertWitness = match challenge_witness { + Some(w) => w, + None => { + challenge_witness_recovered = + recover_challenge_assert_witness(&challenge_assert_tx, verifier_index)?; + &challenge_witness_recovered + } + }; + validate_challenge_witness_index(verifier_index, challenge_witness)?; + let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness .witness .input_labels @@ -4869,4 +4905,84 @@ mod tests { assert_eq!(txins[0].previous_output.vout, vout_0); assert_eq!(txins[1].previous_output.vout, vout_2); } + + // ── recover_challenge_assert_witness ────────────────────────────────────── + + fn build_challenge_assert_tx( + sig: &::Signature, + labels: &[[u8; 16]], + ) -> bitcoin::Transaction { + let mut witness = bitcoin::Witness::new(); + let raw_wots = Wots96::signature_to_raw_witness(sig); + for item in raw_wots.iter() { + witness.push(item); + } + for label in labels { + witness.push(label.as_slice()); + } + witness.push(&[0xde, 0xad]); // placeholder script + witness.push(&[0xbe, 0xef]); // placeholder control block + bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { witness, ..Default::default() }], + output: vec![], + } + } + + #[test] + fn recover_challenge_assert_witness_extracts_wots_sig_and_labels() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-challenge-witness"); + let msg = [0x42u8; 96]; + let sig = Wots96::sign(&sk, &msg); + + let labels: Vec<[u8; 16]> = (0..goat::assert_scripts::INPUT_WIRE_NUM) + .map(|i| [(i % 256) as u8; 16]) + .collect(); + + let tx = build_challenge_assert_tx(&sig, &labels); + let verifier_index = 3; + let recovered = recover_challenge_assert_witness(&tx, verifier_index).unwrap(); + + assert_eq!(recovered.verifier_index, verifier_index); + assert_eq!(recovered.witness.wots_sig.len(), WOTS_SIG_COUNT); + assert_eq!(recovered.witness.input_labels.len(), goat::assert_scripts::INPUT_WIRE_NUM); + + let mut bw = bitcoin::Witness::new(); + for item in Wots96::signature_to_raw_witness(&sig).iter() { + bw.push(item); + } + assert_eq!(recovered.witness.wots_sig, Wots96::raw_witness_to_signature(&bw).to_vec()); + + // labels are correctly extracted + for (i, label) in recovered.witness.input_labels.iter().enumerate() { + assert_eq!(label[0], (i % 256) as u8, "label {i} mismatch"); + } + } + + #[test] + fn recover_challenge_assert_witness_rejects_wrong_length() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-wrong-len"); + let sig = Wots96::sign(&sk, &[0u8; 96]); + let labels: Vec<[u8; 16]> = vec![[0u8; 16]; goat::assert_scripts::INPUT_WIRE_NUM]; + + // one label short + let mut short_labels = labels.clone(); + short_labels.pop(); + let bad_tx = build_challenge_assert_tx(&sig, &short_labels); + assert!(recover_challenge_assert_witness(&bad_tx, 0).is_err()); + + // no inputs at all + let empty_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + assert!(recover_challenge_assert_witness(&empty_tx, 0).is_err()); + } } From e86068f241911b7b0429eef6d822f33d990ab29a Mon Sep 17 00:00:00 2001 From: vanhger Date: Mon, 22 Jun 2026 16:58:05 +0700 Subject: [PATCH 9/9] update deps --- Cargo.lock | 5 ++++- Cargo.toml | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6932f89d..66f2c452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5576,6 +5576,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12224,7 +12225,8 @@ dependencies = [ [[package]] name = "soldering-host" -version = "0.0.1" +version = "1.1.0" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -13910,6 +13912,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "aes", "ark-bn254", diff --git a/Cargo.toml b/Cargo.toml index eb146875..340b22a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,13 +107,13 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } #zkm-verifier = { path = "../Ziren/crates/verifier" } bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } -#verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } -#garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } -#soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } -# -verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"} -garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"} -soldering-host = { path = "../bitvm2-gc/babe-programs/soldering/host" } +verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } + +#verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"} +#garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"} +#soldering-host = { path = "../bitvm2-gc/babe-programs/soldering/host" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" }