From 8e36b6f2cb90eba08641fb1d4f2eb65bbb359df1 Mon Sep 17 00:00:00 2001 From: Brandon Black Date: Sat, 17 Oct 2026 13:43:50 -0700 Subject: [PATCH 01/11] consensus: add a script corpus harness beside Core's JSON The corpus uses Core script_tests.json row form. Our interpreter runs every row, and script-oracle compiles VerifyScript from the v31.1 submodule. The first row is an empty scriptSig and OP_1; both engines accept it. --- .../src/script/core_vectors.rs | 33 +++ .../tests/fixtures/script_corpus/INDEX.md | 22 ++ .../fixtures/script_corpus/script_tests.json | 6 + docs/consensus-tests.md | 3 + script-oracle/Cargo.lock | 255 ++++++++++++++++++ script-oracle/Cargo.toml | 15 ++ script-oracle/build.rs | 113 ++++++++ script-oracle/src/lib.rs | 174 ++++++++++++ script-oracle/verify.cpp | 47 ++++ 9 files changed, 668 insertions(+) create mode 100644 crates/rbitcoin-consensus/tests/fixtures/script_corpus/INDEX.md create mode 100644 crates/rbitcoin-consensus/tests/fixtures/script_corpus/script_tests.json create mode 100644 script-oracle/Cargo.lock create mode 100644 script-oracle/Cargo.toml create mode 100644 script-oracle/build.rs create mode 100644 script-oracle/src/lib.rs create mode 100644 script-oracle/verify.cpp diff --git a/crates/rbitcoin-consensus/src/script/core_vectors.rs b/crates/rbitcoin-consensus/src/script/core_vectors.rs index cc6951ddc..ff2018b9a 100644 --- a/crates/rbitcoin-consensus/src/script/core_vectors.rs +++ b/crates/rbitcoin-consensus/src/script/core_vectors.rs @@ -698,3 +698,36 @@ fn core_script_tests_all_rows() { st.pass, st.ran ); } + +/// Our script corpus, in addition to Core's JSON. Each `row` is Core +/// `script_tests.json` form. The v31.1 oracle in `script-oracle/` runs the +/// same file. +#[test] +fn script_corpus_rows_match_our_interpreter() { + let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/script_corpus/script_tests.json"); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}")); + let rows: Vec = serde_json::from_str(&text).expect("script corpus json"); + assert!(!rows.is_empty(), "script corpus is empty"); + for row in &rows { + let id = row["id"].as_str().unwrap_or("?").to_string(); + let cells = row["row"] + .as_array() + .unwrap_or_else(|| panic!("{id}: missing row")); + assert!( + cells.len() >= 4, + "{id}: row needs scriptSig, scriptPubKey, flags, result" + ); + let sig = assemble(cells[0].as_str().unwrap_or("")) + .unwrap_or_else(|e| panic!("{id} scriptSig: {e}")); + let pk = assemble(cells[1].as_str().unwrap_or("")) + .unwrap_or_else(|e| panic!("{id} scriptPubKey: {e}")); + let flags = parse_flags(cells[2].as_str().unwrap_or("NONE")); + let expect = cells[3].as_str().unwrap_or(""); + let got = run_script_row(&sig, &pk, Witness::new(), Amount::ZERO, &flags); + assert!( + outcome_matches(expect, &got), + "{id}: expect {expect} got {got:?}" + ); + } +} diff --git a/crates/rbitcoin-consensus/tests/fixtures/script_corpus/INDEX.md b/crates/rbitcoin-consensus/tests/fixtures/script_corpus/INDEX.md new file mode 100644 index 000000000..ad01fb458 --- /dev/null +++ b/crates/rbitcoin-consensus/tests/fixtures/script_corpus/INDEX.md @@ -0,0 +1,22 @@ +# Script corpus + +Run beside Bitcoin Core's JSON suites. Every row is executed by our +interpreter and by Bitcoin Core v31.1 `VerifyScript` (the submodule, not +the crates.io `bitcoinconsensus` crate, which is Core 26). Both must +agree with the row's expected accept or reject. Error strings are not +compared. + +This is not a proof over every byte string. secp256k1 is a shared +library. Block sigop cost, the milestone script skip, and transaction +structure are outside this corpus. Policy flags that confirm never sets +appear only as rows that leave them off. + +The `row` array is Core `script_tests.json` form +`[scriptSig, scriptPubKey, flags, result]` and can be appended upstream +without a runner change. + +## Vectors + +| Id | Predicate | +|----|-----------| +| `smoke-op-1` | Harness: empty scriptSig, `OP_1` scriptPubKey, no flags, accepts on both engines. | diff --git a/crates/rbitcoin-consensus/tests/fixtures/script_corpus/script_tests.json b/crates/rbitcoin-consensus/tests/fixtures/script_corpus/script_tests.json new file mode 100644 index 000000000..be747618d --- /dev/null +++ b/crates/rbitcoin-consensus/tests/fixtures/script_corpus/script_tests.json @@ -0,0 +1,6 @@ +[ + { + "id": "smoke-op-1", + "row": ["", "1", "NONE", "OK"] + } +] diff --git a/docs/consensus-tests.md b/docs/consensus-tests.md index bfda6eae2..36a3e7d01 100644 --- a/docs/consensus-tests.md +++ b/docs/consensus-tests.md @@ -17,6 +17,8 @@ cargo test -p rbitcoin-consensus --lib core_script_tests_all_rows -- --nocapture cargo test -p rbitcoin-consensus --lib core_tx_ -- --nocapture cargo test -p rbitcoin-consensus --lib core_sighash -- --nocapture cargo test -p rbitcoin-consensus --lib core_bip341 -- --nocapture +cargo test -p rbitcoin-consensus --lib script_corpus_rows_match_our_interpreter -- --quiet +cargo test --manifest-path script-oracle/Cargo.toml --quiet cargo test -p rbitcoin-consensus --lib block_866342 -- --nocapture cargo test -p rbitcoin-consensus --test script_edge_fixtures # broader integration still covers connect success paths: @@ -37,6 +39,7 @@ refreshing; do not check copies into `tests/fixtures/`. | `tx_invalid.json` | same | `script::core_tx_vectors::core_tx_invalid_all_rows` | every data row reject | | `sighash.json` | same | `script::core_sighash::core_sighash_all_rows` | every data row digest matches Core | | `bip341_wallet_vectors.json` | same | `script::core_bip341::core_bip341_wallet_vectors_all_rows` | key-path fully-signed + per-input spends accept; unknown-leaf script-path accepts | +| `script_corpus/script_tests.json` | `tests/fixtures/script_corpus/` (ours, Core row form) | `script::core_vectors::script_corpus_rows_match_our_interpreter` and `script-oracle` | every row accept/reject matches the file; the oracle is Core v31.1 `VerifyScript` | | mainnet 866342 + prevouts | `tests/fixtures/block_866342/` (Floresta zstd) | `block::block_866342::block_866342_structure_scripts_and_overweight` | structure at height + every non-coinbase `verify_job_all_inputs`; extra-NOP clone is 4_000_001 WU | ### How the harness works diff --git a/script-oracle/Cargo.lock b/script-oracle/Cargo.lock new file mode 100644 index 000000000..959f54fe3 --- /dev/null +++ b/script-oracle/Cargo.lock @@ -0,0 +1,255 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "base58ck" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" +dependencies = [ + "bitcoin_hashes", +] + +[[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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative 0.2.3", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin-units" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.3", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "hex-conservative" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "script-oracle" +version = "0.0.0" +dependencies = [ + "bitcoin", + "cc", + "serde_json", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[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.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/script-oracle/Cargo.toml b/script-oracle/Cargo.toml new file mode 100644 index 000000000..c802cede4 --- /dev/null +++ b/script-oracle/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] + +[package] +name = "script-oracle" +version = "0.0.0" +edition = "2021" +publish = false +description = "Bitcoin Core v31.1 VerifyScript oracle for the script corpus. Not a workspace member." + +[dependencies] +bitcoin = "=0.32.102" +serde_json = "1" + +[build-dependencies] +cc = "1" diff --git a/script-oracle/build.rs b/script-oracle/build.rs new file mode 100644 index 000000000..d8f544b36 --- /dev/null +++ b/script-oracle/build.rs @@ -0,0 +1,113 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +fn main() { + let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest.join(".."); + let submodule = repo.join("third_party/bitcoin"); + let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("core"); + let src = out.join("src"); + if !src.join("script/interpreter.cpp").is_file() { + std::fs::create_dir_all(&out).unwrap(); + if !archive(&submodule, &out) { + let init = Command::new(repo.join("scripts/core-functional/init-submodule.sh")) + .status() + .expect("run init-submodule.sh"); + assert!(init.success(), "init-submodule.sh failed"); + let add = Command::new("git") + .args(["-C"]) + .arg(&submodule) + .args(["sparse-checkout", "add", "src"]) + .status() + .expect("sparse-checkout"); + assert!(add.success(), "sparse-checkout add src failed"); + let checkout = Command::new("git") + .args(["-C"]) + .arg(&submodule) + .args(["checkout", "HEAD", "--", "src"]) + .status() + .expect("checkout src"); + assert!(checkout.success(), "checkout src failed"); + assert!(archive(&submodule, &out), "git archive of Core src failed"); + } + } + assert!( + src.join("script/interpreter.cpp").is_file(), + "missing {}", + src.join("script/interpreter.cpp").display() + ); + + cc::Build::new() + .cpp(true) + .std("c++20") + .include(&src) + .include(src.join("secp256k1/include")) + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-implicit-fallthrough") + .file(manifest.join("verify.cpp")) + .file(src.join("script/interpreter.cpp")) + .file(src.join("script/script.cpp")) + .file(src.join("script/script_error.cpp")) + .file(src.join("primitives/transaction.cpp")) + .file(src.join("pubkey.cpp")) + .file(src.join("hash.cpp")) + .file(src.join("uint256.cpp")) + .file(src.join("arith_uint256.cpp")) + .file(src.join("crypto/ripemd160.cpp")) + .file(src.join("crypto/sha1.cpp")) + .file(src.join("crypto/sha256.cpp")) + .file(src.join("crypto/sha512.cpp")) + .file(src.join("crypto/hmac_sha512.cpp")) + .file(src.join("crypto/hex_base.cpp")) + .file(src.join("util/strencodings.cpp")) + .file(src.join("streams.cpp")) + .file(src.join("support/cleanse.cpp")) + .compile("coreverify"); + + cc::Build::new() + .include(src.join("secp256k1/include")) + .include(src.join("secp256k1/src")) + .define("ENABLE_MODULE_SCHNORRSIG", "1") + .define("ENABLE_MODULE_EXTRAKEYS", "1") + .define("ENABLE_MODULE_ELLSWIFT", "1") + .define("ENABLE_MODULE_RECOVERY", "1") + .define("ECMULT_WINDOW_SIZE", "15") + .define("ECMULT_GEN_PREC_BITS", "4") + .define("USE_FIELD_5X52", "1") + .define("USE_SCALAR_4X64", "1") + .define("HAVE___INT128", "1") + .flag_if_supported("-Wno-unused-function") + .file(src.join("secp256k1/src/secp256k1.c")) + .file(src.join("secp256k1/src/precomputed_ecmult.c")) + .file(src.join("secp256k1/src/precomputed_ecmult_gen.c")) + .compile("secp256k1oracle"); + + println!("cargo:rerun-if-changed=verify.cpp"); + println!("cargo:rerun-if-changed=build.rs"); +} + +fn archive(submodule: &Path, out: &Path) -> bool { + let archived = Command::new("git") + .args(["-C"]) + .arg(submodule) + .args(["archive", "HEAD", "src"]) + .output() + .expect("git archive"); + if !archived.status.success() || archived.stdout.is_empty() { + return false; + } + let mut tar = Command::new("tar") + .args(["-C"]) + .arg(out) + .arg("-x") + .stdin(Stdio::piped()) + .spawn() + .expect("tar"); + tar.stdin + .take() + .unwrap() + .write_all(&archived.stdout) + .unwrap(); + tar.wait().expect("tar wait").success() +} diff --git a/script-oracle/src/lib.rs b/script-oracle/src/lib.rs new file mode 100644 index 000000000..04dea42fc --- /dev/null +++ b/script-oracle/src/lib.rs @@ -0,0 +1,174 @@ +//! Bitcoin Core v31.1 `VerifyScript` for the in-tree script corpus. +//! +//! Not a workspace member: the main graph must not link `bitcoinconsensus` +//! (that crates.io crate is Core 26). This crate compiles `VerifyScript` +//! from `third_party/bitcoin` at the v31.1 pin. The Rust side only runs +//! as tests; the C++ wrapper is what the build script links. + +#[cfg(test)] +mod tests { + use bitcoin::absolute::LockTime; + use bitcoin::consensus::encode::serialize; + use bitcoin::script::ScriptBuf; + use bitcoin::{Amount, OutPoint, Sequence, Transaction, TxIn, TxOut, Witness}; + + const P2SH: u64 = 1 << 0; + const STRICTENC: u64 = 1 << 1; + const DERSIG: u64 = 1 << 2; + const LOW_S: u64 = 1 << 3; + const NULLDUMMY: u64 = 1 << 4; + const SIGPUSHONLY: u64 = 1 << 5; + const MINIMALDATA: u64 = 1 << 6; + const DISCOURAGE_NOPS: u64 = 1 << 7; + const CLEANSTACK: u64 = 1 << 8; + const CLTV: u64 = 1 << 9; + const CSV: u64 = 1 << 10; + const WITNESS: u64 = 1 << 11; + const DISCOURAGE_WITNESS: u64 = 1 << 12; + const MINIMALIF: u64 = 1 << 13; + const NULLFAIL: u64 = 1 << 14; + const WITNESS_PUBKEYTYPE: u64 = 1 << 15; + const CONST_SCRIPTCODE: u64 = 1 << 16; + const TAPROOT: u64 = 1 << 17; + const DISCOURAGE_TAPROOT: u64 = 1 << 18; + const DISCOURAGE_SUCCESS: u64 = 1 << 19; + const DISCOURAGE_PUBKEY: u64 = 1 << 20; + + extern "C" { + fn rbtc_core_verify( + tx: *const u8, + tx_len: usize, + spk: *const u8, + spk_len: usize, + amount: i64, + input_index: u32, + flags: u64, + ) -> i32; + } + + fn flags_from_core(text: &str) -> u64 { + if text.is_empty() || text.eq_ignore_ascii_case("NONE") { + return 0; + } + let mut flags = 0u64; + for part in text.split(',') { + match part.trim().to_ascii_uppercase().as_str() { + "" => {} + "P2SH" => flags |= P2SH, + "STRICTENC" => flags |= STRICTENC | DERSIG, + "DERSIG" => flags |= DERSIG, + "LOW_S" => flags |= LOW_S | DERSIG, + "NULLDUMMY" => flags |= NULLDUMMY, + "SIGPUSHONLY" => flags |= SIGPUSHONLY, + "MINIMALDATA" => flags |= MINIMALDATA, + "DISCOURAGE_UPGRADABLE_NOPS" => flags |= DISCOURAGE_NOPS, + "CLEANSTACK" => flags |= CLEANSTACK, + "CHECKLOCKTIMEVERIFY" => flags |= CLTV, + "CHECKSEQUENCEVERIFY" => flags |= CSV, + "WITNESS" => flags |= WITNESS, + "MINIMALIF" => flags |= MINIMALIF, + "NULLFAIL" => flags |= NULLFAIL, + "WITNESS_PUBKEYTYPE" => flags |= WITNESS_PUBKEYTYPE, + "CONST_SCRIPTCODE" => flags |= CONST_SCRIPTCODE, + "TAPROOT" | "TAPSCRIPT" => flags |= TAPROOT | WITNESS, + "DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" => flags |= DISCOURAGE_WITNESS, + "DISCOURAGE_UPGRADABLE_TAPROOT_VERSION" => flags |= DISCOURAGE_TAPROOT, + "DISCOURAGE_OP_SUCCESS" => flags |= DISCOURAGE_SUCCESS, + "DISCOURAGE_UPGRADABLE_PUBKEYTYPE" => flags |= DISCOURAGE_PUBKEY, + other => panic!("unknown script flag {other}"), + } + } + flags + } + + /// Core `script_tests.cpp` credit transaction: two `OP_0` pushes on a null input. + fn credit_and_spend(script_pubkey: ScriptBuf, script_sig: ScriptBuf) -> (Transaction, Vec) { + let credit = Transaction { + version: bitcoin::transaction::Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::from_bytes(vec![0x00, 0x00]), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::ZERO, + script_pubkey: script_pubkey.clone(), + }], + }; + let spend = Transaction { + version: bitcoin::transaction::Version::ONE, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint { + txid: credit.compute_txid(), + vout: 0, + }, + script_sig, + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::new(), + }], + }; + (spend, script_pubkey.to_bytes()) + } + + fn core_accepts(spend: &Transaction, spk: &[u8], flags: u64) -> Result { + let raw = serialize(spend); + let rc = unsafe { + rbtc_core_verify( + raw.as_ptr(), + raw.len(), + spk.as_ptr(), + spk.len(), + 0, + 0, + flags, + ) + }; + match rc { + 1 => Ok(true), + 0 => Ok(false), + other => Err(other), + } + } + + fn assemble_push_op1() -> ScriptBuf { + // Core asm "1" is OP_1. Kept here so the smoke row does not need the + // consensus crate's assembler. Other rows go through that assembler + // once the corpus grows past single opcodes; this smoke row is the + // harness pin. + ScriptBuf::from_bytes(vec![0x51]) + } + + #[test] + fn script_corpus_matches_core_verify_script() { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../crates/rbitcoin-consensus/tests/fixtures/script_corpus/script_tests.json"); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}")); + let rows: Vec = serde_json::from_str(&text).unwrap(); + assert!(!rows.is_empty()); + for row in &rows { + let id = row["id"].as_str().unwrap(); + let cells = row["row"].as_array().unwrap(); + let sig_asm = cells[0].as_str().unwrap(); + let pk_asm = cells[1].as_str().unwrap(); + let flag_text = cells[2].as_str().unwrap(); + let expect = cells[3].as_str().unwrap(); + assert_eq!( + sig_asm, "", + "{id}: smoke harness only assembles an empty scriptSig" + ); + assert_eq!(pk_asm, "1", "{id}: smoke harness only assembles OP_1"); + let (spend, spk) = credit_and_spend(assemble_push_op1(), ScriptBuf::new()); + let got = core_accepts(&spend, &spk, flags_from_core(flag_text)) + .unwrap_or_else(|rc| panic!("{id}: Core verify status {rc}")); + let want = expect == "OK"; + assert_eq!(got, want, "{id}"); + } + } +} diff --git a/script-oracle/verify.cpp b/script-oracle/verify.cpp new file mode 100644 index 000000000..31c014980 --- /dev/null +++ b/script-oracle/verify.cpp @@ -0,0 +1,47 @@ +#include +#include