Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions bitcoind-tests/tests/common/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub trait Call {
fn get_new_address(&self) -> elements::Address;
fn send_to_address(&self, addr: &elements::Address, amt: &str) -> elements::Txid;
fn get_transaction(&self, txid: &elements::Txid) -> elements::Transaction;
fn send_raw_transaction(&self, hex: &elements::Transaction) -> elements::Txid;
fn send_raw_transaction(&self, hex: &elements::Transaction) -> Result<elements::Txid, String>;
fn generate(&self, blocks: u32);
}

Expand Down Expand Up @@ -89,14 +89,16 @@ impl Call for ElementsD {
elements::Txid::from_str(&tx_id).unwrap()
}

fn send_raw_transaction(&self, tx: &elements::Transaction) -> elements::Txid {
// Returns the node's rejection reason instead of panicking, so that one rejected
// spend does not stop the remaining test cases.
fn send_raw_transaction(&self, tx: &elements::Transaction) -> Result<elements::Txid, String> {
let tx_id = self
.call("sendrawtransaction", &[serialize_hex(tx).into()])
.as_str()
.unwrap()
.to_string();
.client()
.call::<Value>("sendrawtransaction", &[serialize_hex(tx).into()])
.map_err(|error| error.to_string())?;
let tx_id = tx_id.as_str().unwrap();

elements::Txid::from_str(&tx_id).unwrap()
Ok(elements::Txid::from_str(tx_id).unwrap())
}

fn generate(&self, blocks: u32) {
Expand Down
97 changes: 75 additions & 22 deletions bitcoind-tests/tests/common/test.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;

use elements::pset::PartiallySignedTransaction as Psbt;
use elements::{confidential, secp256k1_zkp as secp256k1};
use elementsd::bitcoincore_rpc::jsonrpc::serde_json;
use elementsd::ElementsD;
use secp256k1::XOnlyPublicKey;
use simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
use simplicityhl::ast::ElementsJetHinter;
use simplicityhl::{elements, simplicity};
use simplicityhl::{elements, simplicity, UnresolvedValues, UnstableFeatures};

use crate::common::daemon::Call;

Expand All @@ -17,6 +20,8 @@ pub struct TestCase<'a> {
template: Option<simplicityhl::TemplateAst>,
compiled: Option<simplicityhl::CompiledProgram>,
witness: FnWitness,
witness_file: Option<PathBuf>,
pruned: bool,
lock_time: elements::LockTime,
sequence: elements::Sequence,
daemon: &'a ElementsD,
Expand All @@ -37,6 +42,8 @@ impl<'a> TestCase<'a> {
template: None,
compiled: None,
witness: empty_witness,
witness_file: None,
pruned: false,
lock_time: elements::LockTime::ZERO,
sequence: elements::Sequence::ZERO,
daemon,
Expand All @@ -50,10 +57,8 @@ impl<'a> TestCase<'a> {
self
}

pub fn program_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
let text =
std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(path))
.expect("path should be readable");
pub fn program_path<P: AsRef<Path>>(mut self, path: P) -> Self {
let text = read_file(path);
let compiled = simplicityhl::CompiledProgram::new(
text.as_str(),
simplicityhl::Arguments::default(),
Expand All @@ -65,17 +70,38 @@ impl<'a> TestCase<'a> {
self
}

pub fn template_path<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
let text =
std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(path))
.expect("path should be readable");
let template =
simplicityhl::TemplateAst::new(text.as_str(), Box::new(ElementsJetHinter::new()))
.expect("program should compile");
pub fn template_path<P: AsRef<Path>>(self, path: P) -> Self {
self.template_path_with_unstable(path, &UnstableFeatures::none())
}

pub fn template_path_with_unstable<P: AsRef<Path>>(
mut self,
path: P,
unstable_features: &UnstableFeatures,
) -> Self {
let text = read_file(path);
let template = simplicityhl::TemplateAst::new_with_unstable(
text.as_str(),
unstable_features,
Box::new(ElementsJetHinter::new()),
)
.expect("program should compile");
self.template = Some(template);
self
}

pub fn arguments_file<P: AsRef<Path>>(self, path: P) -> Self {
let parameters = self
.template
.as_ref()
.expect("template should exist")
.parameters();
let arguments = read_values(path)
.resolve(parameters)
.expect("arguments should be consistent with the program");
self.arguments(arguments)
}

pub fn arguments(mut self, arguments: simplicityhl::Arguments) -> Self {
let compiled = self
.template
Expand All @@ -92,6 +118,18 @@ impl<'a> TestCase<'a> {
self
}

/// Read the witness from a file. Use this for witnesses that do not sign the transaction.
pub fn witness_file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.witness_file = Some(path.as_ref().to_path_buf());
self
}

/// Prune the program against the spending transaction before it is broadcast.
pub fn pruned(mut self) -> Self {
self.pruned = true;
self
}

pub fn lock_time(mut self, lock_time: elements::LockTime) -> Self {
self.lock_time = lock_time;
self
Expand Down Expand Up @@ -162,7 +200,9 @@ impl<'a> TestCase<'a> {
self.funding = Some(Funding { address, txid });
}

pub fn spend_utxo(&self) {
/// Spend the funding output, returning the reason if the program cannot be satisfied
/// or the node rejects the spend.
pub fn spend_utxo(&self) -> Result<(), String> {
let compiled = self.compiled.as_ref().expect("test program is missing");
let (outpoint, utxo) = self.get_vout();
let mut psbt = Psbt::from_tx(elements::Transaction {
Expand Down Expand Up @@ -192,28 +232,32 @@ impl<'a> TestCase<'a> {
let control_block = info
.control_block(&script_ver)
.expect("control block should exist");
let sighash_all = {
let env = {
let tx = psbt
.extract_tx()
.expect("transaction should be extractable");
let utxos = vec![ElementsUtxo::from(utxo)];
let index = 0;
let annex = None;
let env = ElementsEnv::new(
&tx,
ElementsEnv::new(
Arc::new(tx),
utxos,
index,
compiled.commit().cmr(),
control_block.clone(),
annex,
self.genesis_hash,
);
env.c_tx_env().sighash_all()
)
};
let witness_values = match &self.witness_file {
Some(path) => read_values(path)
.resolve(compiled.witness_types())
.expect("witness should be consistent with the program"),
None => (self.witness)(env.c_tx_env().sighash_all().to_byte_array()),
};
let witness_values = (self.witness)(sighash_all.to_byte_array());
let satisfied_program = compiled
.satisfy(witness_values)
.expect("program should be satisfiable");
.satisfy_with_env(witness_values, self.pruned.then_some(&env))
.map_err(|error| format!("program is not satisfiable: {error}"))?;
let (program_bytes, witness_bytes) = satisfied_program.redeem().to_vec_with_witness();
psbt.inputs_mut()[0].final_script_witness = Some(elements::Witness::from(vec![
witness_bytes,
Expand All @@ -225,10 +269,19 @@ impl<'a> TestCase<'a> {
.extract_tx()
.expect("transaction should be extractable");

let _txid = self.daemon.send_raw_transaction(&tx);
self.daemon.send_raw_transaction(&tx).map(|_txid| ())
}
}

fn empty_witness(_sighash_all: [u8; 32]) -> simplicityhl::WitnessValues {
simplicityhl::WitnessValues::default()
}

fn read_file<P: AsRef<Path>>(path: P) -> String {
std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join(path))
.expect("path should be readable")
}

fn read_values<P: AsRef<Path>>(path: P) -> UnresolvedValues {
serde_json::from_str(&read_file(path)).expect("values file should be valid JSON")
}
64 changes: 61 additions & 3 deletions bitcoind-tests/tests/spend_utxo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use elements::secp256k1_zkp as secp256k1;
use secp256k1::hashes::{sha256, Hash, HashEngine};
use simplicityhl::types::TypeConstructible;
use simplicityhl::value::ValueConstructible;
use simplicityhl::{elements, ResolvedType, TemplateProgramWitness, Value, WitnessNameToValueMap};
use simplicityhl::{
elements, ResolvedType, TemplateProgramWitness, UnstableFeatures, Value, WitnessNameToValueMap,
};

mod common;
use common::daemon::{self, Call};
Expand Down Expand Up @@ -37,14 +39,70 @@ fn spend_utxo() {
.name("Pay to multisig")
.program_path("../examples/p2ms.simf")
.witness(p2ms),
// Pruned spends: the node rejects programs with unexecuted branches or unread
// witness data, so these check what pruning produces against the real node.
TestCase::new(&daemon, genesis_hash)
.name("OP_CAT (pruned)")
.program_path("../examples/cat.simf")
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("HODL vault (pruned)")
.program_path("../examples/hodl_vault.simf")
.witness(hodl_vault)
.lock_time(elements::LockTime::from_consensus(1000))
.sequence(elements::Sequence::ENABLE_LOCKTIME_NO_RBF)
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Pay to public key (pruned)")
.template_path("../examples/p2pk.simf")
.arguments(p2pk_args())
.witness(p2pk)
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Pay to public key hash (pruned)")
.program_path("../examples/p2pkh.simf")
.witness(p2pkh)
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Pay to multisig (pruned)")
.program_path("../examples/p2ms.simf")
.witness(p2ms)
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Kitchen sink (pruned)")
.template_path_with_unstable("../examples/kitchen_sink.simf", &UnstableFeatures::all())
.arguments_file("../examples/kitchen_sink.args")
.witness_file("../examples/kitchen_sink.wit")
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Witness read only by a hidden branch (pruned)")
.program_path("../fixtures/pruning_hidden_witness.simf")
.witness_file("../fixtures/pruning_hidden_witness.none.wit")
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Witness read by the taken branch (pruned)")
.program_path("../fixtures/pruning_hidden_witness.simf")
.witness_file("../fixtures/pruning_hidden_witness.eq.wit")
.pruned(),
TestCase::new(&daemon, genesis_hash)
.name("Identical case nodes taking different branches (pruned)")
.program_path("../fixtures/pruning_twin_cases.simf")
.pruned(),
];
tests.iter_mut().for_each(TestCase::create_utxo);
daemon.generate(1000); // satisfies lock time of all test cases
let mut rejected = vec![];
for test in tests {
println!("⏳ {}", test.name);
test.spend_utxo();
println!("✅ {}", test.name);
match test.spend_utxo() {
Ok(()) => println!("✅ {}", test.name),
Err(error) => {
println!("❌ {}: {error}", test.name);
rejected.push(test.name);
}
}
}
assert!(rejected.is_empty(), "rejected spends: {rejected:?}");
}

fn hodl_vault(sighash_all: [u8; 32]) -> simplicityhl::WitnessValues {
Expand Down
14 changes: 14 additions & 0 deletions fixtures/pruning_hidden_witness.eq.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"FIRST": {
"value": "Some(0x0000000000000000000000000000000000000000000000000000000000000000)",
"type": "Option<u256>"
},
"SECOND": {
"value": "Some(0x0000000000000000000000000000000000000000000000000000000000000000)",
"type": "Option<u256>"
},
"CHECK_NONE": {
"value": "false",
"type": "bool"
}
}
14 changes: 14 additions & 0 deletions fixtures/pruning_hidden_witness.none.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"FIRST": {
"value": "None",
"type": "Option<u256>"
},
"SECOND": {
"value": "Some(0x0000000000000000000000000000000000000000000000000000000000000000)",
"type": "Option<u256>"
},
"CHECK_NONE": {
"value": "true",
"type": "bool"
}
}
32 changes: 32 additions & 0 deletions fixtures/pruning_hidden_witness.simf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* PRUNING: WITNESS READ ONLY BY A HIDDEN BRANCH
*
* Pruning fixture, not a meaningful contract. `SECOND` is bound outside the
* `match` and read only by the `false` branch. When `CHECK_NONE` is true,
* pruning hides that branch and must shrink `SECOND` to the bits the rest of
* the program reads; otherwise the node finds unread witness data and
* rejects the spend.
*
* Regression test for https://github.com/BlockstreamResearch/rust-simplicity/pull/379
*/
fn assert_eq_256(a: u256, b: u256) {
assert!(jet::eq_256(a, b));
}

fn assert_none_256(value: Option<u256>) {
assert!(is_none::<u256>(value));
}

fn assert_none_or_eq_256(check_none: bool, first: Option<u256>, second: Option<u256>) {
match check_none {
true => assert_none_256(first),
false => assert_eq_256(unwrap(first), unwrap(second)),
};
}

fn main() {
let first: Option<u256> = witness::FIRST;
let second: Option<u256> = witness::SECOND;
let check_none: bool = witness::CHECK_NONE;
assert_none_or_eq_256(check_none, first, second);
}
Loading
Loading