diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 35e7d65..d224863 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -49,7 +49,7 @@ jobs: run: docker exec bitcoind-regtest bitcoin-cli -regtest --rpcuser=user --rpcpassword=PaSsWoRd -generate 101 - name: Run - run: ./target/release/collidervm_toy + run: ./target/release/demo - name: Stop Bitcoin Regtest working-directory: scripts/demo diff --git a/Cargo.lock b/Cargo.lock index d51ee5c..cec39cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,11 +653,14 @@ dependencies = [ "bitcoincore-rpc", "bitvm", "blake3", + "byteorder", "clap", "hex", "indicatif", "itertools 0.14.0", "musig2", + "num-bigint", + "num-traits", "rand", "rstest", "secp256k1 0.29.1", diff --git a/Cargo.toml b/Cargo.toml index 5f7052b..e394e38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,9 @@ anyhow = "1.0.98" bitcoincore-rpc = "0.19.0" serde_json = "1.0" musig2 = { version = "0.2.4", features = ["serde", "rand", "k256"]} +byteorder = "1.5.0" +num-bigint = "0.4.6" +num-traits = "0.2.19" [profile.dev] opt-level = 3 diff --git a/src/main.rs b/src/bin/demo.rs similarity index 98% rename from src/main.rs rename to src/bin/demo.rs index 59cd2f4..bdeb30f 100644 --- a/src/main.rs +++ b/src/bin/demo.rs @@ -1,3 +1,7 @@ +//! ColliderVM Signet Demo Binary +// ...existing code from main.rs... +// ...existing code... + //! ColliderVM Signet Demo Binary //! //! This binary generates **real Bitcoin Signet transactions** that execute the @@ -38,6 +42,10 @@ use bitcoincore_rpc::{Auth, Client, RpcApi}; use clap::Parser; use collidervm_toy::core::{find_valid_nonce, flow_id_to_prefix_bytes}; use collidervm_toy::musig2::simulate_musig2; +use collidervm_toy::output::{ + DemoOutput, DemoParameters, KeyInfo, KeyPair, TransactionInfo, TxInfo, + write_demo_output_to_file, +}; use collidervm_toy::transactions::{ create_f1_tx, create_f2_tx, create_spending_tx, finalize_f1_tx, finalize_lock_tx, @@ -46,13 +54,8 @@ use collidervm_toy::utils::inner_from; use collidervm_toy::utils::{ wait_for_confirmation, wrap_network, write_transaction_to_file, }; -use std::str::FromStr; -mod output; -use output::{ - DemoOutput, DemoParameters, KeyInfo, KeyPair, TransactionInfo, TxInfo, - write_demo_output_to_file, -}; +use std::str::FromStr; /// Minimal amount we ask the user to deposit (200 000 sat ≈ 0.002 BTC) const REQUIRED_AMOUNT_SAT: u64 = 150_000; diff --git a/src/bin/mine-pow-alephium.rs b/src/bin/mine-pow-alephium.rs new file mode 100644 index 0000000..30180cc --- /dev/null +++ b/src/bin/mine-pow-alephium.rs @@ -0,0 +1,460 @@ +// src/bin/mock_pool.rs +// Rust port of mock_pool.py: Alephium mock pool for gpu-miner connectivity tests +// Usage: cargo run --bin mock_pool -- --host 0.0.0.0 --port 10973 --difficulty 1 + +use bitcoin::ScriptBuf; +use bitcoin::opcodes::OP_TRUE; +use bitcoin::script::Builder; + +use bitcoin_script_stack::optimizer; +use bitvm::hash::blake3::blake3_push_message_script_with_limb; +use bitvm::{ + ExecuteInfo, execute_script_buf, + hash::blake3::blake3_compute_script_with_limb, +}; + +use clap::Parser; + +use collidervm_toy::core::{ + build_drop, build_prefix_equalverify, build_script_hash_to_limbs, + combine_scripts, +}; + +use num_bigint::BigUint; +use num_traits::One; +use rand::RngCore; +use std::io::{self, Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::thread; + +const PROTO_VERSION: u8 = 1; +const MSG_JOBS: u8 = 0; +const MSG_SUBMIT_BLOCK: u8 = 0; +const HEADER_LEN: usize = 208; +const NONCE_LEN: usize = 24; +const BLAKE3_BUF_LEN: usize = 326; + +#[derive(Parser, Debug)] +struct Args { + #[arg(long, default_value = "0.0.0.0")] + host: String, + #[arg(long, default_value_t = 10973)] + port: u16, + #[arg(long, default_value_t = 1)] + difficulty: u64, +} + +fn blob(data: &[u8]) -> Vec { + let mut v = Vec::with_capacity(4 + data.len()); + v.extend(&(data.len() as u32).to_be_bytes()); + v.extend(data); + v +} + +fn build_job( + from_group: u32, + to_group: u32, + header_blob: &[u8], + txs_blob: &[u8], + target_blob: &[u8], + height: u32, +) -> Vec { + let mut v = Vec::new(); + v.extend(&from_group.to_be_bytes()); + v.extend(&to_group.to_be_bytes()); + v.extend(blob(header_blob)); + v.extend(blob(txs_blob)); + v.extend(blob(target_blob)); + v.extend(&height.to_be_bytes()); + v +} + +fn build_jobs_message(jobs: &[Vec]) -> Vec { + let mut body = vec![PROTO_VERSION, MSG_JOBS]; + body.extend(&jobs.len().to_be_bytes()); + for job in jobs { + body.extend(job); + } + let mut msg = Vec::new(); + msg.extend(&body.len().to_be_bytes()); + msg.extend(body); + msg +} + +pub fn target_from_difficulty(diff: u64) -> [u8; 32] { + if diff == 0 { + panic!("difficulty must be ≥ 1"); + } + + // max_target = 2^256 − 1 + let max_target = (BigUint::one() << 256) - BigUint::one(); + + let target: BigUint = max_target / BigUint::from(diff); + + // Convert to big-endian bytes and left-pad to 32 bytes + let raw = target.to_bytes_be(); + + if raw.len() > 32 { + panic!("target doesn’t fit in 32 bytes"); + } + + let mut bytes = [0u8; 32]; + + bytes[32 - raw.len()..].copy_from_slice(&raw); + bytes +} + +fn make_jobs(batch_id: u64, diff: u64) -> Vec> { + let mut jobs = Vec::new(); + let mut header_seed = [0u8; 32]; + let mut hasher = blake3::Hasher::new(); + hasher.update(&batch_id.to_le_bytes()); + header_seed.copy_from_slice(&hasher.finalize().as_bytes()[..32]); + for fg in 0..4 { + for tg in 0..4 { + let mut header = vec![0u8; HEADER_LEN]; + rand::thread_rng().fill_bytes(&mut header); + header[..header_seed.len()].copy_from_slice(&header_seed); + jobs.push(build_job( + fg, + tg, + &header, + &[], + &target_from_difficulty(diff), + 0, + )); + } + } + jobs +} + +fn recv_exact(stream: &mut TcpStream, n: usize) -> io::Result> { + let mut buf = vec![0u8; n]; + let mut read = 0; + while read < n { + let r = stream.read(&mut buf[read..])?; + if r == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "socket closed", + )); + } + read += r; + } + Ok(buf) +} + +fn alephium_block_hash(nonce: &[u8], header: &[u8]) -> [u8; 32] { + if nonce.len() != NONCE_LEN || header.len() != HEADER_LEN { + panic!( + "Invalid nonce or header length: nonce={} bytes, header={} bytes", + nonce.len(), + header.len() + ); + } + let mut full_header = Vec::with_capacity(BLAKE3_BUF_LEN); + full_header.extend(nonce); + full_header.extend(header); + full_header.resize(BLAKE3_BUF_LEN, 0); + + let h1 = blake3::hash(&full_header); + let h2 = blake3::hash(h1.as_bytes()); + + // let h1_int = BigUint::from_bytes_be(h1.as_bytes()); + // let h2_int = BigUint::from_bytes_be(h2.as_bytes()); + // println!(" h1: {:0>64x}", h1_int); + // println!(" h2: {:0>64x}", h2_int); + + *h2.as_bytes() +} + +fn is_ok(nonce: &[u8], header: &[u8], target: &[u8]) -> bool { + let h2 = alephium_block_hash(nonce, header); + + let h2_int = BigUint::from_bytes_be(&h2); + let target_int = BigUint::from_bytes_be(target); + + h2_int < target_int +} + +// For debugging purposes, reconstruct the hash from the stack +#[allow(dead_code)] +fn reconstruct_hash_from_stack(result: &ExecuteInfo) -> BigUint { + // Reconstruct hash from nibbles on stack + let stack: Vec = result + .final_stack + .0 + .iter_str() + .map(|v| { + if v.len() > 4 { + panic!("Stack element too large to fit in u32: {v:?}"); + } + let mut arr = [0u8; 4]; + arr[4 - v.len()..].copy_from_slice(&v); + u32::from_be_bytes(arr) + }) + .collect(); + + let mut hash: Vec = Vec::with_capacity(32); + for i in 0..32 { + let hi = stack + .get(2 * i) + .copied() + .expect("Not enough stack elements for hi nibble"); + let lo = stack + .get(2 * i + 1) + .copied() + .expect("Not enough stack elements for lo nibble"); + if hi > 0xF || lo > 0xF { + panic!("Nibble value out of range: hi={hi}, lo={lo}"); + } + let byte = ((hi as u8) << 4) | (lo as u8); + hash.push(byte); + } + BigUint::from_bytes_be(&hash) +} + +fn build_check_alephium_block_hash( + block: &[u8], + expected_block_hash: &[u8], +) -> ScriptBuf { + let limb_len = 16; + + // let message_limbs = blake3_message_to_limbs(&block, limb_len) + // .iter() + // .map(|&limb| limb as i64) + // .fold(Builder::new(), |b, limb| b.push_int(limb)) + // .into_script(); + + let message_limbs = + blake3_push_message_script_with_limb(block, limb_len).compile(); + + let h1 = optimizer::optimize( + blake3_compute_script_with_limb(BLAKE3_BUF_LEN, limb_len).compile(), + ); + + let hash_to_limbs = build_script_hash_to_limbs(); + + let h2 = + optimizer::optimize(blake3_compute_script_with_limb(32, 4).compile()); + + let expected_nibbles = expected_block_hash + .iter() + .flat_map(|&byte| vec![(byte >> 4) & 0xF, byte & 0xF]) + .collect::>(); + + let to_drop = 64 - expected_nibbles.len(); + + let drop_tail = build_drop(to_drop); + + let prefix_cmp = build_prefix_equalverify(&expected_nibbles); + + let success_script = Builder::new().push_opcode(OP_TRUE).into_script(); + + combine_scripts(&[ + message_limbs, + h1, + hash_to_limbs, + h2, + drop_tail, + prefix_cmp, + success_script, + ]) +} + +pub fn verify_alephium_block_hash_with_script( + nonce: &[u8], + header: &[u8], + block_hash: &[u8], +) -> bool { + assert_eq!(nonce.len(), NONCE_LEN, "nonce must be 24 bytes"); + assert_eq!(header.len(), HEADER_LEN, "header must be 208 bytes"); + assert_eq!(block_hash.len(), 32, "block_hash must be 32 bytes"); + + let mut message = Vec::default(); + message.extend_from_slice(nonce); + message.extend_from_slice(header); + message.resize(BLAKE3_BUF_LEN, 0); + + let script = build_check_alephium_block_hash(&message, block_hash); + + let res = execute_script_buf(script); + + println!("Script executed with success: {}", res.success); + println!("stack: {:?}", res.final_stack); + + res.success +} + +fn decode_submit_block(frame: &[u8]) -> Option<(Vec, Vec, Vec)> { + if frame.len() < 10 { + return None; + } + let total_len = + u32::from_be_bytes(frame[0..4].try_into().unwrap()) as usize; + let _ver = frame[4]; + let kind = frame[5]; + let block_size = + u32::from_be_bytes(frame[6..10].try_into().unwrap()) as usize; + if kind != MSG_SUBMIT_BLOCK + || total_len != frame.len() - 4 + || block_size != frame.len() - 10 + { + return None; + } + let mut pos = 10; + let nonce = frame[pos..pos + NONCE_LEN].to_vec(); + pos += NONCE_LEN; + let header = frame[pos..pos + HEADER_LEN].to_vec(); + pos += HEADER_LEN; + let txs = frame[pos..].to_vec(); + Some((nonce, header, txs)) +} + +fn handle_miner(mut stream: TcpStream, diff: u64) -> io::Result<()> { + println!("[+] Miner connected from {}", stream.peer_addr()?); + let batch_ctr = 0u64; + let target = target_from_difficulty(diff); + + // Send initial job set + let jobs_msg = build_jobs_message(&make_jobs(batch_ctr, diff)); + stream.write_all(&jobs_msg)?; + println!("[>] Pushed 16 templates"); + + loop { + let prefix = match recv_exact(&mut stream, 4) { + Ok(p) => p, + Err(_) => { + println!("[*] Miner disconnected"); + break Ok(()); + } + }; + let pay_len = + u32::from_be_bytes(prefix[..4].try_into().unwrap()) as usize; + let payload = match recv_exact(&mut stream, pay_len) { + Ok(p) => p, + Err(_) => { + println!("[*] Miner disconnected"); + break Ok(()); + } + }; + + let mut frame = prefix.clone(); + frame.extend(payload); + + if let Some((nonce, header, txs)) = decode_submit_block(&frame) { + let pow_ok = is_ok(&nonce, &header, &target); + + let block_hash = alephium_block_hash(&nonce, &header); + + let hash_ok = verify_alephium_block_hash_with_script( + &nonce, + &header, + &block_hash, + ); + println!( + "[{}] nonce {} txs {}", + if pow_ok && hash_ok { "✓" } else { "✗" }, + hex::encode(&nonce), + txs.len(), + ); + return Ok(()); + } else { + println!("[!] Bad frame"); + } + } +} + +fn main() -> io::Result<()> { + let args = Args::parse(); + let addr = format!("{}:{}", args.host, args.port); + let listener = TcpListener::bind(&addr)?; + println!("[+] Listening on {} diff={}", addr, args.difficulty); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + let diff = args.difficulty; + thread::spawn(move || { + if let Err(e) = handle_miner(stream, diff) { + eprintln!("[!] Error handling miner: {e}"); + } + std::process::exit(0); + }); + } + Err(e) => { + eprintln!("Connection failed: {e}"); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn hex_to_array(hex: &str) -> [u8; N] { + let bytes = hex::decode(hex).expect("Invalid hex string"); + assert_eq!(bytes.len(), N, "Length mismatch"); + let mut arr = [0u8; N]; + arr.copy_from_slice(&bytes); + arr + } + + const NONCE_HEX: &str = "bd193b24440aca7894a1edea53fc7bf0cc52c06e7b970562"; + const HEADER_HEX: &str = "71e0a99173564931c0b8acc52d2685a8e39c64dc52e3d02390fdac2a12b155cbfbd480ea895758fa18cb534cd1a8f2617a299636d8b77e186fa09b7e369036edadf5d00d823b299d67b91384bcfe977e1efdaf56575410bb59c9ef4dced5df13304357f20c8f8958499833c9d9385534929515b30aea4e19e8dabd1890d970e206d5088ec1125b2d2c9b15c008bff7e0e23cebe0a26fc2fdfc16d13443465828337e4e43ef84b0aee9a348a3baab8d3002e2de9b7adc6ccf3ac6b6d0d881a214914d16e6fcb595768e273e606379fd0e"; + const TARGET_HEX: &str = + "000000068db8bac710cb295e9e1b089a027525460aa64c2f837b4a2339c0ebed"; + + fn fixtures() -> ([u8; NONCE_LEN], [u8; HEADER_LEN], [u8; 32]) { + ( + hex_to_array::(NONCE_HEX), + hex_to_array::(HEADER_HEX), + hex_to_array::<32>(TARGET_HEX), + ) + } + + #[rstest] + #[case::valid(fixtures())] + fn test_verify_alephium_pow_with_script_valid( + #[case] (nonce, header, target): ( + [u8; NONCE_LEN], + [u8; HEADER_LEN], + [u8; 32], + ), + ) { + assert!(is_ok(&nonce, &header, &target)); + + let block_hash = alephium_block_hash(&nonce, &header); + + let res = verify_alephium_block_hash_with_script( + &nonce, + &header, + &block_hash, + ); + + assert!(res, "Expected success for valid PoW"); + } + + #[rstest] + #[case::invalid(fixtures())] + fn test_verify_alephium_pow_with_script_invalid( + #[case] (nonce, header, _): ( + [u8; NONCE_LEN], + [u8; HEADER_LEN], + [u8; 32], + ), + ) { + let mut block_hash = alephium_block_hash(&nonce, &header); + block_hash[0] ^= 0xFF; + + let res = verify_alephium_block_hash_with_script( + &nonce, + &header, + &block_hash, + ); + assert!(!res, "Expected failure for invalid PoW"); + } +} diff --git a/src/core.rs b/src/core.rs index f27ebde..68e0bf2 100644 --- a/src/core.rs +++ b/src/core.rs @@ -137,7 +137,7 @@ pub fn flow_id_to_prefix_bytes(flow_id: u32, b_bits: usize) -> Vec { } /// Helper: combine scripts (by just concatenating the raw bytes). -fn combine_scripts(fragments: &[ScriptBuf]) -> ScriptBuf { +pub fn combine_scripts(fragments: &[ScriptBuf]) -> ScriptBuf { let mut combined = Vec::new(); for frag in fragments { combined.extend(frag.to_bytes()); @@ -153,7 +153,7 @@ fn combine_scripts(fragments: &[ScriptBuf]) -> ScriptBuf { /// We need to take care of the fact that the prefix is now in nibbles. /// Also the ordering of elements on the stack. /// We need to push the prefix in reverse order to the stack. -fn build_prefix_equalverify(prefix_data: &[u8]) -> ScriptBuf { +pub fn build_prefix_equalverify(prefix_data: &[u8]) -> ScriptBuf { let mut b = Builder::new(); // Check each nibble individually, pushing in reverse order to match stack evaluation @@ -166,6 +166,14 @@ fn build_prefix_equalverify(prefix_data: &[u8]) -> ScriptBuf { b.into_script() } +pub fn build_drop(items: usize) -> ScriptBuf { + let mut b = Builder::new(); + for _ in 0..items { + b = b.push_opcode(opcodes::all::OP_DROP); + } + b.into_script() +} + /// duplicates (keeps) the first 8 nibbles, accumulates them into `x`, /// leaves `x` on the stack, original 24 nibbles untouched. fn build_script_reconstruct_x() -> ScriptBuf { @@ -229,14 +237,9 @@ pub fn build_script_f1_blake3_locked( // Needed nibbles: prefix_len (because now represented as nibbles) or B / 4 let needed_nibbles = prefix_len; let blake3_script_hash_len_nibbles = 64; - let to_drop = blake3_script_hash_len_nibbles - needed_nibbles; - let drop_script = { - let mut b = Builder::new(); - for _ in 0..to_drop { - b = b.push_opcode(opcodes::all::OP_DROP); - } - b.into_script() - }; + + let drop_script = + build_drop(blake3_script_hash_len_nibbles - needed_nibbles); // 6) compare prefix => OP_EQUALVERIFY let prefix_cmp_script = build_prefix_equalverify(flow_id_prefix); @@ -302,14 +305,9 @@ fn build_script_f2_blake3_locked_with_mode( // Needed nibbles: prefix_len (because now represented as nibbles) or B / 4 let needed_nibbles = prefix_len; let blake3_script_hash_len_nibbles = 64; - let to_drop = blake3_script_hash_len_nibbles - needed_nibbles; - let drop_script = { - let mut b = Builder::new(); - for _ in 0..to_drop { - b = b.push_opcode(opcodes::all::OP_DROP); - } - b.into_script() - }; + + let drop_script = + build_drop(blake3_script_hash_len_nibbles - needed_nibbles); // 6) compare prefix => OP_EQUALVERIFY let prefix_cmp_script = build_prefix_equalverify(flow_id_prefix); @@ -373,7 +371,7 @@ pub fn benchmark_hash_rate(duration_secs: u64) -> u64 { rate as u64 } -fn chunk_message(message_bytes: &[u8]) -> Vec<[u8; 64]> { +pub fn chunk_message(message_bytes: &[u8]) -> Vec<[u8; 64]> { let len = message_bytes.len(); let needed_padding_bytes = if len % 64 == 0 { 0 } else { 64 - (len % 64) }; @@ -443,6 +441,35 @@ pub fn blake3_message_to_limbs(message_bytes: &[u8], limb_len: u8) -> Vec { limbs } +pub fn build_script_hash_to_limbs() -> ScriptBuf { + let mut builder = Builder::new(); + + for _ in 0..56 { + builder = builder.push_opcode(opcodes::all::OP_TOALTSTACK); + } + for i in 0..8 { + for j in (0..8).step_by(2) { + builder = builder + .push_int(j) + .push_opcode(opcodes::all::OP_ROLL) + .push_int(j + 1) + .push_opcode(opcodes::all::OP_ROLL) + .push_opcode(opcodes::all::OP_SWAP); + } + if i != 7 { + for _ in 0..8 { + builder = builder.push_opcode(opcodes::all::OP_FROMALTSTACK); + } + } + } + + for _ in 0..64 { + builder = builder.push_int(0); + } + + builder.into_script() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 379ae8f..ec99d52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod core; pub mod musig2; +pub mod output; pub mod transactions; pub mod utils; diff --git a/src/output.rs b/src/output.rs index 40af212..5d452e5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -46,7 +46,7 @@ pub struct DemoParameters { } pub fn write_demo_output_to_file( - output: &crate::DemoOutput, + output: &DemoOutput, output_dir: &str, path: &str, ) -> Result<()> {