diff --git a/Cargo.lock b/Cargo.lock index bb53483..9c7fb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,18 +14,66 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1 0.29.1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + [[package]] name = "bitcoin-io" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", +] + [[package]] name = "bitcoin_hashes" version = "0.14.1" @@ -346,6 +394,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hmac" version = "0.12.1" @@ -783,6 +837,7 @@ dependencies = [ name = "programming_bitcoin" version = "0.1.0" dependencies = [ + "bitcoin", "bs58", "dotenvy", "hex", @@ -791,7 +846,7 @@ dependencies = [ "rand 0.8.5", "reqwest", "ripemd", - "secp256k1", + "secp256k1 0.31.1", "serde", "serde_json", "sha2", @@ -979,6 +1034,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys 0.10.1", +] + [[package]] name = "secp256k1" version = "0.31.1" @@ -987,7 +1052,16 @@ checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" dependencies = [ "bitcoin_hashes", "rand 0.9.2", - "secp256k1-sys", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e1448c2..0f22631 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ serde_json = "1.0.149" serde = { version = "1.0", features = ["derive"] } reqwest = "0.11" tokio = { version = "1", features = ["full"] } +bitcoin = "0.32.8" diff --git a/README.md b/README.md index d7a1201..3a42805 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,33 @@ This repository contains a Rust implementation of the exercises and concepts fro The code is organized by chapters, mirroring the book's progression: -- `src/ch01/` - Finite Fields +- `src/ch01_finite_fields/` - Finite Fields - Basic field arithmetic operations - FieldElement struct with addition, subtraction, multiplication, division, and exponentiation -- `src/ch02/` - Elliptic Curves over Finite Fields +- `src/ch02_elliptic_curves/` - Elliptic Curves over Finite Fields - Point operations on elliptic curves - Point addition and scalar multiplication -- `src/ch03/` - Elliptic Curves over secp256k1 +- `src/ch03_ecc/` - Elliptic Curves over secp256k1 - Implementation of the secp256k1 curve used in Bitcoin + - Basically elliptic curve cryptography, as the name states - Large number handling with BigUint/BigInt for cryptographic operations -- `src/ch04/` - Serialization +- `src/ch04_serialization/` - Serialization - Serialization of secp256k1 points and signatures - SEC (Standards for Efficient Cryptography) format implementation -- `src/ch05/` - Transactions +- `src/ch05_transactions/` - Transactions - Bitcoin transaction parsing and structure - Variable-length integer encoding/decoding - Transaction input/output handling +- `src/cho6_script` - Script + - Bitcoin's smart contract language Script + - Implements parsing, serialization and evaluation of scripts from ScriptPubkeys and ScriptSigs + - Implements opcodes used to evaluate scripts + - `src/lib.rs` - Library entry point - `src/main.rs` - Executable entry point - `tests/` - Integration tests for each chapter diff --git a/src/ch04_serialization/ser_private_key.rs b/src/ch04_serialization/ser_private_key.rs index 621c4a9..8e8fffd 100644 --- a/src/ch04_serialization/ser_private_key.rs +++ b/src/ch04_serialization/ser_private_key.rs @@ -2,7 +2,6 @@ use crate::ch04_serialization::ser_s256_field::S256Field; use crate::ch04_serialization::{ser_s256_point, ser_signature::Signature}; use hmac::{Hmac, Mac}; use num_bigint::{BigUint, ToBigUint}; -use rand::{RngCore, rngs::OsRng}; use secp256k1::constants::{CURVE_ORDER, FIELD_SIZE}; use ser_s256_point::S256Point; use sha2::{Digest, Sha256}; @@ -12,9 +11,12 @@ type HmacSha256 = Hmac; #[derive(Debug, Clone, Default)] pub struct PrivateKey { pub secret_bytes: S256Field, - pub point: S256Point, + pub public_key: PublicKey, } +#[derive(Debug, Clone, Default)] +pub struct PublicKey(pub S256Point); + impl PrivateKey { pub fn new(secret: &str) -> Self { let bytes_secret = secret.as_bytes(); @@ -27,7 +29,7 @@ impl PrivateKey { PrivateKey { secret_bytes: felt, - point, + public_key: PublicKey(point), } } @@ -41,15 +43,14 @@ impl PrivateKey { pub fn sign(self, z: S256Field) -> Result { let big_n = BigUint::from_bytes_be(&FIELD_SIZE); - let mut k_bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut k_bytes); - let k = Self::deterministic_k(&self, z.clone()); let r = S256Point::generate_point(k.clone().element).x.unwrap(); let k_inv = k.inv().unwrap(); let mut s = (z + r.clone() * self.secret_bytes) * k_inv; + // Normalise s to the lower half of the curve order to ensure signature + // malleability is not possible (both s and n-s are valid, low-s is canonical). if s.element > &big_n / 2.to_biguint().unwrap() { s = S256Field::new(big_n - s.element); } diff --git a/src/ch04_serialization/ser_s256_point.rs b/src/ch04_serialization/ser_s256_point.rs index 35386bf..fdbaf57 100644 --- a/src/ch04_serialization/ser_s256_point.rs +++ b/src/ch04_serialization/ser_s256_point.rs @@ -1,5 +1,6 @@ use num_bigint::{BigUint, ToBigInt, ToBigUint}; -use secp256k1::constants::{FIELD_SIZE, GENERATOR_X, GENERATOR_Y}; +use secp256k1::constants::{CURVE_ORDER, FIELD_SIZE, GENERATOR_X, GENERATOR_Y}; +use sha2::Sha256; use crate::ch04_serialization::ser_private_key::PrivateKey; use crate::ch04_serialization::ser_s256_field::{S256Field, ToS256Field}; @@ -227,17 +228,42 @@ impl S256Point { generator.scalar_mult(scalar) } + // pub fn verify_sig(&self, z: S256Field, sig: Signature) -> Result { + // // BUG: S256Field always uses FIELD_SIZE (the field prime p) as its modulus. + // // ECDSA requires r, s, and z to be computed modulo the curve ORDER n (CURVE_ORDER). + // // These are different constants. Until S256Field is split into a separate + // // "scalar field" type that uses CURVE_ORDER, verify_sig will produce wrong results + // // for signatures generated by sign(). See ch03 for the correct approach. + // let u = z / sig.s.clone(); + // let v = sig.r.clone() / sig.s.clone(); + + // let generator = Self::generator(); + // let total = (generator.scalar_mult(u.element) + self.scalar_mult(v.element))?; + + // Ok(total.x.unwrap().element == sig.r.element) + // } + pub fn verify_sig(&self, z: S256Field, sig: Signature) -> Result { - let u = z / sig.s.clone(); - let v = sig.r.clone() / sig.s.clone(); + // ECDSA verification: all arithmetic must be done modulo CURVE_ORDER (n), not FIELD_SIZE (p) + let n = BigUint::from_bytes_be(&CURVE_ORDER); + + // Convert to modulo n arithmetic + let s_inv = sig + .s + .element + .modinv(&n) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "s has no inverse mod n"))?; + + let u = (&z.element * &s_inv) % &n; + let v = (&sig.r.element * &s_inv) % &n; let generator = Self::generator(); - let total = (generator.scalar_mult(u.element) + self.scalar_mult(v.element))?; + let total = (generator.scalar_mult(u) + self.scalar_mult(v))?; Ok(total.x.unwrap().element == sig.r.element) } - pub fn sec(&self, compressed: bool) -> Vec { + pub fn to_sec(&self, compressed: bool) -> Vec { let x = self.x.as_ref().unwrap(); let y = self.y.as_ref().unwrap(); @@ -268,7 +294,7 @@ impl S256Point { } } - pub fn parse(&self, sec_bin: Vec) -> Self { + pub fn parse(sec_bin: Vec) -> Self { // returns a Point object from a SEC binary (not hex) let p = S256Field::from_bytes(&FIELD_SIZE); if sec_bin[0] == 4 { @@ -301,15 +327,24 @@ impl S256Point { } } + pub fn pubkey_from_ser(sec_bin: Vec) -> Self { + Self::parse(sec_bin) + } + + // TODO: Write test cases to cover this. I forgot to do the sha256 before the Ripemd160 fn hash160(s: &[u8]) -> Vec { - let mut hasher = Ripemd160::new(); + let mut hasher = Sha256::new(); hasher.update(s); + let hash1 = hasher.finalize(); + + let mut hasher = Ripemd160::new(); + hasher.update(hash1); hasher.finalize().to_vec() } fn point_hash160(&self, compressed: bool) -> Vec { - Self::hash160(&self.sec(compressed)) + Self::hash160(&self.to_sec(compressed)) } pub fn address(&self, compressed: bool, testnet: bool) -> String { diff --git a/src/ch04_serialization/ser_signature.rs b/src/ch04_serialization/ser_signature.rs index 141c8fe..2f0f04b 100644 --- a/src/ch04_serialization/ser_signature.rs +++ b/src/ch04_serialization/ser_signature.rs @@ -1,4 +1,9 @@ -use std::fmt; +use std::{ + fmt, + io::{Error, ErrorKind}, +}; + +use num_bigint::BigUint; use crate::ser_s256_field::S256Field; @@ -19,14 +24,18 @@ impl Signature { Signature { r, s } } - pub fn der(&self) -> Vec { + pub fn to_der(&self) -> Vec { + // to_bytes() returns big-endian; strip any leading 0x00 that BigUint may emit let mut r_bin = self.r.to_bytes(); r_bin = r_bin.strip_prefix(&[0_u8]).unwrap_or(&r_bin).to_vec(); + // DER integers are signed: if the high bit is set, prepend 0x00 so it isn't + // misread as a negative number. if r_bin[0] & 0x80 != 0 { - r_bin.to_vec().insert(0, 0); + r_bin.insert(0, 0); } + // 0x02 = INTEGER tag, followed by length, then the bytes let mut result = vec![2, r_bin.len() as u8]; result.extend_from_slice(&r_bin); @@ -40,9 +49,111 @@ impl Signature { result.extend_from_slice(&[2, s_bin.len() as u8]); result.extend_from_slice(&s_bin); + // 0x30 = SEQUENCE tag; der[1] is the total payload length (not including these 2 bytes) let mut der = vec![0x30, result.len() as u8]; der.extend_from_slice(&result); der } + + pub fn from_der(der: &[u8]) -> Result { + if der.len() < 8 { + return Err(Error::new(ErrorKind::InvalidData, "DER is too short")); + } + + // Expect sequence tag 0x30 + if der[0] != 0x30 { + return Err(Error::new( + ErrorKind::InvalidData, + "DER does not start with sequence tag 30", + )); + } + + // check signature length + let total_len = der[1] as usize; + if total_len != der.len().saturating_sub(2) { + return Err(Error::new( + ErrorKind::InvalidData, + format!( + "DER length mismatch: total_len {} != payload {}", + total_len, + der.len() - 2 + ), + )); + } + + // parse r + let mut index = 2; + if index >= der.len() || der[index] != 0x02 { + return Err(Error::new( + ErrorKind::InvalidData, + "Expected INTEGER (0x02) for r", + )); + } + index += 1; + + if index >= der.len() { + return Err(Error::new( + ErrorKind::InvalidData, + "Unexpected end parsing r length", + )); + } + + let r_len = der[index] as usize; + index += 1; + + if index + r_len > der.len() { + return Err(Error::new( + ErrorKind::InvalidData, + "r length exceeds DER size", + )); + } + + let r_bin = &der[index..index + r_len]; + index += r_len; + + // parse s now + if index >= der.len() || der[index] != 0x02 { + return Err(Error::new( + ErrorKind::InvalidData, + "Expected INTEGER (0x02) for s", + )); + } + index += 1; + + if index >= der.len() { + return Err(Error::new( + ErrorKind::InvalidData, + "Unexpected end parsing s length", + )); + } + + let s_len = der[index] as usize; + index += 1; + + if index + s_len > der.len() { + return Err(Error::new( + ErrorKind::InvalidData, + "s length exceeds DER size", + )); + } + + let s_bin = &der[index..index + s_len]; + index += s_len; + + if index != der.len() { + return Err(Error::new( + ErrorKind::InvalidData, + "Extra data after DER signature", + )); + } + + let r_big = BigUint::from_bytes_be(r_bin); + let s_big = BigUint::from_bytes_be(s_bin); + + let r_field = S256Field::new(r_big); + let s_field = S256Field::new(s_big); + + Ok(Self::new(r_field, s_field)) + } } diff --git a/src/ch05_transactions/transaction.rs b/src/ch05_transactions/transaction.rs index 5d3e9ce..d4317e8 100644 --- a/src/ch05_transactions/transaction.rs +++ b/src/ch05_transactions/transaction.rs @@ -3,6 +3,7 @@ use serde::ser::SerializeStruct; use sha2::{Digest, Sha256}; use crate::{ + decode_varint, encode_varint, tx_input::{TxId, TxIn}, tx_output::TxOut, }; @@ -33,51 +34,6 @@ impl Serialize for Transaction { } } -// Reads the number, and then the index from where to begin the next read -pub fn decode_varint(data: &[u8], index: usize) -> (u64, usize) { - let i = data[index]; - - match i { - 0xfd => { - let start = index + 1; - let to_read = data[start..=start + 1].try_into().unwrap(); - (u16::from_le_bytes(to_read) as u64, start + 2) - } - 0xfe => { - let start = index + 1; - let to_read = data[start..=start + 3].try_into().unwrap(); - (u32::from_le_bytes(to_read) as u64, start + 4) - } - 0xff => { - let start = index + 1; - let to_read = data[start..=start + 7].try_into().unwrap(); - (u64::from_le_bytes(to_read), start + 8) - } - _ => (i as u64, index + 1), - } -} - -pub fn encode_varint(number: u64) -> Vec { - match number { - 0..=0xfc => (number as u8).to_le_bytes().to_vec(), - 0xfd..=0xFFFF => { - let mut bytes = vec![0xfd]; - bytes.extend_from_slice(&(number as u16).to_le_bytes()); - bytes - } - 0x10000..=0xFFFFFFFF => { - let mut bytes = vec![0xfe]; - bytes.extend_from_slice(&(number as u32).to_le_bytes()); - bytes - } - _ => { - let mut bytes = vec![0xff]; - bytes.extend_from_slice(&number.to_le_bytes()); - bytes - } - } -} - impl Transaction { pub fn new( version: u32, @@ -111,12 +67,14 @@ impl Transaction { pub fn parse(serialization: &[u8]) -> Transaction { let mut index = 0; + // Version is 4 bytes, little-endian let version_bytes: [u8; 4] = serialization[index..index + 4].try_into().unwrap(); let version = u32::from_le_bytes(version_bytes); // Check out the stream thing on page 115 index += 4; + // decode_varint returns (value, new_absolute_index) — assign with = not += let (input_count, new_index) = decode_varint(serialization, index); index = new_index; @@ -124,6 +82,8 @@ impl Transaction { let mut inputs = Vec::new(); for _ in 0..input_count { + // TxIn::parse returns (TxIn, displacement) where displacement is bytes consumed, + // NOT an absolute index — so use += here, not = let (input, displacement) = TxIn::parse(serialization, index); inputs.push(input); index += displacement; @@ -135,6 +95,7 @@ impl Transaction { let mut outputs = Vec::new(); for _ in 0..output_count { + // TxOut::parse returns (TxOut, new_absolute_index) — assign with = not += let (output, new_index) = TxOut::parse(serialization, index); outputs.push(output); index = new_index diff --git a/src/ch05_transactions/tx_input.rs b/src/ch05_transactions/tx_input.rs index 8af0608..e9256d7 100644 --- a/src/ch05_transactions/tx_input.rs +++ b/src/ch05_transactions/tx_input.rs @@ -3,10 +3,7 @@ use std::io::Error; use serde::{Serialize, Serializer, ser::SerializeStruct}; use sha2::{Digest, Sha256}; -use crate::{ - transaction::{Transaction, decode_varint, encode_varint}, - tx_fetcher::TxFetcher, -}; +use crate::{decode_varint, encode_varint, transaction::Transaction, tx_fetcher::TxFetcher}; #[derive(Debug, Clone, Copy)] pub struct TxId(pub [u8; 32]); @@ -67,6 +64,7 @@ impl TxIn { let start_index = index; let (script_len, new_index) = decode_varint(data, index); + // decode_varint returns an absolute index; compute how many bytes the varint itself took let varint_size = new_index - start_index; index = new_index; displacement += varint_size; diff --git a/src/ch05_transactions/tx_output.rs b/src/ch05_transactions/tx_output.rs index e0c0d55..38d0c81 100644 --- a/src/ch05_transactions/tx_output.rs +++ b/src/ch05_transactions/tx_output.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use crate::transaction::{decode_varint, encode_varint}; +use crate::{decode_varint, encode_varint}; #[derive(Debug, Clone, Serialize)] pub struct TxOut { diff --git a/src/ch06_script/mod.rs b/src/ch06_script/mod.rs new file mode 100644 index 0000000..0f90240 --- /dev/null +++ b/src/ch06_script/mod.rs @@ -0,0 +1,2 @@ +pub mod opcodes; +pub mod script; diff --git a/src/ch06_script/opcodes.rs b/src/ch06_script/opcodes.rs new file mode 100644 index 0000000..23d076f --- /dev/null +++ b/src/ch06_script/opcodes.rs @@ -0,0 +1,484 @@ +use std::collections::{HashMap, VecDeque}; + +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; + +use crate::{ + script::Cmd, ser_s256_field::S256Field, ser_s256_point::S256Point, ser_signature::Signature, +}; + +/* +stack +altstack +remaining cmds +z (signature hash) +*/ +type OpFn = fn(&mut Vec, &mut Vec, &mut VecDeque, &[u8]) -> bool; + +#[derive(Debug, Clone)] +pub struct OpCodes { + // pub element: Vec, +} + +#[derive(Debug, Clone)] +pub struct Element(pub Vec); + +impl OpCodes { + pub fn encode_num(num: i64) -> Element { + if num == 0 { + return Element(vec![]); + } + + let mut absolute_num = num.abs(); + let is_negative = num < 0; + + let mut result = Vec::new(); + + while absolute_num > 0 { + result.push((absolute_num & 0xff) as u8); + absolute_num >>= 8 + } + + if (result[result.len() - 1] & 0x80) > 0 { + if is_negative { + result.push(0x80); + } else { + result.push(0); + } + } else if is_negative { + let last = result.last_mut().unwrap(); + *last |= 0x80; + } + + Element(result) + } + + pub fn decode_num(element: Element) -> i64 { + if element.0.is_empty() { + return 0; + } + + let mut result: i64; + let is_negative; + + let mut cloned = element.0.clone(); + cloned.reverse(); + + if cloned[0] & 0x80 != 0 { + is_negative = true; + result = (cloned[0] & 0x7f) as i64; + } else { + is_negative = false; + result = cloned[0] as i64; + } + + for c in &cloned[1..] { + result <<= 8; + result += *c as i64; + } + + if is_negative { -result } else { result } + } + + pub fn op_0( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + stack.push(Self::encode_num(0)); + true + } + + pub fn op_1negate( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + stack.push(Self::encode_num(-1)); + true + } + + pub fn op_1( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(1)); + true + } + pub fn op_2( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(2)); + true + } + pub fn op_3( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(3)); + true + } + pub fn op_4( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(4)); + true + } + pub fn op_5( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(5)); + true + } + pub fn op_6( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(6)); + true + } + pub fn op_7( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(7)); + true + } + pub fn op_8( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(8)); + true + } + pub fn op_9( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(9)); + true + } + pub fn op_10( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(10)); + true + } + pub fn op_11( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(11)); + true + } + pub fn op_12( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(12)); + true + } + pub fn op_13( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(13)); + true + } + pub fn op_14( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(14)); + true + } + pub fn op_15( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(15)); + true + } + pub fn op_16( + stack: &mut Vec, + _: &mut Vec, + _: &mut VecDeque, + _: &[u8], + ) -> bool { + stack.push(Self::encode_num(16)); + true + } + + pub fn op_dup( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.is_empty() { + return false; + } + + let top = stack.pop().unwrap(); + stack.push(top.clone()); + stack.push(top); + true + } + + pub fn op_hash256( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.is_empty() { + return false; + } + + let element = stack.pop().unwrap(); + let mut hasher = Sha256::new(); + hasher.update(&element.0); + let hash1 = hasher.finalize(); + + let mut hasher = Sha256::new(); + hasher.update(hash1); + let hash2: Vec = hasher.finalize().to_vec(); + + let new_element = Element(hash2); + stack.push(new_element); + true + } + + pub fn op_hash160( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.is_empty() { + return false; + } + + let element = stack.pop().unwrap(); + let mut hasher = Sha256::new(); + hasher.update(&element.0); + let hash1 = hasher.finalize(); + + let mut hasher = Ripemd160::new(); + hasher.update(hash1); + let hash2: Vec = hasher.finalize().to_vec(); + + let new_element = Element(hash2); + stack.push(new_element); + true + } + + pub fn op_checksig( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.len() < 2 { + return false; + } + + let signature_bytes = stack.pop().unwrap().0; + let pubkey_bytes = stack.pop().unwrap().0; + + if signature_bytes.is_empty() { + stack.push(Self::encode_num(0)); + return true; + } + + // op_checksig strips the last byte of the signature before DER-parsing it. + // In real Bitcoin transactions that byte is the sighash type (e.g. 0x01 = SIGHASH_ALL) + // and is not part of the DER encoding. This is correct behaviour. + let der_sig = &signature_bytes[0..(signature_bytes.len() - 1)]; + let signature = match Signature::from_der(der_sig) { + Ok(sig) => sig, + Err(_) => { + stack.push(Self::encode_num(0)); + return true; + } + }; + + let pubkey = S256Point::pubkey_from_ser(pubkey_bytes); + + let message = S256Field::from_bytes(_z); + // let signature = Signature::from_der(&signature_bytes).unwrap(); + + let is_valid = matches!(pubkey.verify_sig(message, signature), Ok(true)); + + let num = if is_valid { 1 } else { 0 }; + + stack.push(Self::encode_num(num)); + + true + } + + pub fn op_mul( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.len() < 2 { + return false; + } + + let top = Self::decode_num(stack.pop().unwrap()); + let next = Self::decode_num(stack.pop().unwrap()); + + let product = top * next; + + stack.push(Self::encode_num(product)); + + true + } + + pub fn op_add( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.len() < 2 { + return false; + } + + let top = Self::decode_num(stack.pop().unwrap()); + let next = Self::decode_num(stack.pop().unwrap()); + + let sum = top + next; + + stack.push(Self::encode_num(sum)); + + true + } + + pub fn op_equal( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.len() < 2 { + return false; + } + + let top = stack.pop().unwrap(); + let next = stack.pop().unwrap(); + + let result = if top.0 == next.0 { 1 } else { 0 }; + + stack.push(Self::encode_num(result)); + + true + } + + pub fn op_verify( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if stack.is_empty() { + return false; + } + + let element = stack.pop().unwrap(); + let value = Self::decode_num(element); + + if value == 0 { + return false; + } + + true + } + + pub fn op_equal_verify( + stack: &mut Vec, + _altstack: &mut Vec, + _cmds: &mut VecDeque, + _z: &[u8], + ) -> bool { + if !Self::op_equal(stack, _altstack, _cmds, _z) { + return false; + } + + // let res = stack.pop().unwrap(); + // Self::decode_num(res) != 0 + Self::op_verify(stack, _altstack, _cmds, _z) + } +} + +pub fn opcode_functions() -> HashMap { + let mut map = HashMap::new(); + + map.insert(0, OpCodes::op_0 as OpFn); + map.insert(79, OpCodes::op_1negate as OpFn); + map.insert(81, OpCodes::op_1 as OpFn); + map.insert(82, OpCodes::op_2 as OpFn); + map.insert(83, OpCodes::op_3 as OpFn); + map.insert(84, OpCodes::op_4 as OpFn); + map.insert(85, OpCodes::op_5 as OpFn); + map.insert(86, OpCodes::op_6 as OpFn); + map.insert(87, OpCodes::op_7 as OpFn); + map.insert(88, OpCodes::op_8 as OpFn); + map.insert(89, OpCodes::op_9 as OpFn); + map.insert(90, OpCodes::op_10 as OpFn); + map.insert(91, OpCodes::op_11 as OpFn); + map.insert(92, OpCodes::op_12 as OpFn); + map.insert(93, OpCodes::op_13 as OpFn); + map.insert(94, OpCodes::op_14 as OpFn); + map.insert(95, OpCodes::op_15 as OpFn); + map.insert(96, OpCodes::op_16 as OpFn); + map.insert(118, OpCodes::op_dup as OpFn); + map.insert(169, OpCodes::op_hash160 as OpFn); + map.insert(170, OpCodes::op_hash256 as OpFn); + map.insert(172, OpCodes::op_checksig as OpFn); + map.insert(149, OpCodes::op_mul as OpFn); + map.insert(147, OpCodes::op_add as OpFn); + map.insert(135, OpCodes::op_equal as OpFn); + map.insert(105, OpCodes::op_verify as OpFn); + map.insert(136, OpCodes::op_equal_verify as OpFn); + + map +} diff --git a/src/ch06_script/script.rs b/src/ch06_script/script.rs new file mode 100644 index 0000000..e7ccd2a --- /dev/null +++ b/src/ch06_script/script.rs @@ -0,0 +1,181 @@ +use std::{collections::VecDeque, io::Error, ops::Add}; + +use crate::{ + decode_varint, encode_varint, + opcodes::{Element, opcode_functions}, +}; + +#[derive(Debug, Clone)] +pub struct Script { + pub commands: Vec, +} + +#[derive(Debug, Clone)] +pub enum Cmd { + OpCode(u8), + Data(Vec), + // OtherCodes(u8), +} + +impl Add for Script { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + let mut commands = self.commands; + commands.extend_from_slice(&rhs.commands); + + Script { commands } + } +} + +impl Script { + pub fn new(commands: Vec) -> Self { + Self { commands } + } + + pub fn parse(ser: &[u8]) -> Result { + // ser[0] (or more bytes) is a varint giving the total byte length of the script body. + // varint_size is the number of bytes the varint itself occupies (1, 3, 5, or 9). + // end = varint_size + length, NOT just length — the loop must account for the offset. + let (length, varint_size) = decode_varint(ser, 0); + let mut commands = Vec::new(); + let mut index: usize = varint_size; + let end = varint_size + length as usize; + + while index < end { + // let current = &ser[index]; + + let current_byte = ser[index]; + index += 1; + match current_byte { + // 0x01–0x4b: the byte itself is the number of data bytes to push + 1..=75 => { + let n = current_byte as usize; + // commands.extend_from_slice(&ser[index..index+n]); + commands.push(Cmd::Data((ser[index..index + n]).to_vec())); + index += n; + } + // 0x4c = OP_PUSHDATA1: next 1 byte is the data length + 76 => { + // let (data_length, _) = decode_varint(ser, index); + let data_length = ser[index] as usize; + index += 1; + // commands.extend_from_slice(&ser[index..index + data_length as usize]); + // commands.push(Cmd::OpCode((ser[index..index + data_length]).to_vec())); + let data = ser[index..index + data_length].to_vec(); + commands.push(Cmd::Data(data)); + index += data_length; + } + // 0x4d = OP_PUSHDATA2: next 2 bytes (little-endian) are the data length + 77 => { + let data_length = u16::from_le_bytes(ser[index..index + 2].try_into().unwrap()); + index += 2; + // commands.push(Cmd::OpCode( + // ser[index..index + data_length as usize].to_vec(), + // )); + let data = ser[index..index + data_length as usize].to_vec(); + commands.push(Cmd::Data(data)); + + index += data_length as usize + } + // Everything else is an opcode + op_code => { + println!("Pushing Operation"); + commands.push(Cmd::OpCode(op_code)); + } + }; + } + if index != end { + return Err(Error::new( + std::io::ErrorKind::InvalidData, + "Parsing script failed", + )); + } + + Ok(Script { commands }) + } + + fn raw_serialize(&self) -> Vec { + let mut result = Vec::new(); + + for command in &self.commands { + match command { + Cmd::OpCode(op) => { + result.push(*op); + } + Cmd::Data(data) => { + let len = data.len(); + + if len <= 75 { + result.push(len as u8); + } else if len < 0x100 { + result.push(76); // OP_PUSHDATA1 + result.push(len as u8); + } else if len <= 520 { + result.push(77); + result.extend_from_slice(&(len as u16).to_le_bytes()); + } else { + panic!("pushdata too long"); + } + + result.extend_from_slice(data); + } + } + } + + result + } + + pub fn serialize(&self) -> Vec { + let mut ser = Vec::new(); + let result = self.raw_serialize(); + let total = result.len() as u64; + + ser.extend_from_slice(&encode_varint(total)); + ser.extend_from_slice(&result); + + ser + } + + pub fn evaluate(&self, z: &[u8]) -> Result { + let mut commands: VecDeque = VecDeque::from(self.commands.clone()); + let mut stack: Vec = Vec::new(); + let mut altstack: Vec = Vec::new(); + + let opcodes = opcode_functions(); + + while let Some(cmd) = commands.pop_front() { + match cmd { + Cmd::Data(bytes) => { + stack.push(Element(bytes)); + } + + Cmd::OpCode(opcode) => { + let op_fn = match opcodes.get(&opcode) { + Some(f) => *f, + None => { + return Ok(false); + } + }; + + let ok = op_fn(&mut stack, &mut altstack, &mut commands, z); + + if !ok { + return Ok(false); + } + } + } + } + + if stack.is_empty() { + return Ok(false); + } + + let top = stack.pop().unwrap(); + + if top.0.is_empty() { + Ok(false) + } else { + Ok(true) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 101de54..1c6ae89 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{error::Error, io::Error as IoError}; mod ch01_finite_fields; pub use ch01_finite_fields::*; @@ -15,16 +15,67 @@ pub use ch04_serialization::*; mod ch05_transactions; pub use ch05_transactions::*; -use crate::transaction::Transaction; +mod ch06_script; +pub use ch06_script::*; -pub fn decode(transaction_hex: &str) -> Result> { +use crate::{ch06_script::script::Script, transaction::Transaction}; + +pub fn decode_transaction(transaction_hex: &str) -> Result> { let tx_bytes = hex::decode(transaction_hex).map_err(|e| format!("Hex decode error: {}", e))?; - // let transaction = Transaction::consensus_decode(&mut tx_bytes.as_slice()); + Ok(serde_json::to_string_pretty(&Transaction::parse( &tx_bytes, ))?) - // serde_json::to_string_pretty(&transaction) +} + +// Reads the number, and then the index from where to begin the next read +pub fn decode_varint(data: &[u8], index: usize) -> (u64, usize) { + let i = data[index]; + + match i { + 0xfd => { + let start = index + 1; + let to_read = data[start..=start + 1].try_into().unwrap(); + (u16::from_le_bytes(to_read) as u64, start + 2) + } + 0xfe => { + let start = index + 1; + let to_read = data[start..=start + 3].try_into().unwrap(); + (u32::from_le_bytes(to_read) as u64, start + 4) + } + 0xff => { + let start = index + 1; + let to_read = data[start..=start + 7].try_into().unwrap(); + (u64::from_le_bytes(to_read), start + 8) + } + _ => (i as u64, index + 1), + } +} + +pub fn encode_varint(number: u64) -> Vec { + match number { + 0..=0xfc => (number as u8).to_le_bytes().to_vec(), + 0xfd..=0xFFFF => { + let mut bytes = vec![0xfd]; + bytes.extend_from_slice(&(number as u16).to_le_bytes()); + bytes + } + 0x10000..=0xFFFFFFFF => { + let mut bytes = vec![0xfe]; + bytes.extend_from_slice(&(number as u32).to_le_bytes()); + bytes + } + _ => { + let mut bytes = vec![0xff]; + bytes.extend_from_slice(&number.to_le_bytes()); + bytes + } + } +} - // println!("Transaction: {}", json_transaction); - // Ok(()) +pub fn parse_opcodes(codes: Vec) -> Result { + // Send in the codes without length prefix + let mut prefixed = encode_varint(codes.len() as u64); + prefixed.extend_from_slice(&codes); + Script::parse(&prefixed) } diff --git a/src/main.rs b/src/main.rs index e9bd14e..0ca889f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,21 +3,40 @@ use std::env; use dotenvy::dotenv; // use programming_bitcoin::field_element::FieldElement; // use programming_bitcoin::s256_point::test_point; -use programming_bitcoin::{decode, ser_private_key::PrivateKey}; +use programming_bitcoin::{parse_opcodes, ser_private_key::PrivateKey}; fn main() { dotenv().ok(); let secret = env::var("PRIVATE_KEY_SECRET").expect("PRIVATE_KEY_SECRET must be set"); let pk = PrivateKey::new(&secret); - let pub_key = pk.point; + let pub_key = pk.public_key; - let test_addr = pub_key.address(false, true); + let test_addr = pub_key.0.address(false, true); println!("Compressed testnet address: {test_addr}"); - let tx = "010000000456919960ac691763688d3d3bcea9ad6ecaf875df5339e148a1fc61c6ed7a069e010000006a47304402204585bcdef85e6b1c6af5c2669d4830ff86e42dd205c0e089bc2a821657e951c002201024a10366077f87d6bce1f7100ad8cfa8a064b39d4e8fe4ea13a7b71aa8180f012102f0da57e85eec2934a82a585ea337ce2f4998b50ae699dd79f5880e253dafafb7feffffffeb8f51f4038dc17e6313cf831d4f02281c2a468bde0fafd37f1bf882729e7fd3000000006a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937feffffff567bf40595119d1bb8a3037c356efd56170b64cbcc160fb028fa10704b45d775000000006a47304402204c7c7818424c7f7911da6cddc59655a70af1cb5eaf17c69dadbfc74ffa0b662f02207599e08bc8023693ad4e9527dc42c34210f7a7d1d1ddfc8492b654a11e7620a0012102158b46fbdff65d0172b7989aec8850aa0dae49abfb84c81ae6e5b251a58ace5cfeffffffd63a5e6c16e620f86f375925b21cabaf736c779f88fd04dcad51d26690f7f345010000006a47304402200633ea0d3314bea0d95b3cd8dadb2ef79ea8331ffe1e61f762c0f6daea0fabde022029f23b3e9c30f080446150b23852028751635dcee2be669c2a1686a4b5edf304012103ffd6f4a67e94aba353a00882e563ff2722eb4cff0ad6006e86ee20dfe7520d55feffffff0251430f00000000001976a914ab0c0b2e98b1ab6dbf67d4750b0a56244948a87988ac005a6202000000001976a9143c82d7df364eb6c75be8c80df2b3eda8db57397088ac46430600"; + // let tx = "010000000456919960ac691763688d3d3bcea9ad6ecaf875df5339e148a1fc61c6ed7a069e010000006a47304402204585bcdef85e6b1c6af5c2669d4830ff86e42dd205c0e089bc2a821657e951c002201024a10366077f87d6bce1f7100ad8cfa8a064b39d4e8fe4ea13a7b71aa8180f012102f0da57e85eec2934a82a585ea337ce2f4998b50ae699dd79f5880e253dafafb7feffffffeb8f51f4038dc17e6313cf831d4f02281c2a468bde0fafd37f1bf882729e7fd3000000006a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937feffffff567bf40595119d1bb8a3037c356efd56170b64cbcc160fb028fa10704b45d775000000006a47304402204c7c7818424c7f7911da6cddc59655a70af1cb5eaf17c69dadbfc74ffa0b662f02207599e08bc8023693ad4e9527dc42c34210f7a7d1d1ddfc8492b654a11e7620a0012102158b46fbdff65d0172b7989aec8850aa0dae49abfb84c81ae6e5b251a58ace5cfeffffffd63a5e6c16e620f86f375925b21cabaf736c779f88fd04dcad51d26690f7f345010000006a47304402200633ea0d3314bea0d95b3cd8dadb2ef79ea8331ffe1e61f762c0f6daea0fabde022029f23b3e9c30f080446150b23852028751635dcee2be669c2a1686a4b5edf304012103ffd6f4a67e94aba353a00882e563ff2722eb4cff0ad6006e86ee20dfe7520d55feffffff0251430f00000000001976a914ab0c0b2e98b1ab6dbf67d4750b0a56244948a87988ac005a6202000000001976a9143c82d7df364eb6c75be8c80df2b3eda8db57397088ac46430600"; - let json = decode(tx).unwrap(); + // let json = decode(tx).unwrap(); - println!("{json}"); + // println!("{json}"); + + // let code = Opcode::from(0x76); + // println!("{:?}", code); + + // let codes_string = "6e879169a77ca787"; + // let codes_string = "767695935687"; + // let codes_string = "5455935987"; + // let codes_vec = hex::decode(codes_string).unwrap(); + let another_vec = vec![0x54, 0x55, 0x93, 0x59, 0x87]; + + let script = parse_opcodes(another_vec).unwrap(); + let z = vec![0_u8]; + let script_valid = script.evaluate(&z).unwrap(); + + let ser_script = hex::encode(script.serialize()); + + println!("{:?}", script); + println!("Script Valid: {}", script_valid); + println!("{}", ser_script); } diff --git a/tests/ch04_serialization_tests.rs b/tests/ch04_serialization_tests.rs index d645bf7..519d198 100644 --- a/tests/ch04_serialization_tests.rs +++ b/tests/ch04_serialization_tests.rs @@ -79,7 +79,7 @@ fn test_s256_field_sqrt() { #[test] fn test_sec_compressed_format() { let g = S256Point::generator(); - let sec = g.sec(true); + let sec = g.to_sec(true); // Compressed SEC should be 33 bytes assert_eq!(sec.len(), 33); @@ -91,7 +91,7 @@ fn test_sec_compressed_format() { #[test] fn test_sec_uncompressed_format() { let g = S256Point::generator(); - let sec = g.sec(false); + let sec = g.to_sec(false); // Uncompressed SEC should be 65 bytes assert_eq!(sec.len(), 65); @@ -103,8 +103,8 @@ fn test_sec_uncompressed_format() { #[test] fn test_sec_parse_uncompressed() { let g = S256Point::generator(); - let sec = g.sec(false); - let parsed = g.parse(sec); + let sec = g.to_sec(false); + let parsed = S256Point::parse(sec); // Parsed point should match original assert_eq!(parsed.x.unwrap().element, g.x.unwrap().element); @@ -114,8 +114,8 @@ fn test_sec_parse_uncompressed() { #[test] fn test_sec_parse_compressed() { let g = S256Point::generator(); - let sec = g.sec(true); - let parsed = g.parse(sec); + let sec = g.to_sec(true); + let parsed = S256Point::parse(sec); // Parsed point should match original assert_eq!(parsed.x.unwrap().element, g.x.unwrap().element); @@ -127,8 +127,8 @@ fn test_sec_round_trip_compressed() { let scalar = 12345_u64.to_biguint().unwrap(); let point = S256Point::generate_point(scalar); - let sec = point.sec(true); - let parsed = point.parse(sec); + let sec = point.to_sec(true); + let parsed = S256Point::parse(sec); assert_eq!(point.x.unwrap().element, parsed.x.unwrap().element); assert_eq!(point.y.unwrap().element, parsed.y.unwrap().element); @@ -139,8 +139,8 @@ fn test_sec_round_trip_uncompressed() { let scalar = 54321_u64.to_biguint().unwrap(); let point = S256Point::generate_point(scalar); - let sec = point.sec(false); - let parsed = point.parse(sec); + let sec = point.to_sec(false); + let parsed = S256Point::parse(sec); assert_eq!(point.x.unwrap().element, parsed.x.unwrap().element); assert_eq!(point.y.unwrap().element, parsed.y.unwrap().element); @@ -153,7 +153,7 @@ fn test_sec_even_y_coordinate() { let y_element = &g.y.as_ref().unwrap().element; let is_even = y_element % 2_u64.to_biguint().unwrap() == 0_u64.to_biguint().unwrap(); - let sec = g.sec(true); + let sec = g.to_sec(true); if is_even { assert_eq!(sec[0], 0x02); @@ -172,7 +172,7 @@ fn test_der_signature_format() { let s = S256Field::new(67890_u64.to_biguint().unwrap()); let sig = Signature::new(r, s); - let der = sig.der(); + let der = sig.to_der(); // DER should start with 0x30 assert_eq!(der[0], 0x30); @@ -187,7 +187,7 @@ fn test_der_signature_structure() { let s = S256Field::new(200_u64.to_biguint().unwrap()); let sig = Signature::new(r, s); - let der = sig.der(); + let der = sig.to_der(); // Check DER structure assert_eq!(der[0], 0x30); // SEQUENCE tag @@ -201,7 +201,7 @@ fn test_der_with_large_values() { let large_s = S256Field::new(BigUint::from_bytes_be(&FIELD_SIZE) / 3_u64.to_biguint().unwrap()); let sig = Signature::new(large_r, large_s); - let der = sig.der(); + let der = sig.to_der(); // Should produce valid DER encoding assert_eq!(der[0], 0x30); @@ -215,7 +215,7 @@ fn test_der_high_bit_padding() { let s = S256Field::new(0x90_u64.to_biguint().unwrap()); let sig = Signature::new(r, s); - let der = sig.der(); + let der = sig.to_der(); // DER should be valid assert_eq!(der[0], 0x30); @@ -333,7 +333,7 @@ fn test_wif_different_networks() { #[test] fn test_address_mainnet_compressed() { let pk = PrivateKey::new(SECRET); - let address = pk.point.address(true, false); + let address = pk.public_key.0.address(true, false); // Address should be a non-empty string assert!(!address.is_empty()); @@ -345,7 +345,7 @@ fn test_address_mainnet_compressed() { #[test] fn test_address_mainnet_uncompressed() { let pk = PrivateKey::new(SECRET); - let address = pk.point.address(false, false); + let address = pk.public_key.0.address(false, false); assert!(!address.is_empty()); } @@ -353,7 +353,7 @@ fn test_address_mainnet_uncompressed() { #[test] fn test_address_testnet_compressed() { let pk = PrivateKey::new(SECRET); - let address = pk.point.address(true, true); + let address = pk.public_key.0.address(true, true); assert!(!address.is_empty()); } @@ -361,7 +361,7 @@ fn test_address_testnet_compressed() { #[test] fn test_address_testnet_uncompressed() { let pk = PrivateKey::new(SECRET); - let address = pk.point.address(false, true); + let address = pk.public_key.0.address(false, true); assert!(!address.len() > 0); } @@ -370,8 +370,8 @@ fn test_address_testnet_uncompressed() { fn test_address_different_compression() { let pk = PrivateKey::new(SECRET); - let compressed = pk.point.address(true, false); - let uncompressed = pk.point.address(false, false); + let compressed = pk.public_key.0.address(true, false); + let uncompressed = pk.public_key.0.address(false, false); // Different compression should produce different addresses assert_ne!(compressed, uncompressed); @@ -381,8 +381,8 @@ fn test_address_different_compression() { fn test_address_different_networks() { let pk = PrivateKey::new(SECRET); - let mainnet = pk.point.address(true, false); - let testnet = pk.point.address(true, true); + let mainnet = pk.public_key.0.address(true, false); + let testnet = pk.public_key.0.address(true, true); // Different networks should produce different addresses assert_ne!(mainnet, testnet); @@ -402,11 +402,11 @@ fn test_complete_key_serialization_workflow() { assert!(!wif.is_empty()); // Get address - let address = pk.point.address(true, false); + let address = pk.public_key.0.address(true, false); assert!(!address.is_empty()); // Get SEC format - let sec = pk.point.sec(true); + let sec = pk.public_key.0.to_sec(true); assert_eq!(sec.len(), 33); } @@ -419,7 +419,7 @@ fn test_signature_serialization_workflow() { let sig = pk.sign(z.clone()).unwrap(); // Serialize to DER - let der = sig.der(); + let der = sig.to_der(); assert!(!der.is_empty()); assert_eq!(der[0], 0x30); } @@ -430,11 +430,11 @@ fn test_point_serialization_all_formats() { let point = S256Point::generate_point(scalar); // SEC compressed - let sec_compressed = point.sec(true); + let sec_compressed = point.to_sec(true); assert_eq!(sec_compressed.len(), 33); // SEC uncompressed - let sec_uncompressed = point.sec(false); + let sec_uncompressed = point.to_sec(false); assert_eq!(sec_uncompressed.len(), 65); // Address mainnet @@ -455,8 +455,8 @@ fn test_sec_deterministic() { let scalar = 77777_u64.to_biguint().unwrap(); let point = S256Point::generate_point(scalar); - let sec1 = point.sec(true); - let sec2 = point.sec(true); + let sec1 = point.to_sec(true); + let sec2 = point.to_sec(true); // Same point should produce same SEC assert_eq!(sec1, sec2); @@ -470,8 +470,8 @@ fn test_der_deterministic() { let sig1 = Signature::new(r.clone(), s.clone()); let sig2 = Signature::new(r, s); - let der1 = sig1.der(); - let der2 = sig2.der(); + let der1 = sig1.to_der(); + let der2 = sig2.to_der(); // Same signature should produce same DER assert_eq!(der1, der2); @@ -498,8 +498,8 @@ fn test_wif_deterministic() { fn test_sec_generator_point() { let g = S256Point::generator(); - let sec_compressed = g.sec(true); - let sec_uncompressed = g.sec(false); + let sec_compressed = g.to_sec(true); + let sec_uncompressed = g.to_sec(false); assert_eq!(sec_compressed.len(), 33); assert_eq!(sec_uncompressed.len(), 65); @@ -511,7 +511,7 @@ fn test_der_small_signature_values() { let s = S256Field::new(1_u64.to_biguint().unwrap()); let sig = Signature::new(r, s); - let der = sig.der(); + let der = sig.to_der(); // Should handle small values correctly assert_eq!(der[0], 0x30); @@ -534,12 +534,12 @@ fn test_sec_format_validation() { let point = S256Point::generate_point(12345_u64.to_biguint().unwrap()); // Compressed - let sec_comp = point.sec(true); + let sec_comp = point.to_sec(true); assert!(sec_comp[0] == 0x02 || sec_comp[0] == 0x03); assert_eq!(sec_comp.len(), 33); // Uncompressed - let sec_uncomp = point.sec(false); + let sec_uncomp = point.to_sec(false); assert_eq!(sec_uncomp[0], 0x04); assert_eq!(sec_uncomp.len(), 65); } @@ -550,7 +550,7 @@ fn test_der_format_validation() { let s = S256Field::new(888_u64.to_biguint().unwrap()); let sig = Signature::new(r, s); - let der = sig.der(); + let der = sig.to_der(); // Validate DER structure assert_eq!(der[0], 0x30); // SEQUENCE @@ -590,8 +590,8 @@ fn test_multiple_keys_unique_addresses() { let pk1 = PrivateKey::new(SECRET); let pk2 = PrivateKey::new("another_secret"); - let addr1 = pk1.point.address(true, false); - let addr2 = pk2.point.address(true, false); + let addr1 = pk1.public_key.0.address(true, false); + let addr2 = pk2.public_key.0.address(true, false); // Different keys should have different addresses assert_ne!(addr1, addr2); @@ -602,9 +602,121 @@ fn test_multiple_keys_unique_sec() { let pk1 = PrivateKey::new(SECRET); let pk2 = PrivateKey::new("another_secret"); - let sec1 = pk1.point.sec(true); - let sec2 = pk2.point.sec(true); + let sec1 = pk1.public_key.0.to_sec(true); + let sec2 = pk2.public_key.0.to_sec(true); // Different keys should have different SEC assert_ne!(sec1, sec2); } + +// ============================================================ +// UNIT TESTS - Signature::from_der +// DER layout: 0x30 0x02 0x02 +// r and s may have a leading 0x00 padding byte when their high bit is set. +// ============================================================ + +#[test] +fn test_from_der_round_trip() { + let r = S256Field::new(99999_u64.to_biguint().unwrap()); + let s = S256Field::new(12345_u64.to_biguint().unwrap()); + let sig = Signature::new(r, s); + + let der = sig.to_der(); + let parsed = Signature::from_der(&der).unwrap(); + + assert_eq!(parsed.r.element, sig.r.element); + assert_eq!(parsed.s.element, sig.s.element); +} + +#[test] +fn test_from_der_large_values_round_trip() { + let r = S256Field::new(BigUint::from_bytes_be(&FIELD_SIZE) / 2_u64.to_biguint().unwrap()); + let s = S256Field::new(BigUint::from_bytes_be(&FIELD_SIZE) / 3_u64.to_biguint().unwrap()); + let sig = Signature::new(r, s); + + let der = sig.to_der(); + let parsed = Signature::from_der(&der).unwrap(); + + assert_eq!(parsed.r.element, sig.r.element); + assert_eq!(parsed.s.element, sig.s.element); +} + +#[test] +fn test_from_der_rejects_too_short() { + let result = Signature::from_der(&[0x30, 0x06, 0x02]); + assert!(result.is_err()); +} + +#[test] +fn test_from_der_rejects_wrong_sequence_tag() { + // Replace 0x30 with 0x31 + let r = S256Field::new(1_u64.to_biguint().unwrap()); + let s = S256Field::new(1_u64.to_biguint().unwrap()); + let mut der = Signature::new(r, s).to_der(); + der[0] = 0x31; + assert!(Signature::from_der(&der).is_err()); +} + +#[test] +fn test_from_der_rejects_length_mismatch() { + let r = S256Field::new(1_u64.to_biguint().unwrap()); + let s = S256Field::new(1_u64.to_biguint().unwrap()); + let mut der = Signature::new(r, s).to_der(); + // Corrupt the total-length byte + der[1] = 0xff; + assert!(Signature::from_der(&der).is_err()); +} + +#[test] +fn test_from_der_rejects_wrong_integer_tag_for_r() { + let r = S256Field::new(1_u64.to_biguint().unwrap()); + let s = S256Field::new(1_u64.to_biguint().unwrap()); + let mut der = Signature::new(r, s).to_der(); + // der[2] should be 0x02; corrupt it + der[2] = 0x03; + assert!(Signature::from_der(&der).is_err()); +} + +#[test] +fn test_from_der_rejects_extra_trailing_bytes() { + let r = S256Field::new(1_u64.to_biguint().unwrap()); + let s = S256Field::new(1_u64.to_biguint().unwrap()); + let mut der = Signature::new(r, s).to_der(); + der.push(0x00); // extra byte + assert!(Signature::from_der(&der).is_err()); +} + +#[test] +fn test_from_der_with_high_bit_padding() { + // r and s with high bit set — to_der should add 0x00 padding + let r = S256Field::new(0x80_u64.to_biguint().unwrap()); + let s = S256Field::new(0xff_u64.to_biguint().unwrap()); + let sig = Signature::new(r, s); + + let der = sig.to_der(); + let parsed = Signature::from_der(&der).unwrap(); + + assert_eq!(parsed.r.element, sig.r.element); + assert_eq!(parsed.s.element, sig.s.element); +} + +#[test] +fn test_from_der_sign_preserves_r_and_s() { + // Verifies that sign → to_der → from_der preserves r and s exactly. + // Full verify_sig is NOT tested here because verify_sig has a known issue: + // it uses FIELD_SIZE (prime p) as the modulus instead of CURVE_ORDER (n). + // ECDSA scalar arithmetic must use n, not p. Fix verify_sig before adding + // an end-to-end sign/verify test here. + use programming_bitcoin::ser_private_key::PrivateKey; + + let pk = PrivateKey::new("test_secret_for_der"); + let z = S256Field::new(42_u64.to_biguint().unwrap()); + + let sig = pk.sign(z).unwrap(); + let der = sig.to_der(); + + let parsed = Signature::from_der(&der).unwrap(); + + assert_eq!(parsed.r.element, sig.r.element); + assert_eq!(parsed.s.element, sig.s.element); +} diff --git a/tests/ch05_transactions_tests.rs b/tests/ch05_transactions_tests.rs new file mode 100644 index 0000000..693b016 --- /dev/null +++ b/tests/ch05_transactions_tests.rs @@ -0,0 +1,217 @@ +// ============================================================ +// CHAPTER 5: TRANSACTIONS - INTEGRATION TESTS +// ============================================================ + +use programming_bitcoin::{ + decode_varint, encode_varint, + transaction::Transaction, + tx_input::{TxId, TxIn}, + tx_output::TxOut, +}; + +// A real mainnet tx (4 inputs, 2 outputs) used throughout. +// Parsed values (version, locktime, amounts) are verified against known-good data +// so any regression in parse/serialize will be caught immediately. +const REAL_TX_HEX: &str = "010000000456919960ac691763688d3d3bcea9ad6ecaf875df5339e148a1fc61c6ed7a069e010000006a47304402204585bcdef85e6b1c6af5c2669d4830ff86e42dd205c0e089bc2a821657e951c002201024a10366077f87d6bce1f7100ad8cfa8a064b39d4e8fe4ea13a7b71aa8180f012102f0da57e85eec2934a82a585ea337ce2f4998b50ae699dd79f5880e253dafafb7feffffffeb8f51f4038dc17e6313cf831d4f02281c2a468bde0fafd37f1bf882729e7fd3000000006a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937feffffff567bf40595119d1bb8a3037c356efd56170b64cbcc160fb028fa10704b45d775000000006a47304402204c7c7818424c7f7911da6cddc59655a70af1cb5eaf17c69dadbfc74ffa0b662f02207599e08bc8023693ad4e9527dc42c34210f7a7d1d1ddfc8492b654a11e7620a0012102158b46fbdff65d0172b7989aec8850aa0dae49abfb84c81ae6e5b251a58ace5cfeffffffd63a5e6c16e620f86f375925b21cabaf736c779f88fd04dcad51d26690f7f345010000006a47304402200633ea0d3314bea0d95b3cd8dadb2ef79ea8331ffe1e61f762c0f6daea0fabde022029f23b3e9c30f080446150b23852028751635dcee2be669c2a1686a4b5edf304012103ffd6f4a67e94aba353a00882e563ff2722eb4cff0ad6006e86ee20dfe7520d55feffffff0251430f00000000001976a914ab0c0b2e98b1ab6dbf67d4750b0a56244948a87988ac005a6202000000001976a9143c82d7df364eb6c75be8c80df2b3eda8db57397088ac46430600"; + +fn parse_real_tx() -> Transaction { + let bytes = hex::decode(REAL_TX_HEX).unwrap(); + Transaction::parse(&bytes) +} + +// ============================================================ +// varint helpers +// ============================================================ + +#[test] +fn test_decode_varint_single_byte() { + let data = vec![0x05]; + let (val, next) = decode_varint(&data, 0); + assert_eq!(val, 5); + assert_eq!(next, 1); +} + +#[test] +fn test_decode_varint_fd_prefix() { + // 0xfd signals a 2-byte (u16 LE) value follows; 0x0001 = 256 + let data = vec![0xfd, 0x00, 0x01]; + let (val, next) = decode_varint(&data, 0); + assert_eq!(val, 256); + assert_eq!(next, 3); +} + +#[test] +fn test_encode_varint_single_byte() { + assert_eq!(encode_varint(0xfc), vec![0xfc]); +} + +#[test] +fn test_encode_varint_fd_prefix() { + let encoded = encode_varint(256); + assert_eq!(encoded[0], 0xfd); + assert_eq!(encoded.len(), 3); +} + +#[test] +fn test_varint_round_trip() { + // Covers all four varint encoding widths: 1-byte, 0xfd+2, 0xfe+4, 0xff+8 + for n in [0u64, 1, 100, 252, 253, 300, 65535, 65536] { + let encoded = encode_varint(n); + let (decoded, _) = decode_varint(&encoded, 0); + assert_eq!(decoded, n, "round-trip failed for {n}"); + } +} + +// ============================================================ +// Transaction::parse +// ============================================================ + +#[test] +fn test_parse_version() { + let tx = parse_real_tx(); + assert_eq!(tx.version, 1); +} + +#[test] +fn test_parse_input_count() { + let tx = parse_real_tx(); + assert_eq!(tx.inputs.len(), 4); +} + +#[test] +fn test_parse_output_count() { + let tx = parse_real_tx(); + assert_eq!(tx.outputs.len(), 2); +} + +#[test] +fn test_parse_locktime() { + let tx = parse_real_tx(); + assert_eq!(tx.locktime, 410438); +} + +#[test] +fn test_parse_first_input_output_index() { + let tx = parse_real_tx(); + assert_eq!(tx.inputs[0].output_index, 1); +} + +#[test] +fn test_parse_first_input_sequence() { + let tx = parse_real_tx(); + // feffffff = 4294967294 + assert_eq!(tx.inputs[0].sequence, 0xfffffffe); +} + +#[test] +fn test_parse_output_amounts() { + let tx = parse_real_tx(); + assert_eq!(tx.outputs[0].amount, 1000273); + assert_eq!(tx.outputs[1].amount, 40000000); +} + +#[test] +fn test_parse_output_script_pubkeys_non_empty() { + let tx = parse_real_tx(); + assert!(!tx.outputs[0].script_pubkey.is_empty()); + assert!(!tx.outputs[1].script_pubkey.is_empty()); +} + +#[test] +fn test_parse_input_script_sig_non_empty() { + let tx = parse_real_tx(); + // All 4 inputs have a scriptSig + for input in &tx.inputs { + assert!(!input.script_sig.is_empty()); + } +} + +// ============================================================ +// Transaction::serialize (round-trip) +// ============================================================ + +#[test] +fn test_serialize_round_trip() { + let tx = parse_real_tx(); + let serialized = tx.serialize(); + assert_eq!(serialized, REAL_TX_HEX); +} + +#[test] +fn test_serialize_starts_with_version() { + let tx = parse_real_tx(); + let hex = tx.serialize(); + // version 1 little-endian = "01000000" + assert!(hex.starts_with("01000000")); +} + +// ============================================================ +// TxIn +// ============================================================ + +#[test] +fn test_txin_repr() { + let id = [0u8; 32]; + let txin = TxIn::new(id, 0, String::new(), 0xffffffff); + let repr = txin.repr(); + assert!(repr.contains(':')); +} + +#[test] +fn test_txin_serialize_deserialize() { + let tx = parse_real_tx(); + let original = &tx.inputs[0]; + let bytes = original.serialize(); + + // TxIn::parse returns (TxIn, displacement) where displacement == bytes consumed. + // After a full round-trip the displacement must equal the total serialized length. + let (parsed, displacement) = TxIn::parse(&bytes, 0); + assert_eq!(displacement, bytes.len()); + assert_eq!(parsed.output_index, original.output_index); + assert_eq!(parsed.sequence, original.sequence); + assert_eq!(parsed.script_sig, original.script_sig); +} + +#[test] +fn test_txid_from_hash() { + let bytes = [1u8; 32]; + let id = TxId::from_hash(bytes); + assert_eq!(id.0, bytes); +} + +#[test] +fn test_txid_from_raw_transaction() { + let raw = vec![0xde, 0xad, 0xbe, 0xef]; + let id = TxId::from_raw_transaction(raw); + assert!(id.is_ok()); + assert_eq!(id.unwrap().0.len(), 32); +} + +// ============================================================ +// TxOut +// ============================================================ + +#[test] +fn test_txout_new() { + let out = TxOut::new(50000, "76a914ab".to_string()); + assert_eq!(out.amount, 50000); + assert_eq!(out.script_pubkey, "76a914ab"); +} + +#[test] +fn test_txout_serialize_round_trip() { + let tx = parse_real_tx(); + let original = &tx.outputs[0]; + let bytes = original.serialize(); + + let (parsed, _) = TxOut::parse(&bytes, 0); + assert_eq!(parsed.amount, original.amount); + assert_eq!(parsed.script_pubkey, original.script_pubkey); +} + +#[test] +fn test_txout_repr() { + let out = TxOut::new(1000, "deadbeef".to_string()); + let repr = out.repr(); + assert!(repr.contains("1000")); +} diff --git a/tests/ch06_script_tests.rs b/tests/ch06_script_tests.rs new file mode 100644 index 0000000..cdeda23 --- /dev/null +++ b/tests/ch06_script_tests.rs @@ -0,0 +1,252 @@ +// ============================================================ +// CHAPTER 6: SCRIPT / OPCODES - INTEGRATION TESTS +// ============================================================ + +use programming_bitcoin::{ + opcodes::{Element, OpCodes}, + parse_opcodes, + script::{Cmd, Script}, +}; + +// ============================================================ +// encode_num / decode_num +// Bitcoin Script uses a custom little-endian signed integer encoding where +// the sign bit is the high bit of the last byte (not two's complement). +// Zero is encoded as an empty byte vector. +// ============================================================ + +#[test] +fn test_encode_num_zero() { + let e = OpCodes::encode_num(0); + assert!(e.0.is_empty()); +} + +#[test] +fn test_encode_decode_positive() { + for n in [1i64, 2, 9, 16, 127, 128, 255, 1000] { + let encoded = OpCodes::encode_num(n); + let decoded = OpCodes::decode_num(encoded); + assert_eq!(decoded, n, "round-trip failed for {n}"); + } +} + +#[test] +fn test_encode_decode_negative() { + for n in [-1i64, -2, -127, -128, -1000] { + let encoded = OpCodes::encode_num(n); + let decoded = OpCodes::decode_num(encoded); + assert_eq!(decoded, n, "round-trip failed for {n}"); + } +} + +#[test] +fn test_decode_num_zero_element() { + let e = Element(vec![]); + assert_eq!(OpCodes::decode_num(e), 0); +} + +// ============================================================ +// Script::parse via parse_opcodes +// Note: parse_opcodes prepends the varint length prefix before calling +// Script::parse. If you call Script::parse directly, you must include +// the length prefix yourself. +// ============================================================ + +#[test] +fn test_parse_all_opcodes() { + // OP_4 OP_5 OP_ADD OP_9 OP_EQUAL + let script = parse_opcodes(vec![0x54, 0x55, 0x93, 0x59, 0x87]).unwrap(); + assert_eq!(script.commands.len(), 5); +} + +#[test] +fn test_parse_opcode_values() { + let script = parse_opcodes(vec![0x54, 0x55, 0x93, 0x59, 0x87]).unwrap(); + assert!(matches!(script.commands[0], Cmd::OpCode(0x54))); + assert!(matches!(script.commands[4], Cmd::OpCode(0x87))); +} + +#[test] +fn test_parse_data_push() { + // push 3 bytes of data: 0x03 + let script = parse_opcodes(vec![0x03, 0xaa, 0xbb, 0xcc]).unwrap(); + assert_eq!(script.commands.len(), 1); + assert!(matches!(&script.commands[0], Cmd::Data(d) if d == &[0xaa, 0xbb, 0xcc])); +} + +#[test] +fn test_parse_empty_script() { + let script = parse_opcodes(vec![]).unwrap(); + assert!(script.commands.is_empty()); +} + +// ============================================================ +// Script::serialize round-trip +// ============================================================ + +#[test] +fn test_serialize_round_trip() { + let raw = vec![0x54, 0x55, 0x93, 0x59, 0x87]; + let script = parse_opcodes(raw.clone()).unwrap(); + let serialized = script.serialize(); + // serialized = [length_varint, ...raw] + assert_eq!(&serialized[1..], raw.as_slice()); +} + +#[test] +fn test_serialize_length_prefix() { + let raw = vec![0x76, 0xa9, 0x88, 0xac]; // OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + let script = parse_opcodes(raw.clone()).unwrap(); + let serialized = script.serialize(); + assert_eq!(serialized[0], raw.len() as u8); +} + +// ============================================================ +// Script::evaluate — arithmetic +// ============================================================ + +fn eval(opcodes: Vec) -> bool { + let script = parse_opcodes(opcodes).unwrap(); + script.evaluate(&[0u8]).unwrap() +} + +#[test] +fn test_op_add_valid() { + // OP_4 OP_5 OP_ADD OP_9 OP_EQUAL + assert!(eval(vec![0x54, 0x55, 0x93, 0x59, 0x87])); +} + +#[test] +fn test_op_add_invalid() { + // OP_4 OP_5 OP_ADD OP_8 OP_EQUAL (4+5=9 ≠ 8) + assert!(!eval(vec![0x54, 0x55, 0x93, 0x58, 0x87])); +} + +#[test] +fn test_op_mul_valid() { + // OP_3 OP_4 OP_MUL OP_12 OP_EQUAL (3*4=12) + assert!(eval(vec![0x53, 0x54, 0x95, 0x5c, 0x87])); +} + +#[test] +fn test_op_mul_invalid() { + // OP_3 OP_4 OP_MUL OP_9 OP_EQUAL (3*4=12 ≠ 9) + assert!(!eval(vec![0x53, 0x54, 0x95, 0x59, 0x87])); +} + +#[test] +fn test_op_equal_true() { + // OP_5 OP_5 OP_EQUAL + assert!(eval(vec![0x55, 0x55, 0x87])); +} + +#[test] +fn test_op_equal_false() { + // OP_5 OP_4 OP_EQUAL + assert!(!eval(vec![0x55, 0x54, 0x87])); +} + +// ============================================================ +// Script::evaluate — stack ops +// ============================================================ + +#[test] +fn test_op_dup() { + // OP_1 OP_DUP OP_EQUAL (dup then compare — should be true) + assert!(eval(vec![0x51, 0x76, 0x87])); +} + +#[test] +fn test_op_verify_passes() { + // OP_1 OP_VERIFY — leaves empty stack but verify itself returns true + // After verify the stack is empty → evaluate returns false (empty stack) + // So we add OP_1 after to leave something on stack + assert!(eval(vec![0x51, 0x69, 0x51])); +} + +#[test] +fn test_op_verify_fails_on_zero() { + // OP_0 OP_VERIFY — should fail + assert!(!eval(vec![0x00, 0x69])); +} + +#[test] +fn test_op_equal_verify_passes() { + // OP_5 OP_5 OP_EQUALVERIFY OP_1 + assert!(eval(vec![0x55, 0x55, 0x88, 0x51])); +} + +#[test] +fn test_op_equal_verify_fails() { + // OP_5 OP_4 OP_EQUALVERIFY OP_1 + assert!(!eval(vec![0x55, 0x54, 0x88, 0x51])); +} + +// ============================================================ +// Script::evaluate — small number push opcodes +// ============================================================ + +#[test] +fn test_op_1_through_16() { + // OP_1=0x51 through OP_16=0x60 push the literal integer value onto the stack. + // The opcode byte encodes the value as (opcode - 80), so OP_4=0x54 pushes 4. + for n in 1u8..=16 { + let opcode = 80 + n; // OP_1=81 .. OP_16=96 + // push n, push n, OP_EQUAL + let valid = eval(vec![opcode, opcode, 0x87]); + assert!(valid, "OP_{n} self-equal failed"); + } +} + +#[test] +fn test_op_1negate() { + // OP_1NEGATE = 0x4f (79). Push -1, push -1, OP_EQUAL + assert!(eval(vec![0x4f, 0x4f, 0x87])); +} + +// ============================================================ +// Script::evaluate — hash ops +// ============================================================ + +#[test] +fn test_op_hash256_deterministic() { + // Push data, hash256, push same hash manually, equal + // We'll just test that hash256 produces a 32-byte result + let mut stack: Vec = vec![Element(vec![0xde, 0xad])]; + let mut alt: Vec = vec![]; + let mut cmds = std::collections::VecDeque::new(); + let ok = OpCodes::op_hash256(&mut stack, &mut alt, &mut cmds, &[]); + assert!(ok); + assert_eq!(stack[0].0.len(), 32); +} + +#[test] +fn test_op_hash160_produces_20_bytes() { + let mut stack: Vec = vec![Element(b"hello".to_vec())]; + let mut alt: Vec = vec![]; + let mut cmds = std::collections::VecDeque::new(); + let ok = OpCodes::op_hash160(&mut stack, &mut alt, &mut cmds, &[]); + assert!(ok); + assert_eq!(stack[0].0.len(), 20); +} + +#[test] +fn test_op_hash160_empty_stack_fails() { + let mut stack: Vec = vec![]; + let mut alt: Vec = vec![]; + let mut cmds = std::collections::VecDeque::new(); + let ok = OpCodes::op_hash160(&mut stack, &mut alt, &mut cmds, &[]); + assert!(!ok); +} + +// ============================================================ +// Script + operator (concatenation) +// ============================================================ + +#[test] +fn test_script_add_concatenates() { + let s1 = Script::new(vec![Cmd::OpCode(0x51)]); + let s2 = Script::new(vec![Cmd::OpCode(0x51), Cmd::OpCode(0x87)]); + let combined = s1 + s2; + assert_eq!(combined.commands.len(), 3); +}