From adf80cba70145055484ac5d90576a5da07a55ff6 Mon Sep 17 00:00:00 2001 From: henrypeters Date: Tue, 9 Dec 2025 18:49:51 +0100 Subject: [PATCH 1/3] bit_check init --- submissions/bitcheck/Cargo.lock | 356 +++++++++++++ submissions/bitcheck/Cargo.toml | 13 + submissions/bitcheck/README.md | 47 ++ submissions/bitcheck/src/main.rs | 883 +++++++++++++++++++++++++++++++ 4 files changed, 1299 insertions(+) create mode 100644 submissions/bitcheck/Cargo.lock create mode 100644 submissions/bitcheck/Cargo.toml create mode 100644 submissions/bitcheck/README.md create mode 100644 submissions/bitcheck/src/main.rs diff --git a/submissions/bitcheck/Cargo.lock b/submissions/bitcheck/Cargo.lock new file mode 100644 index 0000000..119a78d --- /dev/null +++ b/submissions/bitcheck/Cargo.lock @@ -0,0 +1,356 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitcheck" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitcoin", + "hex", + "lazy_static", + "ripemd", + "secp256k1 0.29.1", + "sha2", +] + +[[package]] +name = "bitcoin" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0694ea59225b0c5f3cb405ff3f670e4828358ed26aec49dc352f730f0cb1a8a3" +dependencies = [ + "bech32", + "bitcoin_hashes", + "secp256k1 0.24.3", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "secp256k1" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1629c9c557ef9b293568b338dddfc8208c98a18c59d722a9d53f859d9c9b62" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys 0.6.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/submissions/bitcheck/Cargo.toml b/submissions/bitcheck/Cargo.toml new file mode 100644 index 0000000..d9659b0 --- /dev/null +++ b/submissions/bitcheck/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bitcheck" +version = "0.1.0" +edition = "2024" + +[dependencies] +hex = "0.4" +sha2 = "0.10" +ripemd = "0.1" +lazy_static = "1.4" +anyhow = "1.0" +secp256k1 = { version = "0.29", features = ["rand-std", "serde"] } +bitcoin = { version = "0.29", features = ["serde"] } \ No newline at end of file diff --git a/submissions/bitcheck/README.md b/submissions/bitcheck/README.md new file mode 100644 index 0000000..1dd4af0 --- /dev/null +++ b/submissions/bitcheck/README.md @@ -0,0 +1,47 @@ +# bit_check + +Bitcoin script validator in Rust. Runs locking and unlocking scripts through a stack interpreter. + +## Setup + +```toml +[package] +name = "bit_check" +version = "0.1.0" +edition = "2021" + +[dependencies] +sha2 = "0.10" +ripemd = "0.1" +hex = "0.4" +``` + +```bash +cargo run +``` + +## Usage + +Input locking script (scriptPubKey) and unlocking script (scriptSig). Accepts hex or assembly format. + +``` +Locking Script: OP_DUP OP_HASH160 89abcdefabbaabbaabbaabbaabbaabbaabbaabba OP_EQUALVERIFY OP_CHECKSIG +Type: P2PKH + +Unlocking Script: 3045022100... 04ae1a62... + +Run this script? (y/n): y + +This is a valid script! +``` + +## Features + +- Parses hex and assembly scripts +- Detects P2PK, P2PKH, P2SH, P2MS, OP_RETURN +- Stack-based execution +- Hash160 (SHA256 + RIPEMD160) + +## Opcodes + +OP_DUP, OP_HASH160, OP_EQUAL, OP_EQUALVERIFY, OP_CHECKSIG, OP_CHECKMULTISIG, OP_RETURN, OP_0-16 \ No newline at end of file diff --git a/submissions/bitcheck/src/main.rs b/submissions/bitcheck/src/main.rs new file mode 100644 index 0000000..98f626d --- /dev/null +++ b/submissions/bitcheck/src/main.rs @@ -0,0 +1,883 @@ + +// use anyhow::{bail, Result}; +// use hex::{decode as hex_decode, encode as hex_encode}; +// use sha2::{Digest, Sha256}; +// use ripemd::Ripemd160; +// use std::collections::HashMap; +// use std::io::{self, Write}; +// // use std::hash::Hash; +// use secp256k1::Message; +// use secp256k1::Secp256k1; + +// use bitcoin::hashes::{sha256d, Hash}; + +// type Stack = Vec>; + +// macro_rules! lazy_static { +// ($init:expr) => { +// std::sync::OnceLock::from($init) +// }; +// } + + +// static OPCODE_MAP: std::sync::OnceLock> = std::sync::OnceLock::new(); +// static REVERSE_OPCODE_MAP: std::sync::OnceLock> = std::sync::OnceLock::new(); + +// fn init_opcodes() { +// let mut op = HashMap::new(); +// op.insert(0x00, "OP_0"); +// for i in 1..=16 { +// op.insert(0x50 + i, match i { +// 1 => "OP_1", +// 2 => "OP_2", +// 3 => "OP_3", +// 4 => "OP_4", +// 5 => "OP_5", +// 6 => "OP_6", +// 7 => "OP_7", +// 8 => "OP_8", +// 9 => "OP_9", +// 10 => "OP_10", +// 11 => "OP_11", +// 12 => "OP_12", +// 13 => "OP_13", +// 14 => "OP_14", +// 15 => "OP_15", +// 16 => "OP_16", +// _ => unreachable!(), +// }); +// } +// op.insert(0x76, "OP_DUP"); +// op.insert(0x87, "OP_EQUAL"); +// op.insert(0x88, "OP_EQUALVERIFY"); +// op.insert(0xac, "OP_CHECKSIG"); +// op.insert(0xae, "OP_CHECKMULTISIG"); +// op.insert(0xa9, "OP_HASH160"); +// op.insert(0x6a, "OP_RETURN"); + +// let mut rev = HashMap::new(); +// for (&byte, &name) in &op { +// rev.insert(name, byte); +// } +// rev.insert("OP_FALSE", 0x00); +// rev.insert("OP_TRUE", 0x51); + +// let _ = OPCODE_MAP.set(op); +// let _ = REVERSE_OPCODE_MAP.set(rev); +// } + +// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// enum ScriptType { +// P2PK, +// P2PKH, + +// P2SH, +// P2MS, +// Return, +// Unknown, +// } + +// #[derive(Clone)] +// struct Script { +// hex: String, +// asm: Vec, +// script_type: ScriptType, +// } + +// fn hash160(data: &[u8]) -> Vec { +// let sha = Sha256::digest(data); +// let mut ripemd = Ripemd160::new(); +// ripemd.update(sha); //feed the SHA-256 hash into RIPEMD-160. +// ripemd.finalize().to_vec() //compute the final RIPEMD-160 hash and return it as a Vec (vector of bytes). +// } + +// impl Script { +// fn new(input: &str) -> Result { +// let trimmed = input.trim(); +// if trimmed.contains(' ') || trimmed.contains("OP_") { +// Self::from_asm(trimmed) +// } else { +// Self::from_hex(trimmed) +// } +// } + +// fn from_hex(hex_str: &str) -> Result { +// let bytes = hex_decode(hex_str)?; +// let asm = Self::bytes_to_asm(&bytes); +// let script_type = Self::detect_type(&asm); +// Ok(Script { +// hex: hex_str.to_ascii_lowercase(), +// asm, +// script_type, +// }) +// } + +// fn from_asm(asm_str: &str) -> Result { +// let asm: Vec = asm_str.split_whitespace().map(|s| s.to_string()).collect(); +// let bytes = Self::asm_to_bytes(&asm)?; +// let hex = hex_encode(&bytes); +// let script_type = Self::detect_type(&asm); +// Ok(Script { hex, asm, script_type }) +// } + +// fn bytes_to_asm(bytes: &[u8]) -> Vec { +// let mut asm = Vec::new(); +// let mut i = 0; +// while i < bytes.len() { +// let op = bytes[i]; +// i += 1; + +// if op >= 0x01 && op <= 0x4b { +// let len = op as usize; +// if i + len > bytes.len() { +// break; +// } +// let data = &bytes[i..(i + len)]; +// asm.push(hex_encode(data)); +// i += len; +// } else if let Some(&name) = OPCODE_MAP.get().and_then(|m| m.get(&op)) { +// asm.push(name.to_string()); +// } else { +// asm.push(format!("{:02x}", op)); +// } +// } +// asm +// } + +// fn asm_to_bytes(asm: &[String]) -> Result> { +// let mut bytes = Vec::new(); +// for part in asm { +// if let Some(&code) = REVERSE_OPCODE_MAP.get().and_then(|m| m.get(part.as_str())) { +// bytes.push(code); +// } else if let Ok(n) = part.strip_prefix("OP_").unwrap_or("").parse::() { +// if n <= 16 { +// bytes.push(0x50 + n); +// } else { +// bail!("Invalid OP_n"); +// } +// } else { +// // Raw data push +// let data = hex_decode(part)?; +// let len = data.len(); +// if len < 0x4c { +// bytes.push(len as u8); +// } else if len <= 0xff { +// bytes.push(0x4c); +// bytes.push(len as u8); +// } else { +// bail!("Data too large"); +// } +// bytes.extend_from_slice(&data); +// } +// } +// Ok(bytes) +// } + +// fn detect_type(asm: &[String]) -> ScriptType { +// let op_ch="OP_CHECKSIG".to_string(); +// let op_dup="OP_DUP".to_string(); +// let op_has="OP_HASH160".to_string(); +// let op_eq="OP_EQUALVERIFY".to_string(); +// let op_equ="OP_EQUAL".to_string(); +// let op_ren="OP_RETURN".to_string(); + +// match asm { +// [_, op_ch] => ScriptType::P2PK, + +// [op_dup, op_has, _, op_eq, op_ch] => ScriptType::P2PKH, + +// [op_has, _, op_equ] => ScriptType::P2SH, + +// _ if asm.last().map_or(false, |s| s == "OP_CHECKMULTISIG") => ScriptType::P2MS, + +// [op_ren, ..] => ScriptType::Return, + +// _ => ScriptType::Unknown, +// } +// } + +// fn run(scripts: &[Script], debug: bool) -> Result { +// let mut full_script: Vec = scripts.iter().flat_map(|s| s.asm.clone()).collect(); +// let mut stack: Stack = Vec::new(); + +// while let Some(op) = full_script.first().cloned() { +// full_script.remove(0); + +// let executed = if let Some(&code) = REVERSE_OPCODE_MAP.get().and_then(|m| m.get(op.as_str())) { +// match code { +// 0x76 => { // OP_DUP +// let top = stack.last().ok_or_else(|| anyhow::anyhow!("OP_DUP on empty stack"))?.clone(); +// stack.push(top); +// true +// } +// 0xa9 => { // OP_HASH160 +// let elem = stack.pop().ok_or_else(|| anyhow::anyhow!("OP_HASH160 on empty stack"))?; +// stack.push(hash160(&elem)); +// true +// } +// 0x87 => { // OP_EQUAL +// let b = stack.pop().unwrap(); +// let a = stack.pop().unwrap(); +// stack.push(if a == b { vec![1u8] } else { vec![] }); +// true +// } +// 0x88 => { // OP_EQUALVERIFY +// let b = stack.pop().unwrap(); +// let a = stack.pop().unwrap(); +// if a != b { +// bail!("OP_EQUALVERIFY failed"); +// } +// true +// } +// 0xac => { // OP_CHECKSIG – fake success +// // stack.pop(); +// // stack.pop(); +// // stack.push(vec![1u8]); +// // true + + +// // let sig = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; +// // let pubkey = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; + +// // // stack.push(vec![1]); +// // // stack.push(if pubkey == sig { vec![1u8] } else { vec![] }); + +// // // Strip sighash byte if present +// // // let sig = if !sig.is_empty() && sig[0] & 0x80 != 0 || sig.len() > 65 { +// // // &sig[..sig.len()-1] +// // // } else { +// // // &sig +// // // }; +// // let sig_no_sighash = &sig[..sig.len()-1]; + +// // // Deterministic message: hash of "Bitcoin" + pubkey + sig (common trick in demos) +// // let mut hash_input = b"Bitcoin".to_vec(); +// // hash_input.extend_from_slice(&pubkey); +// // hash_input.extend_from_slice(sig_no_sighash); +// // let msg_hash = sha256d::Hash::hash(&hash_input); +// // let msg = Message::from_slice(&msg_hash[..])?; + +// // let secp = Secp256k1::new(); + +// // let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey) else { +// // stack.push(vec![]); +// // continue; +// // }; +// // let Ok(sig) = secp256k1::ecdsa::Signature::from_der(sig_no_sighash) else { +// // stack.push(vec![]); +// // continue; +// // }; + +// // let verified = secp.verify_ecdsa(&msg, &sig, &pk).is_ok(); +// // stack.push(if verified { vec![1u8] } else { vec![] }); +// // true + + +// let sig = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; +// let pubkey = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; + +// let sig_no_sighash = &sig[..sig.len()-1]; // strip sighash byte + +// // For demo purposes, hash "Bitcoin" + pubkey + sig +// let mut hash_input = b"Bitcoin".to_vec(); +// hash_input.extend_from_slice(&pubkey); +// hash_input.extend_from_slice(sig_no_sighash); +// let msg_hash = sha256d::Hash::hash(&hash_input); +// let msg = Message::from_slice(&msg_hash[..])?; + +// let secp = Secp256k1::new(); + +// let verified = if let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey) { +// if let Ok(sig) = secp256k1::ecdsa::Signature::from_der(sig_no_sighash) { +// secp.verify_ecdsa(&msg, &sig, &pk).is_ok() +// } else { false } +// } else { false }; + +// stack.push(if verified { vec![1u8] } else { vec![] }); +// true +// } + +// // 0xae => { // OP_CHECKMULTISIG – fake + off-by-one bug +// // let n = stack.pop().unwrap()[0] as usize - 0x50; + +// // for _ in 0..n { stack.pop(); } + +// // let m = stack.pop().unwrap()[0] as usize - 0x50; + +// // for _ in 0..m { stack.pop(); } + +// // stack.pop(); // extra pop – Bitcoin bug emulation +// // stack.push(vec![1u8]); +// // true +// // } +// 0x6a => bail!("OP_RETURN makes script invalid"), +// _ => false, +// } +// } else { +// false +// }; + +// if !executed { +// // Must be pushed data +// let data = hex_decode(&op)?; +// stack.push(data); +// } + +// if debug { +// Self::debug_print(&full_script, &stack); +// let mut dummy = String::new(); +// io::stdin().read_line(&mut dummy).ok(); +// } +// } + +// Ok(stack) +// } + +// fn debug_print(remaining: &[String], stack: &Stack) { +// // print!("\x1B[2J\x1B[H"); // clear screen +// println!("Remaining script: {remaining:?}\n"); +// println!("Stack (top → bottom):"); +// if stack.is_empty() { +// println!(" "); +// } else { +// for item in stack.iter().rev() { +// println!(" {}", hex_encode(item)); +// } +// } +// println!("\nPress Enter for next step..."); +// io::stdout().flush().unwrap(); +// } + +// fn validate(stack: &Stack) -> bool { +// !stack.is_empty() && !stack.last().unwrap().is_empty() +// } +// } + +// fn main() -> Result<()> { +// init_opcodes(); + +// println!("Bitcoin Script Interpreter (Rust)\n"); + +// print!("Locking script (hex or asm): "); +// io::stdout().flush()?; +// let mut input = String::new(); +// io::stdin().read_line(&mut input)?; +// let locking = Script::new(&input)?; + +// println!("Type: {:?}\n", locking.script_type); + +// print!("Unlocking script (hex or asm): "); +// io::stdout().flush()?; +// input.clear(); +// io::stdin().read_line(&mut input)?; +// let unlocking = Script::new(&input)?; + +// println!("\n=== Scripts ==="); +// println!("Locking : {}", locking.asm.join(" ")); +// println!("Unlocking: {}", unlocking.asm.join(" ")); +// println!("\nPress Enter to start execution..."); +// io::stdin().read_line(&mut String::new())?; + +// let final_stack = if locking.script_type == ScriptType::P2SH { +// // Very simple P2SH handling – assumes redeem script is last push in unlocking scriptSig +// let redeem_hex = unlocking.asm.last().unwrap(); +// let redeem_script = Script::from_hex(redeem_hex)?; +// Script::run(&[unlocking.clone(), redeem_script], true)? +// } else { +// Script::run(&[unlocking.clone(), locking.clone()], true)? +// }; + +// println!("\n=== Final stack ==="); +// for item in final_stack.iter().rev() { +// println!(" {}", hex_encode(item)); +// } + +// if Script::validate(&final_stack) { +// println!("\nVALID – Transaction would be accepted"); +// } else { +// println!("\nINVALID – Transaction rejected"); +// } + + +// Ok(()) +// } + + +// // # EXAMPLE SCRIPTS (Standard) +// // # --------------- + +// // # p2pk: 4104240ac91558e66c0628693cee5f5120d43caf73cad8586f9f56a447cc6b926520d2b3b259874e5d79dfb4b9aff3405a10cbce47ee820e0824dc7004d5bbcea86fac +// // # p2pk (unlock): 4730440220277c967dda11986e06e508235006b7e83bc27a1cb0ffaa0d97a543e178199b6a022040d4f8f17865e45de9ca7bcfe3ee2228e175cfcb4468b7650f09b534d3f71f4401 + +// // # p2pkh: 76a91491ef7f43180d71d61ca3870a1b0445c116efa78088ac +// // # p2pkh (unlock): + +// // # p2sh: a914e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a87 +// // # p2sh (unlock): 00483045022100ad0851c69dd756b45190b5a8e97cb4ac3c2b0fa2f2aae23aed6ca97ab33bf88302200b248593abc1259512793e7dea61036c601775ebb23640a0120b0dba2c34b79001455141042f90074d7a5bf30c72cf3a8dfd1381bdbd30407010e878f3a11269d5f74a58788505cdca22ea6eab7cfb40dc0e07aba200424ab0d79122a653ad0c7ec9896bdf51ae + +// // # p2ms: 5141204e00003bf2a106de6a91d6b7d3d8f067e70fd40ab0bd7c12f278c35eba8e16e1cd73e5d9871f1f2a027659bce210737856849248260a58e973a9a37a6fbca6354100d8fbd53efe72e1fd664c935e929b2c41b050f5813c93b2d3e8128b3c0e283362002e687c41785947241b3c2523bb9143c80ee82d50867259af4b47a332a8a0aa412f3258f7717826ed1e585af67f5712abe35fb533513d929087cbb364532da3340e377bb156f25c8ee3e2cabb986158eaefe7c3adb4f4a88771440947b1b0c1a34053ae + +// // # return: 6a24aa21a9edcdcb2e39372f6650e4f9d730c34318cc4f0c8d2b9ba3ec2a8b9c74350f7b3044 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +use sha2::{Digest, Sha256}; +use ripemd::{Ripemd160}; +use std::io::{self, Write}; + +// Utility functions +fn hash160(data: &str) -> String { + let binary = hex::decode(data).expect("Invalid hex string"); + let sha256 = Sha256::digest(&binary); + let ripemd = Ripemd160::digest(&sha256); + hex::encode(ripemd) +} + +#[derive(Debug, Clone, PartialEq)] +enum ScriptType { + P2PK, + P2PKH, + P2SH, + P2MS, + Return, + Unknown, +} + +#[derive(Debug, Clone)] +struct Script { + asm: String, + hex: String, + script_type: ScriptType, +} + +impl Script { + fn new(data: &str) -> Self { + let (hex, asm) = if data.chars().all(|c| c.is_ascii_hexdigit() || c.is_whitespace()) + && !data.contains(' ') { + // It's hex + let h = data.to_string(); + let a = Self::hex_to_asm(&h); + (h, a) + } else { + // It's asm + let a = data.to_string(); + let h = Self::asm_to_hex(&a); + (h, a) + }; + + let script_type = Self::get_type(&asm); + + Script { + asm, + hex, + script_type, + } + } + + fn hex_to_asm(hex: &str) -> String { + let mut asm = Vec::new(); + let bytes: Vec<&str> = hex.as_bytes() + .chunks(2) + .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .collect(); + + let mut i = 0; + while i < bytes.len() { + let byte = bytes[i]; + let int_val = u8::from_str_radix(byte, 16).unwrap(); + + if int_val > 0 && int_val < 0x4b { + let data_len = int_val as usize; + i += 1; + let data: String = bytes[i..i + data_len].join(""); + asm.push(data); + i += data_len; + } else { + asm.push(Opcodes::get_opcode(byte)); + i += 1; + } + } + + asm.join(" ") + } + + fn asm_to_hex(asm: &str) -> String { + let mut hex = String::new(); + let pieces: Vec<&str> = asm.split_whitespace().collect(); + + for piece in pieces { + if let Some(opcode_hex) = Opcodes::get_hex(piece) { + hex.push_str(&opcode_hex); + } else { + // It's data, not an opcode + let push = format!("{:02x}", piece.len() / 2); + hex.push_str(&push); + hex.push_str(piece); + } + } + + hex + } + + fn get_type(asm: &str) -> ScriptType { + let parts: Vec<&str> = asm.split_whitespace().collect(); + + if parts.len() == 2 && parts[1] == "OP_CHECKSIG" { + ScriptType::P2PK + } else if parts.len() == 5 + && parts[0] == "OP_DUP" + && parts[1] == "OP_HASH160" + && parts[3] == "OP_EQUALVERIFY" + && parts[4] == "OP_CHECKSIG" { + ScriptType::P2PKH + } else if parts.len() == 3 + && parts[0] == "OP_HASH160" + && parts[2] == "OP_EQUAL" { + ScriptType::P2SH + } else if parts.len() >= 3 + && parts[0].starts_with("OP_") + && parts[parts.len() - 2].starts_with("OP_") + && parts[parts.len() - 1] == "OP_CHECKMULTISIG" { + ScriptType::P2MS + } else if parts.len() == 2 && parts[0] == "OP_RETURN" { + ScriptType::Return + } else { + ScriptType::Unknown + } + } + + fn run(scripts: &[Script]) -> Result, String> { + // Check if P2SH + if scripts.len() > 1 && scripts[1].script_type == ScriptType::P2SH { + return Self::run_p2sh(scripts); + } + + // Combine all scripts + let mut script: Vec = scripts + .iter() + .flat_map(|s| s.asm.split_whitespace().map(|x| x.to_string())) + .collect(); + + let mut stack: Vec = Vec::new(); + + while !script.is_empty() { + let opcode = script.remove(0); + + if Opcodes::is_opcode(&opcode) { + stack = Opcodes::execute(&opcode, stack)?; + } else { + stack.push(opcode); + } + } + + Ok(stack) + } + + fn run_p2sh(scripts: &[Script]) -> Result, String> { + let unlocking = &scripts[0]; + let locking = &scripts[1]; + + // Run unlocking script first + let mut stack = Self::run(&[unlocking.clone()])?; + let stack_copy = stack.clone(); + + // Run combined script + let combined = Script::new(&format!("{}{}", unlocking.hex, locking.hex)); + stack = Self::run(&[combined])?; + + // Validate primary script + if Self::validate(&stack) { + // Get redeem script from stack copy + let mut stack_copy = stack_copy; + let redeem_hex = stack_copy.pop().ok_or("No redeem script on stack")?; + let redeem_script = Script::new(&redeem_hex); + + // Run secondary script + let combined2 = Script::new(&format!("{}{}", unlocking.hex, redeem_script.hex)); + let stack2 = Self::run(&[combined2])?; + + return Ok(stack2); + } + + Ok(stack) + } + + fn validate(stack: &[String]) -> bool { + if stack.is_empty() { + return false; + } + + let top = &stack[stack.len() - 1]; + + if top == "OP_TRUE" { + return true; + } + + if top.starts_with("OP_") { + if let Some(num_str) = top.strip_prefix("OP_") { + if let Ok(num) = num_str.parse::() { + return num > 0; + } + } + } + + false + } +} + +struct Opcodes; + +impl Opcodes { + fn get_opcode(hex: &str) -> String { + match hex.to_lowercase().as_str() { + "00" => "OP_0".to_string(), + "51" => "OP_1".to_string(), + "52" => "OP_2".to_string(), + "53" => "OP_3".to_string(), + "54" => "OP_4".to_string(), + "55" => "OP_5".to_string(), + "56" => "OP_6".to_string(), + "57" => "OP_7".to_string(), + "58" => "OP_8".to_string(), + "59" => "OP_9".to_string(), + "5a" => "OP_10".to_string(), + "5b" => "OP_11".to_string(), + "5c" => "OP_12".to_string(), + "5d" => "OP_13".to_string(), + "5e" => "OP_14".to_string(), + "5f" => "OP_15".to_string(), + "60" => "OP_16".to_string(), + "6a" => "OP_RETURN".to_string(), + "76" => "OP_DUP".to_string(), + "87" => "OP_EQUAL".to_string(), + "88" => "OP_EQUALVERIFY".to_string(), + "ac" => "OP_CHECKSIG".to_string(), + "ae" => "OP_CHECKMULTISIG".to_string(), + "a9" => "OP_HASH160".to_string(), + _ => hex.to_string(), + } + } + + fn get_hex(opcode: &str) -> Option { + match opcode { + "OP_0" => Some("00".to_string()), + "OP_1" => Some("51".to_string()), + "OP_2" => Some("52".to_string()), + "OP_3" => Some("53".to_string()), + "OP_4" => Some("54".to_string()), + "OP_5" => Some("55".to_string()), + "OP_6" => Some("56".to_string()), + "OP_7" => Some("57".to_string()), + "OP_8" => Some("58".to_string()), + "OP_9" => Some("59".to_string()), + "OP_10" => Some("5a".to_string()), + "OP_11" => Some("5b".to_string()), + "OP_12" => Some("5c".to_string()), + "OP_13" => Some("5d".to_string()), + "OP_14" => Some("5e".to_string()), + "OP_15" => Some("5f".to_string()), + "OP_16" => Some("60".to_string()), + "OP_RETURN" => Some("6a".to_string()), + "OP_DUP" => Some("76".to_string()), + "OP_EQUAL" => Some("87".to_string()), + "OP_EQUALVERIFY" => Some("88".to_string()), + "OP_CHECKSIG" => Some("ac".to_string()), + "OP_CHECKMULTISIG" => Some("ae".to_string()), + "OP_HASH160" => Some("a9".to_string()), + _ => None, + } + } + + fn is_opcode(s: &str) -> bool { + s.starts_with("OP_") + } + + fn execute(opcode: &str, mut stack: Vec) -> Result, String> { + match opcode { + "OP_DUP" => { + if stack.is_empty() { + return Err("Stack is empty, cannot duplicate".to_string()); + } + let top = stack.last().unwrap().clone(); + stack.push(top); + Ok(stack) + } + "OP_HASH160" => { + if stack.is_empty() { + return Err("Stack is empty, cannot hash160".to_string()); + } + let top = stack.pop().unwrap(); + let hashed = hash160(&top); + stack.push(hashed); + Ok(stack) + } + "OP_EQUAL" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_EQUAL".to_string()); + } + let a = stack.pop().unwrap(); + let b = stack.pop().unwrap(); + if a == b { + stack.push("OP_TRUE".to_string()); + Ok(stack) + } else { + Err(format!("Items not equal: {} != {}", a, b)) + } + } + "OP_EQUALVERIFY" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_EQUALVERIFY".to_string()); + } + let a = stack.pop().unwrap(); + let b = stack.pop().unwrap(); + if a == b { + Ok(stack) + } else { + Err(format!("Items not equal: {} != {}", a, b)) + } + } + "OP_CHECKSIG" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_CHECKSIG".to_string()); + } + let _pubkey = stack.pop().unwrap(); + let _signature = stack.pop().unwrap(); + // Simplified - not actually validating + stack.push("OP_TRUE".to_string()); + Ok(stack) + } + "OP_CHECKMULTISIG" => { + if stack.is_empty() { + return Err("Stack empty for OP_CHECKMULTISIG".to_string()); + } + let m_str = stack.pop().unwrap(); + let m = m_str.strip_prefix("OP_") + .and_then(|s| s.parse::().ok()) + .ok_or("Invalid m value")?; + + if stack.len() < m { + return Err("Not enough signatures on stack".to_string()); + } + for _ in 0..m { + stack.pop(); + } + + if stack.is_empty() { + return Err("Stack empty getting n value".to_string()); + } + let n_str = stack.pop().unwrap(); + let n = n_str.strip_prefix("OP_") + .and_then(|s| s.parse::().ok()) + .ok_or("Invalid n value")?; + + if stack.len() < n + 1 { + return Err("Not enough public keys on stack".to_string()); + } + for _ in 0..n + 1 { + stack.pop(); + } + + stack.push("OP_TRUE".to_string()); + Ok(stack) + } + "OP_RETURN" => { + Err("Script is invalid. (OP_RETURN always invalidates a script.)".to_string()) + } + _ => Ok(stack), + } + } +} + +fn main() { + println!("Bit_Check"); + println!("==========================\n"); + + print!("Locking Script: "); + io::stdout().flush().unwrap(); + let mut locking_input = String::new(); + io::stdin().read_line(&mut locking_input).unwrap(); + let locking_script = Script::new(locking_input.trim()); + println!("Type: {:?}\n", locking_script.script_type); + + print!("Unlocking Script: "); + io::stdout().flush().unwrap(); + let mut unlocking_input = String::new(); + io::stdin().read_line(&mut unlocking_input).unwrap(); + let unlocking_script = Script::new(unlocking_input.trim()); + + println!("\nLocking Script: {}", locking_script.asm); + println!("Unlocking Script: {}", unlocking_script.asm); + println!(); + + print!("Run this script? (y/n): "); + io::stdout().flush().unwrap(); + let mut yn = String::new(); + io::stdin().read_line(&mut yn).unwrap(); + + if yn.trim() == "y" || yn.trim().is_empty() { + match Script::run(&[unlocking_script, locking_script]) { + Ok(stack) => { + println!("\nFinal Stack: {:?}", stack); + if Script::validate(&stack) { + println!("\n✓ This is a valid script!"); + } else { + println!("\n✗ This is not a valid script."); + } + } + Err(e) => { + println!("\n✗ Script execution failed: {}", e); + } + } + } +} + +// scriptPubKey hex: 1976a9146291ad5107bf1ab687dc744cc9d082aa9522eff088ac +// scriptPubKey asm: OP_DUP OP_HASH160 6291ad5107bf1ab687dc744cc9d082aa9522eff0 OP_EQUALVERIFY + +// scriptSig hex: 6b483045022100c9b46687338c296533a8cd914b9e7e7930535d8c614856ebc7ecaf69fcc3088d0220221409c6d8912d703c32846c975f80c56fd6079e4a03c3311089d27fab730edd012103d031d8d65ef3d737f4f231c11d29c0d037717850402efc55c3544bec1217efd2 +// scriptSig asm: 3045022100c9b46687338c296533a8cd914b9e7e7930535d8c614856ebc7ecaf69fcc3088d0220221409c6d8912d703c32846c975f80c56fd6079e4a03c3311089d27fab730edd01 03d031d8d65ef3d737f4f231c11d29c0d037717850402efc55c3544bec1217efd2 \ No newline at end of file From 663008220a680ce433422b22074361dfeff52cf5 Mon Sep 17 00:00:00 2001 From: henrypeters Date: Wed, 10 Dec 2025 10:32:01 +0100 Subject: [PATCH 2/3] add colored crate --- assessment/Cargo.lock | 16 ++++++ submissions/bitcheck/Cargo.lock | 83 ++++++++++++++++++++++++++++++++ submissions/bitcheck/Cargo.toml | 3 +- submissions/bitcheck/src/main.rs | 16 +++--- 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 assessment/Cargo.lock diff --git a/assessment/Cargo.lock b/assessment/Cargo.lock new file mode 100644 index 0000000..afadbb7 --- /dev/null +++ b/assessment/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "rust-week-2-exercises" +version = "0.1.0" +dependencies = [ + "hex", +] diff --git a/submissions/bitcheck/Cargo.lock b/submissions/bitcheck/Cargo.lock index 119a78d..e116b2b 100644 --- a/submissions/bitcheck/Cargo.lock +++ b/submissions/bitcheck/Cargo.lock @@ -20,6 +20,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bitcoin", + "colored", "hex", "lazy_static", "ripemd", @@ -73,6 +74,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -335,6 +345,79 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.8.31" diff --git a/submissions/bitcheck/Cargo.toml b/submissions/bitcheck/Cargo.toml index d9659b0..1ba884f 100644 --- a/submissions/bitcheck/Cargo.toml +++ b/submissions/bitcheck/Cargo.toml @@ -10,4 +10,5 @@ ripemd = "0.1" lazy_static = "1.4" anyhow = "1.0" secp256k1 = { version = "0.29", features = ["rand-std", "serde"] } -bitcoin = { version = "0.29", features = ["serde"] } \ No newline at end of file +bitcoin = { version = "0.29", features = ["serde"] } +colored = "3.0.0" diff --git a/submissions/bitcheck/src/main.rs b/submissions/bitcheck/src/main.rs index 98f626d..5bae0df 100644 --- a/submissions/bitcheck/src/main.rs +++ b/submissions/bitcheck/src/main.rs @@ -469,6 +469,7 @@ use sha2::{Digest, Sha256}; use ripemd::{Ripemd160}; use std::io::{self, Write}; +use colored::*; // Utility functions fn hash160(data: &str) -> String { @@ -499,12 +500,10 @@ impl Script { fn new(data: &str) -> Self { let (hex, asm) = if data.chars().all(|c| c.is_ascii_hexdigit() || c.is_whitespace()) && !data.contains(' ') { - // It's hex let h = data.to_string(); let a = Self::hex_to_asm(&h); (h, a) } else { - // It's asm let a = data.to_string(); let h = Self::asm_to_hex(&a); (h, a) @@ -834,17 +833,17 @@ impl Opcodes { } fn main() { - println!("Bit_Check"); - println!("==========================\n"); + println!("{}", "\nBit_Check".yellow().bold()); + println!("{}", "==========================\n".black()); - print!("Locking Script: "); + print!("{}", "Locking Script: ".blue().bold()); io::stdout().flush().unwrap(); let mut locking_input = String::new(); io::stdin().read_line(&mut locking_input).unwrap(); let locking_script = Script::new(locking_input.trim()); println!("Type: {:?}\n", locking_script.script_type); - print!("Unlocking Script: "); + print!("{}", "Unlocking Script: ".blue().bold()); io::stdout().flush().unwrap(); let mut unlocking_input = String::new(); io::stdin().read_line(&mut unlocking_input).unwrap(); @@ -864,13 +863,14 @@ fn main() { Ok(stack) => { println!("\nFinal Stack: {:?}", stack); if Script::validate(&stack) { - println!("\n✓ This is a valid script!"); + println!("{}", "\n✓ This is a valid script!".green()); } else { println!("\n✗ This is not a valid script."); + println!(""); } } Err(e) => { - println!("\n✗ Script execution failed: {}", e); + println!("\n Script execution failed: {}", e); } } } From 6212a79aed25e6a69dae2bcb91c2bd83710bf650 Mon Sep 17 00:00:00 2001 From: henrypeters Date: Mon, 15 Dec 2025 12:57:47 +0100 Subject: [PATCH 3/3] chore: modularize code --- submissions/bitcheck/README.md | 4 +- submissions/bitcheck/src/hashing.rs | 9 + submissions/bitcheck/src/main.rs | 841 +----------------------- submissions/bitcheck/src/opcodes.rs | 162 +++++ submissions/bitcheck/src/script_impl.rs | 175 +++++ submissions/bitcheck/src/script_type.rs | 9 + 6 files changed, 363 insertions(+), 837 deletions(-) create mode 100644 submissions/bitcheck/src/hashing.rs create mode 100644 submissions/bitcheck/src/opcodes.rs create mode 100644 submissions/bitcheck/src/script_impl.rs create mode 100644 submissions/bitcheck/src/script_type.rs diff --git a/submissions/bitcheck/README.md b/submissions/bitcheck/README.md index 1dd4af0..10ac3af 100644 --- a/submissions/bitcheck/README.md +++ b/submissions/bitcheck/README.md @@ -25,10 +25,10 @@ cargo run Input locking script (scriptPubKey) and unlocking script (scriptSig). Accepts hex or assembly format. ``` -Locking Script: OP_DUP OP_HASH160 89abcdefabbaabbaabbaabbaabbaabbaabbaabba OP_EQUALVERIFY OP_CHECKSIG +Locking Script: OP_DUP OP_HASH160 6291ad5107bf1ab687dc744cc9d082aa9522eff0 OP_EQUALVERIFY OP_CHECKSIG Type: P2PKH -Unlocking Script: 3045022100... 04ae1a62... +Unlocking Script: 3045022100... 03d19433... Run this script? (y/n): y diff --git a/submissions/bitcheck/src/hashing.rs b/submissions/bitcheck/src/hashing.rs new file mode 100644 index 0000000..5a8016d --- /dev/null +++ b/submissions/bitcheck/src/hashing.rs @@ -0,0 +1,9 @@ +use sha2::{Digest, Sha256}; +use ripemd::{Ripemd160}; + +pub fn hash160(data: &str) -> String { + let binary = hex::decode(data).expect("Invalid hex string"); + let sha256 = Sha256::digest(&binary); + let ripemd = Ripemd160::digest(&sha256); + hex::encode(ripemd) +} \ No newline at end of file diff --git a/submissions/bitcheck/src/main.rs b/submissions/bitcheck/src/main.rs index 5bae0df..1beaebf 100644 --- a/submissions/bitcheck/src/main.rs +++ b/submissions/bitcheck/src/main.rs @@ -1,836 +1,13 @@ +mod script_type; +mod script_impl; +mod opcodes; +mod hashing; -// use anyhow::{bail, Result}; -// use hex::{decode as hex_decode, encode as hex_encode}; -// use sha2::{Digest, Sha256}; -// use ripemd::Ripemd160; -// use std::collections::HashMap; -// use std::io::{self, Write}; -// // use std::hash::Hash; -// use secp256k1::Message; -// use secp256k1::Secp256k1; - -// use bitcoin::hashes::{sha256d, Hash}; - -// type Stack = Vec>; - -// macro_rules! lazy_static { -// ($init:expr) => { -// std::sync::OnceLock::from($init) -// }; -// } - - -// static OPCODE_MAP: std::sync::OnceLock> = std::sync::OnceLock::new(); -// static REVERSE_OPCODE_MAP: std::sync::OnceLock> = std::sync::OnceLock::new(); - -// fn init_opcodes() { -// let mut op = HashMap::new(); -// op.insert(0x00, "OP_0"); -// for i in 1..=16 { -// op.insert(0x50 + i, match i { -// 1 => "OP_1", -// 2 => "OP_2", -// 3 => "OP_3", -// 4 => "OP_4", -// 5 => "OP_5", -// 6 => "OP_6", -// 7 => "OP_7", -// 8 => "OP_8", -// 9 => "OP_9", -// 10 => "OP_10", -// 11 => "OP_11", -// 12 => "OP_12", -// 13 => "OP_13", -// 14 => "OP_14", -// 15 => "OP_15", -// 16 => "OP_16", -// _ => unreachable!(), -// }); -// } -// op.insert(0x76, "OP_DUP"); -// op.insert(0x87, "OP_EQUAL"); -// op.insert(0x88, "OP_EQUALVERIFY"); -// op.insert(0xac, "OP_CHECKSIG"); -// op.insert(0xae, "OP_CHECKMULTISIG"); -// op.insert(0xa9, "OP_HASH160"); -// op.insert(0x6a, "OP_RETURN"); - -// let mut rev = HashMap::new(); -// for (&byte, &name) in &op { -// rev.insert(name, byte); -// } -// rev.insert("OP_FALSE", 0x00); -// rev.insert("OP_TRUE", 0x51); - -// let _ = OPCODE_MAP.set(op); -// let _ = REVERSE_OPCODE_MAP.set(rev); -// } - -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// enum ScriptType { -// P2PK, -// P2PKH, - -// P2SH, -// P2MS, -// Return, -// Unknown, -// } - -// #[derive(Clone)] -// struct Script { -// hex: String, -// asm: Vec, -// script_type: ScriptType, -// } - -// fn hash160(data: &[u8]) -> Vec { -// let sha = Sha256::digest(data); -// let mut ripemd = Ripemd160::new(); -// ripemd.update(sha); //feed the SHA-256 hash into RIPEMD-160. -// ripemd.finalize().to_vec() //compute the final RIPEMD-160 hash and return it as a Vec (vector of bytes). -// } - -// impl Script { -// fn new(input: &str) -> Result { -// let trimmed = input.trim(); -// if trimmed.contains(' ') || trimmed.contains("OP_") { -// Self::from_asm(trimmed) -// } else { -// Self::from_hex(trimmed) -// } -// } - -// fn from_hex(hex_str: &str) -> Result { -// let bytes = hex_decode(hex_str)?; -// let asm = Self::bytes_to_asm(&bytes); -// let script_type = Self::detect_type(&asm); -// Ok(Script { -// hex: hex_str.to_ascii_lowercase(), -// asm, -// script_type, -// }) -// } - -// fn from_asm(asm_str: &str) -> Result { -// let asm: Vec = asm_str.split_whitespace().map(|s| s.to_string()).collect(); -// let bytes = Self::asm_to_bytes(&asm)?; -// let hex = hex_encode(&bytes); -// let script_type = Self::detect_type(&asm); -// Ok(Script { hex, asm, script_type }) -// } - -// fn bytes_to_asm(bytes: &[u8]) -> Vec { -// let mut asm = Vec::new(); -// let mut i = 0; -// while i < bytes.len() { -// let op = bytes[i]; -// i += 1; - -// if op >= 0x01 && op <= 0x4b { -// let len = op as usize; -// if i + len > bytes.len() { -// break; -// } -// let data = &bytes[i..(i + len)]; -// asm.push(hex_encode(data)); -// i += len; -// } else if let Some(&name) = OPCODE_MAP.get().and_then(|m| m.get(&op)) { -// asm.push(name.to_string()); -// } else { -// asm.push(format!("{:02x}", op)); -// } -// } -// asm -// } - -// fn asm_to_bytes(asm: &[String]) -> Result> { -// let mut bytes = Vec::new(); -// for part in asm { -// if let Some(&code) = REVERSE_OPCODE_MAP.get().and_then(|m| m.get(part.as_str())) { -// bytes.push(code); -// } else if let Ok(n) = part.strip_prefix("OP_").unwrap_or("").parse::() { -// if n <= 16 { -// bytes.push(0x50 + n); -// } else { -// bail!("Invalid OP_n"); -// } -// } else { -// // Raw data push -// let data = hex_decode(part)?; -// let len = data.len(); -// if len < 0x4c { -// bytes.push(len as u8); -// } else if len <= 0xff { -// bytes.push(0x4c); -// bytes.push(len as u8); -// } else { -// bail!("Data too large"); -// } -// bytes.extend_from_slice(&data); -// } -// } -// Ok(bytes) -// } - -// fn detect_type(asm: &[String]) -> ScriptType { -// let op_ch="OP_CHECKSIG".to_string(); -// let op_dup="OP_DUP".to_string(); -// let op_has="OP_HASH160".to_string(); -// let op_eq="OP_EQUALVERIFY".to_string(); -// let op_equ="OP_EQUAL".to_string(); -// let op_ren="OP_RETURN".to_string(); - -// match asm { -// [_, op_ch] => ScriptType::P2PK, - -// [op_dup, op_has, _, op_eq, op_ch] => ScriptType::P2PKH, - -// [op_has, _, op_equ] => ScriptType::P2SH, - -// _ if asm.last().map_or(false, |s| s == "OP_CHECKMULTISIG") => ScriptType::P2MS, - -// [op_ren, ..] => ScriptType::Return, - -// _ => ScriptType::Unknown, -// } -// } - -// fn run(scripts: &[Script], debug: bool) -> Result { -// let mut full_script: Vec = scripts.iter().flat_map(|s| s.asm.clone()).collect(); -// let mut stack: Stack = Vec::new(); - -// while let Some(op) = full_script.first().cloned() { -// full_script.remove(0); - -// let executed = if let Some(&code) = REVERSE_OPCODE_MAP.get().and_then(|m| m.get(op.as_str())) { -// match code { -// 0x76 => { // OP_DUP -// let top = stack.last().ok_or_else(|| anyhow::anyhow!("OP_DUP on empty stack"))?.clone(); -// stack.push(top); -// true -// } -// 0xa9 => { // OP_HASH160 -// let elem = stack.pop().ok_or_else(|| anyhow::anyhow!("OP_HASH160 on empty stack"))?; -// stack.push(hash160(&elem)); -// true -// } -// 0x87 => { // OP_EQUAL -// let b = stack.pop().unwrap(); -// let a = stack.pop().unwrap(); -// stack.push(if a == b { vec![1u8] } else { vec![] }); -// true -// } -// 0x88 => { // OP_EQUALVERIFY -// let b = stack.pop().unwrap(); -// let a = stack.pop().unwrap(); -// if a != b { -// bail!("OP_EQUALVERIFY failed"); -// } -// true -// } -// 0xac => { // OP_CHECKSIG – fake success -// // stack.pop(); -// // stack.pop(); -// // stack.push(vec![1u8]); -// // true - - -// // let sig = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; -// // let pubkey = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; - -// // // stack.push(vec![1]); -// // // stack.push(if pubkey == sig { vec![1u8] } else { vec![] }); - -// // // Strip sighash byte if present -// // // let sig = if !sig.is_empty() && sig[0] & 0x80 != 0 || sig.len() > 65 { -// // // &sig[..sig.len()-1] -// // // } else { -// // // &sig -// // // }; -// // let sig_no_sighash = &sig[..sig.len()-1]; - -// // // Deterministic message: hash of "Bitcoin" + pubkey + sig (common trick in demos) -// // let mut hash_input = b"Bitcoin".to_vec(); -// // hash_input.extend_from_slice(&pubkey); -// // hash_input.extend_from_slice(sig_no_sighash); -// // let msg_hash = sha256d::Hash::hash(&hash_input); -// // let msg = Message::from_slice(&msg_hash[..])?; - -// // let secp = Secp256k1::new(); - -// // let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey) else { -// // stack.push(vec![]); -// // continue; -// // }; -// // let Ok(sig) = secp256k1::ecdsa::Signature::from_der(sig_no_sighash) else { -// // stack.push(vec![]); -// // continue; -// // }; - -// // let verified = secp.verify_ecdsa(&msg, &sig, &pk).is_ok(); -// // stack.push(if verified { vec![1u8] } else { vec![] }); -// // true - - -// let sig = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; -// let pubkey = stack.pop().ok_or_else(|| anyhow::anyhow!("Empty stack"))?; - -// let sig_no_sighash = &sig[..sig.len()-1]; // strip sighash byte - -// // For demo purposes, hash "Bitcoin" + pubkey + sig -// let mut hash_input = b"Bitcoin".to_vec(); -// hash_input.extend_from_slice(&pubkey); -// hash_input.extend_from_slice(sig_no_sighash); -// let msg_hash = sha256d::Hash::hash(&hash_input); -// let msg = Message::from_slice(&msg_hash[..])?; - -// let secp = Secp256k1::new(); - -// let verified = if let Ok(pk) = secp256k1::PublicKey::from_slice(&pubkey) { -// if let Ok(sig) = secp256k1::ecdsa::Signature::from_der(sig_no_sighash) { -// secp.verify_ecdsa(&msg, &sig, &pk).is_ok() -// } else { false } -// } else { false }; - -// stack.push(if verified { vec![1u8] } else { vec![] }); -// true -// } - -// // 0xae => { // OP_CHECKMULTISIG – fake + off-by-one bug -// // let n = stack.pop().unwrap()[0] as usize - 0x50; - -// // for _ in 0..n { stack.pop(); } - -// // let m = stack.pop().unwrap()[0] as usize - 0x50; - -// // for _ in 0..m { stack.pop(); } - -// // stack.pop(); // extra pop – Bitcoin bug emulation -// // stack.push(vec![1u8]); -// // true -// // } -// 0x6a => bail!("OP_RETURN makes script invalid"), -// _ => false, -// } -// } else { -// false -// }; - -// if !executed { -// // Must be pushed data -// let data = hex_decode(&op)?; -// stack.push(data); -// } - -// if debug { -// Self::debug_print(&full_script, &stack); -// let mut dummy = String::new(); -// io::stdin().read_line(&mut dummy).ok(); -// } -// } - -// Ok(stack) -// } - -// fn debug_print(remaining: &[String], stack: &Stack) { -// // print!("\x1B[2J\x1B[H"); // clear screen -// println!("Remaining script: {remaining:?}\n"); -// println!("Stack (top → bottom):"); -// if stack.is_empty() { -// println!(" "); -// } else { -// for item in stack.iter().rev() { -// println!(" {}", hex_encode(item)); -// } -// } -// println!("\nPress Enter for next step..."); -// io::stdout().flush().unwrap(); -// } - -// fn validate(stack: &Stack) -> bool { -// !stack.is_empty() && !stack.last().unwrap().is_empty() -// } -// } - -// fn main() -> Result<()> { -// init_opcodes(); - -// println!("Bitcoin Script Interpreter (Rust)\n"); - -// print!("Locking script (hex or asm): "); -// io::stdout().flush()?; -// let mut input = String::new(); -// io::stdin().read_line(&mut input)?; -// let locking = Script::new(&input)?; - -// println!("Type: {:?}\n", locking.script_type); - -// print!("Unlocking script (hex or asm): "); -// io::stdout().flush()?; -// input.clear(); -// io::stdin().read_line(&mut input)?; -// let unlocking = Script::new(&input)?; - -// println!("\n=== Scripts ==="); -// println!("Locking : {}", locking.asm.join(" ")); -// println!("Unlocking: {}", unlocking.asm.join(" ")); -// println!("\nPress Enter to start execution..."); -// io::stdin().read_line(&mut String::new())?; - -// let final_stack = if locking.script_type == ScriptType::P2SH { -// // Very simple P2SH handling – assumes redeem script is last push in unlocking scriptSig -// let redeem_hex = unlocking.asm.last().unwrap(); -// let redeem_script = Script::from_hex(redeem_hex)?; -// Script::run(&[unlocking.clone(), redeem_script], true)? -// } else { -// Script::run(&[unlocking.clone(), locking.clone()], true)? -// }; - -// println!("\n=== Final stack ==="); -// for item in final_stack.iter().rev() { -// println!(" {}", hex_encode(item)); -// } - -// if Script::validate(&final_stack) { -// println!("\nVALID – Transaction would be accepted"); -// } else { -// println!("\nINVALID – Transaction rejected"); -// } - - -// Ok(()) -// } - - -// // # EXAMPLE SCRIPTS (Standard) -// // # --------------- - -// // # p2pk: 4104240ac91558e66c0628693cee5f5120d43caf73cad8586f9f56a447cc6b926520d2b3b259874e5d79dfb4b9aff3405a10cbce47ee820e0824dc7004d5bbcea86fac -// // # p2pk (unlock): 4730440220277c967dda11986e06e508235006b7e83bc27a1cb0ffaa0d97a543e178199b6a022040d4f8f17865e45de9ca7bcfe3ee2228e175cfcb4468b7650f09b534d3f71f4401 - -// // # p2pkh: 76a91491ef7f43180d71d61ca3870a1b0445c116efa78088ac -// // # p2pkh (unlock): - -// // # p2sh: a914e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a87 -// // # p2sh (unlock): 00483045022100ad0851c69dd756b45190b5a8e97cb4ac3c2b0fa2f2aae23aed6ca97ab33bf88302200b248593abc1259512793e7dea61036c601775ebb23640a0120b0dba2c34b79001455141042f90074d7a5bf30c72cf3a8dfd1381bdbd30407010e878f3a11269d5f74a58788505cdca22ea6eab7cfb40dc0e07aba200424ab0d79122a653ad0c7ec9896bdf51ae - -// // # p2ms: 5141204e00003bf2a106de6a91d6b7d3d8f067e70fd40ab0bd7c12f278c35eba8e16e1cd73e5d9871f1f2a027659bce210737856849248260a58e973a9a37a6fbca6354100d8fbd53efe72e1fd664c935e929b2c41b050f5813c93b2d3e8128b3c0e283362002e687c41785947241b3c2523bb9143c80ee82d50867259af4b47a332a8a0aa412f3258f7717826ed1e585af67f5712abe35fb533513d929087cbb364532da3340e377bb156f25c8ee3e2cabb986158eaefe7c3adb4f4a88771440947b1b0c1a34053ae - -// // # return: 6a24aa21a9edcdcb2e39372f6650e4f9d730c34318cc4f0c8d2b9ba3ec2a8b9c74350f7b3044 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -use sha2::{Digest, Sha256}; -use ripemd::{Ripemd160}; use std::io::{self, Write}; use colored::*; -// Utility functions -fn hash160(data: &str) -> String { - let binary = hex::decode(data).expect("Invalid hex string"); - let sha256 = Sha256::digest(&binary); - let ripemd = Ripemd160::digest(&sha256); - hex::encode(ripemd) -} - -#[derive(Debug, Clone, PartialEq)] -enum ScriptType { - P2PK, - P2PKH, - P2SH, - P2MS, - Return, - Unknown, -} - -#[derive(Debug, Clone)] -struct Script { - asm: String, - hex: String, - script_type: ScriptType, -} - -impl Script { - fn new(data: &str) -> Self { - let (hex, asm) = if data.chars().all(|c| c.is_ascii_hexdigit() || c.is_whitespace()) - && !data.contains(' ') { - let h = data.to_string(); - let a = Self::hex_to_asm(&h); - (h, a) - } else { - let a = data.to_string(); - let h = Self::asm_to_hex(&a); - (h, a) - }; - - let script_type = Self::get_type(&asm); - - Script { - asm, - hex, - script_type, - } - } - - fn hex_to_asm(hex: &str) -> String { - let mut asm = Vec::new(); - let bytes: Vec<&str> = hex.as_bytes() - .chunks(2) - .map(|chunk| std::str::from_utf8(chunk).unwrap()) - .collect(); - - let mut i = 0; - while i < bytes.len() { - let byte = bytes[i]; - let int_val = u8::from_str_radix(byte, 16).unwrap(); - - if int_val > 0 && int_val < 0x4b { - let data_len = int_val as usize; - i += 1; - let data: String = bytes[i..i + data_len].join(""); - asm.push(data); - i += data_len; - } else { - asm.push(Opcodes::get_opcode(byte)); - i += 1; - } - } - - asm.join(" ") - } - - fn asm_to_hex(asm: &str) -> String { - let mut hex = String::new(); - let pieces: Vec<&str> = asm.split_whitespace().collect(); - - for piece in pieces { - if let Some(opcode_hex) = Opcodes::get_hex(piece) { - hex.push_str(&opcode_hex); - } else { - // It's data, not an opcode - let push = format!("{:02x}", piece.len() / 2); - hex.push_str(&push); - hex.push_str(piece); - } - } - - hex - } - - fn get_type(asm: &str) -> ScriptType { - let parts: Vec<&str> = asm.split_whitespace().collect(); - - if parts.len() == 2 && parts[1] == "OP_CHECKSIG" { - ScriptType::P2PK - } else if parts.len() == 5 - && parts[0] == "OP_DUP" - && parts[1] == "OP_HASH160" - && parts[3] == "OP_EQUALVERIFY" - && parts[4] == "OP_CHECKSIG" { - ScriptType::P2PKH - } else if parts.len() == 3 - && parts[0] == "OP_HASH160" - && parts[2] == "OP_EQUAL" { - ScriptType::P2SH - } else if parts.len() >= 3 - && parts[0].starts_with("OP_") - && parts[parts.len() - 2].starts_with("OP_") - && parts[parts.len() - 1] == "OP_CHECKMULTISIG" { - ScriptType::P2MS - } else if parts.len() == 2 && parts[0] == "OP_RETURN" { - ScriptType::Return - } else { - ScriptType::Unknown - } - } - - fn run(scripts: &[Script]) -> Result, String> { - // Check if P2SH - if scripts.len() > 1 && scripts[1].script_type == ScriptType::P2SH { - return Self::run_p2sh(scripts); - } - - // Combine all scripts - let mut script: Vec = scripts - .iter() - .flat_map(|s| s.asm.split_whitespace().map(|x| x.to_string())) - .collect(); - - let mut stack: Vec = Vec::new(); - - while !script.is_empty() { - let opcode = script.remove(0); - - if Opcodes::is_opcode(&opcode) { - stack = Opcodes::execute(&opcode, stack)?; - } else { - stack.push(opcode); - } - } - - Ok(stack) - } - - fn run_p2sh(scripts: &[Script]) -> Result, String> { - let unlocking = &scripts[0]; - let locking = &scripts[1]; - - // Run unlocking script first - let mut stack = Self::run(&[unlocking.clone()])?; - let stack_copy = stack.clone(); - - // Run combined script - let combined = Script::new(&format!("{}{}", unlocking.hex, locking.hex)); - stack = Self::run(&[combined])?; - - // Validate primary script - if Self::validate(&stack) { - // Get redeem script from stack copy - let mut stack_copy = stack_copy; - let redeem_hex = stack_copy.pop().ok_or("No redeem script on stack")?; - let redeem_script = Script::new(&redeem_hex); - - // Run secondary script - let combined2 = Script::new(&format!("{}{}", unlocking.hex, redeem_script.hex)); - let stack2 = Self::run(&[combined2])?; +use script_impl::{Script}; - return Ok(stack2); - } - - Ok(stack) - } - - fn validate(stack: &[String]) -> bool { - if stack.is_empty() { - return false; - } - - let top = &stack[stack.len() - 1]; - - if top == "OP_TRUE" { - return true; - } - - if top.starts_with("OP_") { - if let Some(num_str) = top.strip_prefix("OP_") { - if let Ok(num) = num_str.parse::() { - return num > 0; - } - } - } - - false - } -} - -struct Opcodes; - -impl Opcodes { - fn get_opcode(hex: &str) -> String { - match hex.to_lowercase().as_str() { - "00" => "OP_0".to_string(), - "51" => "OP_1".to_string(), - "52" => "OP_2".to_string(), - "53" => "OP_3".to_string(), - "54" => "OP_4".to_string(), - "55" => "OP_5".to_string(), - "56" => "OP_6".to_string(), - "57" => "OP_7".to_string(), - "58" => "OP_8".to_string(), - "59" => "OP_9".to_string(), - "5a" => "OP_10".to_string(), - "5b" => "OP_11".to_string(), - "5c" => "OP_12".to_string(), - "5d" => "OP_13".to_string(), - "5e" => "OP_14".to_string(), - "5f" => "OP_15".to_string(), - "60" => "OP_16".to_string(), - "6a" => "OP_RETURN".to_string(), - "76" => "OP_DUP".to_string(), - "87" => "OP_EQUAL".to_string(), - "88" => "OP_EQUALVERIFY".to_string(), - "ac" => "OP_CHECKSIG".to_string(), - "ae" => "OP_CHECKMULTISIG".to_string(), - "a9" => "OP_HASH160".to_string(), - _ => hex.to_string(), - } - } - - fn get_hex(opcode: &str) -> Option { - match opcode { - "OP_0" => Some("00".to_string()), - "OP_1" => Some("51".to_string()), - "OP_2" => Some("52".to_string()), - "OP_3" => Some("53".to_string()), - "OP_4" => Some("54".to_string()), - "OP_5" => Some("55".to_string()), - "OP_6" => Some("56".to_string()), - "OP_7" => Some("57".to_string()), - "OP_8" => Some("58".to_string()), - "OP_9" => Some("59".to_string()), - "OP_10" => Some("5a".to_string()), - "OP_11" => Some("5b".to_string()), - "OP_12" => Some("5c".to_string()), - "OP_13" => Some("5d".to_string()), - "OP_14" => Some("5e".to_string()), - "OP_15" => Some("5f".to_string()), - "OP_16" => Some("60".to_string()), - "OP_RETURN" => Some("6a".to_string()), - "OP_DUP" => Some("76".to_string()), - "OP_EQUAL" => Some("87".to_string()), - "OP_EQUALVERIFY" => Some("88".to_string()), - "OP_CHECKSIG" => Some("ac".to_string()), - "OP_CHECKMULTISIG" => Some("ae".to_string()), - "OP_HASH160" => Some("a9".to_string()), - _ => None, - } - } - - fn is_opcode(s: &str) -> bool { - s.starts_with("OP_") - } - - fn execute(opcode: &str, mut stack: Vec) -> Result, String> { - match opcode { - "OP_DUP" => { - if stack.is_empty() { - return Err("Stack is empty, cannot duplicate".to_string()); - } - let top = stack.last().unwrap().clone(); - stack.push(top); - Ok(stack) - } - "OP_HASH160" => { - if stack.is_empty() { - return Err("Stack is empty, cannot hash160".to_string()); - } - let top = stack.pop().unwrap(); - let hashed = hash160(&top); - stack.push(hashed); - Ok(stack) - } - "OP_EQUAL" => { - if stack.len() < 2 { - return Err("Not enough items on stack for OP_EQUAL".to_string()); - } - let a = stack.pop().unwrap(); - let b = stack.pop().unwrap(); - if a == b { - stack.push("OP_TRUE".to_string()); - Ok(stack) - } else { - Err(format!("Items not equal: {} != {}", a, b)) - } - } - "OP_EQUALVERIFY" => { - if stack.len() < 2 { - return Err("Not enough items on stack for OP_EQUALVERIFY".to_string()); - } - let a = stack.pop().unwrap(); - let b = stack.pop().unwrap(); - if a == b { - Ok(stack) - } else { - Err(format!("Items not equal: {} != {}", a, b)) - } - } - "OP_CHECKSIG" => { - if stack.len() < 2 { - return Err("Not enough items on stack for OP_CHECKSIG".to_string()); - } - let _pubkey = stack.pop().unwrap(); - let _signature = stack.pop().unwrap(); - // Simplified - not actually validating - stack.push("OP_TRUE".to_string()); - Ok(stack) - } - "OP_CHECKMULTISIG" => { - if stack.is_empty() { - return Err("Stack empty for OP_CHECKMULTISIG".to_string()); - } - let m_str = stack.pop().unwrap(); - let m = m_str.strip_prefix("OP_") - .and_then(|s| s.parse::().ok()) - .ok_or("Invalid m value")?; - - if stack.len() < m { - return Err("Not enough signatures on stack".to_string()); - } - for _ in 0..m { - stack.pop(); - } - - if stack.is_empty() { - return Err("Stack empty getting n value".to_string()); - } - let n_str = stack.pop().unwrap(); - let n = n_str.strip_prefix("OP_") - .and_then(|s| s.parse::().ok()) - .ok_or("Invalid n value")?; - - if stack.len() < n + 1 { - return Err("Not enough public keys on stack".to_string()); - } - for _ in 0..n + 1 { - stack.pop(); - } - - stack.push("OP_TRUE".to_string()); - Ok(stack) - } - "OP_RETURN" => { - Err("Script is invalid. (OP_RETURN always invalidates a script.)".to_string()) - } - _ => Ok(stack), - } - } -} fn main() { println!("{}", "\nBit_Check".yellow().bold()); @@ -874,10 +51,4 @@ fn main() { } } } -} - -// scriptPubKey hex: 1976a9146291ad5107bf1ab687dc744cc9d082aa9522eff088ac -// scriptPubKey asm: OP_DUP OP_HASH160 6291ad5107bf1ab687dc744cc9d082aa9522eff0 OP_EQUALVERIFY - -// scriptSig hex: 6b483045022100c9b46687338c296533a8cd914b9e7e7930535d8c614856ebc7ecaf69fcc3088d0220221409c6d8912d703c32846c975f80c56fd6079e4a03c3311089d27fab730edd012103d031d8d65ef3d737f4f231c11d29c0d037717850402efc55c3544bec1217efd2 -// scriptSig asm: 3045022100c9b46687338c296533a8cd914b9e7e7930535d8c614856ebc7ecaf69fcc3088d0220221409c6d8912d703c32846c975f80c56fd6079e4a03c3311089d27fab730edd01 03d031d8d65ef3d737f4f231c11d29c0d037717850402efc55c3544bec1217efd2 \ No newline at end of file +} \ No newline at end of file diff --git a/submissions/bitcheck/src/opcodes.rs b/submissions/bitcheck/src/opcodes.rs new file mode 100644 index 0000000..299c208 --- /dev/null +++ b/submissions/bitcheck/src/opcodes.rs @@ -0,0 +1,162 @@ +use crate::hashing::hash160; +pub struct Opcodes; + +impl Opcodes { + pub fn get_opcode(hex: &str) -> String { + match hex.to_lowercase().as_str() { + "00" => "OP_0".to_string(), + "51" => "OP_1".to_string(), + "52" => "OP_2".to_string(), + "53" => "OP_3".to_string(), + "54" => "OP_4".to_string(), + "55" => "OP_5".to_string(), + "56" => "OP_6".to_string(), + "57" => "OP_7".to_string(), + "58" => "OP_8".to_string(), + "59" => "OP_9".to_string(), + "5a" => "OP_10".to_string(), + "5b" => "OP_11".to_string(), + "5c" => "OP_12".to_string(), + "5d" => "OP_13".to_string(), + "5e" => "OP_14".to_string(), + "5f" => "OP_15".to_string(), + "60" => "OP_16".to_string(), + "6a" => "OP_RETURN".to_string(), + "76" => "OP_DUP".to_string(), + "87" => "OP_EQUAL".to_string(), + "88" => "OP_EQUALVERIFY".to_string(), + "ac" => "OP_CHECKSIG".to_string(), + "ae" => "OP_CHECKMULTISIG".to_string(), + "a9" => "OP_HASH160".to_string(), + _ => hex.to_string(), + } + } + + pub fn get_hex(opcode: &str) -> Option { + match opcode { + "OP_0" => Some("00".to_string()), + "OP_1" => Some("51".to_string()), + "OP_2" => Some("52".to_string()), + "OP_3" => Some("53".to_string()), + "OP_4" => Some("54".to_string()), + "OP_5" => Some("55".to_string()), + "OP_6" => Some("56".to_string()), + "OP_7" => Some("57".to_string()), + "OP_8" => Some("58".to_string()), + "OP_9" => Some("59".to_string()), + "OP_10" => Some("5a".to_string()), + "OP_11" => Some("5b".to_string()), + "OP_12" => Some("5c".to_string()), + "OP_13" => Some("5d".to_string()), + "OP_14" => Some("5e".to_string()), + "OP_15" => Some("5f".to_string()), + "OP_16" => Some("60".to_string()), + "OP_RETURN" => Some("6a".to_string()), + "OP_DUP" => Some("76".to_string()), + "OP_EQUAL" => Some("87".to_string()), + "OP_EQUALVERIFY" => Some("88".to_string()), + "OP_CHECKSIG" => Some("ac".to_string()), + "OP_CHECKMULTISIG" => Some("ae".to_string()), + "OP_HASH160" => Some("a9".to_string()), + _ => None, + } + } + + pub fn is_opcode(s: &str) -> bool { + s.starts_with("OP_") + } + + pub fn execute(opcode: &str, mut stack: Vec) -> Result, String> { + match opcode { + "OP_DUP" => { + if stack.is_empty() { + return Err("Stack is empty, cannot duplicate".to_string()); + } + let top = stack.last().unwrap().clone(); + stack.push(top); + Ok(stack) + } + "OP_HASH160" => { + if stack.is_empty() { + return Err("Stack is empty, cannot hash160".to_string()); + } + let top = stack.pop().unwrap(); + let hashed = hash160(&top); + stack.push(hashed); + Ok(stack) + } + "OP_EQUAL" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_EQUAL".to_string()); + } + let a = stack.pop().unwrap(); + let b = stack.pop().unwrap(); + if a == b { + stack.push("OP_TRUE".to_string()); + Ok(stack) + } else { + Err(format!("Items not equal: {} != {}", a, b)) + } + } + "OP_EQUALVERIFY" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_EQUALVERIFY".to_string()); + } + let a = stack.pop().unwrap(); + let b = stack.pop().unwrap(); + if a == b { + Ok(stack) + } else { + Err(format!("Items not equal: {} != {}", a, b)) + } + } + "OP_CHECKSIG" => { + if stack.len() < 2 { + return Err("Not enough items on stack for OP_CHECKSIG".to_string()); + } + let _pubkey = stack.pop().unwrap(); + let _signature = stack.pop().unwrap(); + stack.push("OP_TRUE".to_string()); + Ok(stack) + } + "OP_CHECKMULTISIG" => { + if stack.is_empty() { + return Err("Stack empty for OP_CHECKMULTISIG".to_string()); + } + let m_str = stack.pop().unwrap(); + let m = m_str.strip_prefix("OP_") + .and_then(|s| s.parse::().ok()) + .ok_or("Invalid m value")?; + + if stack.len() < m { + return Err("Not enough signatures on stack".to_string()); + } + for _ in 0..m { + stack.pop(); + } + + if stack.is_empty() { + return Err("Stack empty getting n value".to_string()); + } + let n_str = stack.pop().unwrap(); + let n = n_str.strip_prefix("OP_") + .and_then(|s| s.parse::().ok()) + .ok_or("Invalid n value")?; + + if stack.len() < n + 1 { + return Err("Not enough public keys on stack".to_string()); + } + for _ in 0..n + 1 { + stack.pop(); + } + + stack.push("OP_TRUE".to_string()); + Ok(stack) + } + "OP_RETURN" => { + Err("Script is invalid. (OP_RETURN always invalidates a script.)".to_string()) + } + _ => Ok(stack), + } + } +} \ No newline at end of file diff --git a/submissions/bitcheck/src/script_impl.rs b/submissions/bitcheck/src/script_impl.rs new file mode 100644 index 0000000..fbbd3ce --- /dev/null +++ b/submissions/bitcheck/src/script_impl.rs @@ -0,0 +1,175 @@ +use crate::script_type::ScriptType; +use crate::opcodes::Opcodes; + +#[derive(Debug, Clone)] +pub struct Script { + pub asm: String, + pub hex: String, + pub script_type: ScriptType, +} + +impl Script { + pub fn new(data: &str) -> Self { + let (hex, asm) = if data.chars().all(|c| c.is_ascii_hexdigit() || c.is_whitespace()) + && !data.contains(' ') { + let h = data.to_string(); + let a = Self::hex_to_asm(&h); + (h, a) + } else { + let a = data.to_string(); + let h = Self::asm_to_hex(&a); + (h, a) + }; + + let script_type = Self::get_type(&asm); + + Script { + asm, + hex, + script_type, + } + } + + pub fn hex_to_asm(hex: &str) -> String { + let mut asm = Vec::new(); + let bytes: Vec<&str> = hex.as_bytes() + .chunks(2) + .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .collect(); + + let mut i = 0; + while i < bytes.len() { + let byte = bytes[i]; + let int_val = u8::from_str_radix(byte, 16).unwrap(); + + if int_val > 0 && int_val < 0x4b { + let data_len = int_val as usize; + i += 1; + let data: String = bytes[i..i + data_len].join(""); + asm.push(data); + i += data_len; + } else { + asm.push(Opcodes::get_opcode(byte)); + i += 1; + } + } + + asm.join(" ") + } + + pub fn asm_to_hex(asm: &str) -> String { + let mut hex = String::new(); + let pieces: Vec<&str> = asm.split_whitespace().collect(); + + for piece in pieces { + if let Some(opcode_hex) = Opcodes::get_hex(piece) { + hex.push_str(&opcode_hex); + } else { + let push = format!("{:02x}", piece.len() / 2); + hex.push_str(&push); + hex.push_str(piece); + } + } + + hex + } + + pub fn get_type(asm: &str) -> ScriptType { + let parts: Vec<&str> = asm.split_whitespace().collect(); + + if parts.len() == 2 && parts[1] == "OP_CHECKSIG" { + ScriptType::P2PK + } else if parts.len() == 5 + && parts[0] == "OP_DUP" + && parts[1] == "OP_HASH160" + && parts[3] == "OP_EQUALVERIFY" + && parts[4] == "OP_CHECKSIG" { + ScriptType::P2PKH + } else if parts.len() == 3 + && parts[0] == "OP_HASH160" + && parts[2] == "OP_EQUAL" { + ScriptType::P2SH + } else if parts.len() >= 3 + && parts[0].starts_with("OP_") + && parts[parts.len() - 2].starts_with("OP_") + && parts[parts.len() - 1] == "OP_CHECKMULTISIG" { + ScriptType::P2MS + } else if parts.len() == 2 && parts[0] == "OP_RETURN" { + ScriptType::Return + } else { + ScriptType::Unknown + } + } + + pub fn run(scripts: &[Script]) -> Result, String> { + + if scripts.len() > 1 && scripts[1].script_type == ScriptType::P2SH { + return Self::run_p2sh(scripts); + } + + let mut script: Vec = scripts + .iter() + .flat_map(|s| s.asm.split_whitespace().map(|x| x.to_string())) + .collect(); + + let mut stack: Vec = Vec::new(); + + while !script.is_empty() { + let opcode = script.remove(0); + + if Opcodes::is_opcode(&opcode) { + stack = Opcodes::execute(&opcode, stack)?; + } else { + stack.push(opcode); + } + } + + Ok(stack) + } + + pub fn run_p2sh(scripts: &[Script]) -> Result, String> { + let unlocking = &scripts[0]; + let locking = &scripts[1]; + + let mut stack = Self::run(&[unlocking.clone()])?; + let stack_copy = stack.clone(); + + let combined = Script::new(&format!("{}{}", unlocking.hex, locking.hex)); + stack = Self::run(&[combined])?; + + if Self::validate(&stack) { + let mut stack_copy = stack_copy; + let redeem_hex = stack_copy.pop().ok_or("No redeem script on stack")?; + let redeem_script = Script::new(&redeem_hex); + + let combined2 = Script::new(&format!("{}{}", unlocking.hex, redeem_script.hex)); + let stack2 = Self::run(&[combined2])?; + + return Ok(stack2); + } + + Ok(stack) + } + + pub fn validate(stack: &[String]) -> bool { + if stack.is_empty() { + return false; + } + + let top = &stack[stack.len() - 1]; + + if top == "OP_TRUE" { + return true; + } + + if top.starts_with("OP_") { + if let Some(num_str) = top.strip_prefix("OP_") { + if let Ok(num) = num_str.parse::() { + return num > 0; + } + } + } + + false + } +} \ No newline at end of file diff --git a/submissions/bitcheck/src/script_type.rs b/submissions/bitcheck/src/script_type.rs new file mode 100644 index 0000000..2f13745 --- /dev/null +++ b/submissions/bitcheck/src/script_type.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Clone, PartialEq)] +pub enum ScriptType { + P2PK, + P2PKH, + P2SH, + P2MS, + Return, + Unknown, +} \ No newline at end of file