Skip to content

Commit

Permalink
refactor: Represent compiled Simfony program w/o witness
Browse files Browse the repository at this point in the history
We can compile a Simfony program to Simplicity without knowing the
witness data. As of now, the witness data is added directly to the
Simplicity target code.

This commit creates a new type CompiledProgram that represents the
compiled, but not-yet-satisfied, Simfony program. There are methods
to upgrade CompiledProgram to the existing SatisfiedProgram.

This commit also makes the members inside SatisfiedProgram private, as
these members are prone to change in the near future.
  • Loading branch information
uncomputable committed Sep 25, 2024
1 parent bee7a84 commit 3d2b242
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 62 deletions.
7 changes: 4 additions & 3 deletions bitcoind-tests/tests/test_arith.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::str::FromStr;
use ::secp256k1::XOnlyPublicKey;
use simfony::elements::taproot::{TaprootBuilder, LeafVersion};
use simfony::elements;
use simfony::SatisfiedProgram;

use elements::pset::PartiallySignedTransaction as Psbt;
use elements::{
Expand Down Expand Up @@ -43,13 +44,13 @@ pub fn test_simplicity(cl: &ElementsD, program_file: &str, witness_file: &str) {
let program_text = std::fs::read_to_string(program_path).unwrap();
let witness_text = std::fs::read_to_string(witness_path).unwrap();
let witness_values = serde_json::from_str::<WitnessValues>(&witness_text).unwrap();
let program = simfony::satisfy(&program_text, &witness_values).unwrap().simplicity;
let program = SatisfiedProgram::new(&program_text, &witness_values).unwrap();

let secp = secp256k1::Secp256k1::new();
let internal_key = XOnlyPublicKey::from_str("f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2").unwrap();

let builder = TaprootBuilder::new();
let script = elements::script::Script::from(program.cmr().as_ref().to_vec());
let script = elements::script::Script::from(program.redeem().cmr().as_ref().to_vec());
let script_ver = (script, LeafVersion::from_u8(0xbe).unwrap());
let builder = builder.add_leaf_with_ver(0, script_ver.0.clone(), script_ver.1).unwrap();
let data = builder.finalize(&secp, internal_key).unwrap();
Expand Down Expand Up @@ -79,7 +80,7 @@ pub fn test_simplicity(cl: &ElementsD, program_file: &str, witness_file: &str) {
psbt.add_output(psbt::Output::from_txout(out));
let fee_out = TxOut::new_fee(3_000, witness_utxo.asset.explicit().unwrap());
psbt.add_output(psbt::Output::from_txout(fee_out));
let (program_bytes, witness_bytes) = program.encode_to_vec();
let (program_bytes, witness_bytes) = program.redeem().encode_to_vec();
psbt.inputs_mut()[0].final_script_witness =
Some(vec![
witness_bytes,
Expand Down
3 changes: 2 additions & 1 deletion fuzz/fuzz_targets/compile_text.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![no_main]

use libfuzzer_sys::{fuzz_target, Corpus};
use simfony::CompiledProgram;

/// The PEST parser is slow for inputs with many open brackets.
/// Detect some of these inputs to reject them from the corpus.
Expand Down Expand Up @@ -31,7 +32,7 @@ fuzz_target!(|data: &[u8]| -> Corpus {
return Corpus::Reject;
}

let _ = simfony::compile(program_text);
let _ = CompiledProgram::new(program_text);
}

Corpus::Keep
Expand Down
146 changes: 102 additions & 44 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,95 @@ use simplicity::{jet::Elements, CommitNode, RedeemNode};
pub extern crate simplicity;
pub use simplicity::elements;

use crate::ast::DeclaredWitnesses;
use crate::debug::DebugSymbols;
use crate::error::WithFile;
use crate::parse::ParseFromStr;
use crate::witness::WitnessValues;

pub fn compile(prog_text: &str) -> Result<Arc<CommitNode<Elements>>, String> {
let parse_program = parse::Program::parse_from_str(prog_text)?;
let ast_program = ast::Program::analyze(&parse_program).with_file(prog_text)?;
let simplicity_named_construct = ast_program.compile().with_file(prog_text)?;
let simplicity_commit = named::to_commit_node(&simplicity_named_construct)
.expect("Failed to set program source and target type to unit");
Ok(simplicity_commit)
/// A Simfony program, compiled to Simplicity.
#[derive(Clone, Debug)]
pub struct CompiledProgram {
simplicity: ProgNode,
witness_types: DeclaredWitnesses,
debug_symbols: DebugSymbols,
}

/// A satisfied Simfony program, compiled to Simplicity.
impl CompiledProgram {
/// Parse and compile a Simfony program from the given string.
///
/// ## Errors
///
/// The string is not a valid Simfony program.
pub fn new(s: &str) -> Result<Self, String> {
let parse_program = parse::Program::parse_from_str(s)?;
let ast_program = ast::Program::analyze(&parse_program).with_file(s)?;
let simplicity_named_construct = ast_program.compile().with_file(s)?;
Ok(Self {
simplicity: simplicity_named_construct,
witness_types: ast_program.witnesses().clone(),
debug_symbols: ast_program.debug_symbols(s),
})
}

/// Access the debug symbols for the Simplicity target code.
pub fn debug_symbols(&self) -> &DebugSymbols {
&self.debug_symbols
}

/// Access the Simplicity target code, without witness data.
pub fn commit(&self) -> Arc<CommitNode<Elements>> {
named::to_commit_node(&self.simplicity).expect("Compiled Simfony program has type 1 -> 1")
}

/// Satisfy the Simfony program with the given `witness_values`.
///
/// ## Errors
///
/// - Witness values have a different type than declared in the Simfony program.
/// - There are missing witness values.
pub fn satisfy(&self, witness_values: &WitnessValues) -> Result<SatisfiedProgram, String> {
witness_values
.is_consistent(&self.witness_types)
.map_err(|e| e.to_string())?;
let simplicity_witness = named::to_witness_node(&self.simplicity, witness_values);
let simplicity_redeem = simplicity_witness.finalize().map_err(|e| e.to_string())?;
Ok(SatisfiedProgram {
simplicity: simplicity_redeem,
debug_symbols: self.debug_symbols.clone(),
})
}
}

/// A Simfony program, compiled to Simplicity and satisfied with witness data.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SatisfiedProgram {
/// Simplicity target code, including witness data.
pub simplicity: Arc<RedeemNode<Elements>>,
/// Debug symbols for the Simplicity target code.
pub debug_symbols: DebugSymbols,
simplicity: Arc<RedeemNode<Elements>>,
debug_symbols: DebugSymbols,
}

pub fn satisfy(prog_text: &str, witness: &WitnessValues) -> Result<SatisfiedProgram, String> {
let parse_program = parse::Program::parse_from_str(prog_text)?;
let ast_program = ast::Program::analyze(&parse_program).with_file(prog_text)?;
let simplicity_named_construct = ast_program.compile().with_file(prog_text)?;
witness
.is_consistent(&ast_program)
.map_err(|e| e.to_string())?;

let simplicity_witness = named::to_witness_node(&simplicity_named_construct, witness);
let simplicity_redeem = simplicity_witness.finalize().map_err(|e| e.to_string())?;

Ok(SatisfiedProgram {
simplicity: simplicity_redeem,
debug_symbols: ast_program.debug_symbols(prog_text),
})
impl SatisfiedProgram {
/// Parse, compile and satisfy a Simfony program
/// from the given string and the given `witness_values`.
///
/// ## See
///
/// - [`CompiledProgram::new`]
/// - [`CompiledProgram::satisfy`]
pub fn new(s: &str, witness_values: &WitnessValues) -> Result<Self, String> {
let compiled = CompiledProgram::new(s)?;
compiled.satisfy(witness_values)
}

/// Access the Simplicity target code, including witness data.
pub fn redeem(&self) -> &Arc<RedeemNode<Elements>> {
&self.simplicity
}

/// Access the debug symbols for the Simplicity target code.
pub fn debug_symbols(&self) -> &DebugSymbols {
&self.debug_symbols
}
}

/// Recursively implement [`PartialEq`], [`Eq`] and [`std::hash::Hash`]
Expand Down Expand Up @@ -138,30 +189,34 @@ mod tests {

use crate::*;

struct Simfony<'a>(Cow<'a, str>);
struct Compiled(Arc<RedeemNode<Elements>>);

struct TestCase<T> {
program: T,
lock_time: elements::LockTime,
sequence: elements::Sequence,
}

impl<'a> TestCase<Simfony<'a>> {
impl<'a> TestCase<CompiledProgram> {
pub fn program_file<P: AsRef<Path>>(program_file_path: P) -> Self {
let program_text = std::fs::read_to_string(program_file_path).unwrap();
Self::program_text(Cow::Owned(program_text))
}

pub fn program_text(program_text: Cow<'a, str>) -> TestCase<Simfony<'a>> {
pub fn program_text(program_text: Cow<'a, str>) -> Self {
let program = match CompiledProgram::new(program_text.as_ref()) {
Ok(x) => x,
Err(error) => panic!("{error}"),
};
Self {
program: Simfony(program_text),
program,
lock_time: elements::LockTime::ZERO,
sequence: elements::Sequence::MAX,
}
}

pub fn with_witness_file<P: AsRef<Path>>(self, witness_file_path: P) -> TestCase<Compiled> {
pub fn with_witness_file<P: AsRef<Path>>(
self,
witness_file_path: P,
) -> TestCase<SatisfiedProgram> {
let witness_text = std::fs::read_to_string(witness_file_path).unwrap();
let witness_values = match serde_json::from_str::<WitnessValues>(&witness_text) {
Ok(x) => x,
Expand All @@ -170,13 +225,16 @@ mod tests {
self.with_witness_values(&witness_values)
}

pub fn with_witness_values(self, witness_values: &WitnessValues) -> TestCase<Compiled> {
let program = match satisfy(self.program.0.as_ref(), witness_values) {
pub fn with_witness_values(
self,
witness_values: &WitnessValues,
) -> TestCase<SatisfiedProgram> {
let program = match self.program.satisfy(witness_values) {
Ok(x) => x,
Err(error) => panic!("{error}"),
};
TestCase {
program: Compiled(program.simplicity),
program,
lock_time: self.lock_time,
sequence: self.sequence,
}
Expand Down Expand Up @@ -206,10 +264,10 @@ mod tests {
}
}

impl TestCase<Compiled> {
impl TestCase<SatisfiedProgram> {
#[allow(dead_code)]
pub fn print_encoding(self) -> Self {
let (program_bytes, witness_bytes) = self.program.0.encode_to_vec();
let (program_bytes, witness_bytes) = self.program.redeem().encode_to_vec();
println!(
"Program:\n{}",
Base64Display::new(&program_bytes, &STANDARD)
Expand All @@ -222,9 +280,9 @@ mod tests {
}

fn run(self) -> Result<(), simplicity::bit_machine::ExecutionError> {
let mut mac = BitMachine::for_program(&self.program.0);
let mut mac = BitMachine::for_program(self.program.redeem());
let env = dummy_env::dummy_with(self.lock_time, self.sequence);
mac.exec(&self.program.0, &env).map(|_| ())
mac.exec(self.program.redeem(), &env).map(|_| ())
}

pub fn assert_run_success(self) {
Expand Down Expand Up @@ -368,7 +426,7 @@ fn main() {
jet_verify(my_true());
}
"#;
match satisfy(prog_text, &WitnessValues::empty()) {
match SatisfiedProgram::new(prog_text, &WitnessValues::empty()) {
Ok(_) => panic!("Accepted faulty program"),
Err(error) => {
if !error.contains("Expected expression of type `bool`, found type `()`") {
Expand All @@ -380,12 +438,12 @@ fn main() {

#[test]
fn fuzz_regression_1() {
compile("type f=f").unwrap_err();
CompiledProgram::new("type f=f").unwrap_err();
}

#[test]
#[ignore]
fn fuzz_slow_unit_1() {
compile("fn fnnfn(MMet:(((sssss,((((((sssss,ssssss,ss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,ssss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,sssssssssssss,(((((((((((u|(").unwrap_err();
CompiledProgram::new("fn fnnfn(MMet:(((sssss,((((((sssss,ssssss,ss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,ssss,((((((sssss,ss,((((((sssss,ssssss,ss,((((((sssss,ssssss,((((((sssss,sssssssss,(((((((sssss,sssssssss,(((((ssss,((((((sssss,sssssssss,(((((((sssss,sssssssssssss,(((((((((((u|(").unwrap_err();
}
}
11 changes: 5 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use base64::display::Base64Display;
use base64::engine::general_purpose::STANDARD;

use simfony::witness::WitnessValues;
use simfony::{compile, satisfy};

use simfony::CompiledProgram;
use std::env;

// Directly returning Result<(), String> prints the error using Debug
Expand All @@ -30,15 +30,16 @@ fn run() -> Result<(), String> {
let prog_file = &args[1];
let prog_path = std::path::Path::new(prog_file);
let prog_text = std::fs::read_to_string(prog_path).map_err(|e| e.to_string())?;
let compiled = CompiledProgram::new(&prog_text)?;

if args.len() >= 3 {
let wit_file = &args[2];
let wit_path = std::path::Path::new(wit_file);
let wit_text = std::fs::read_to_string(wit_path).map_err(|e| e.to_string())?;
let witness = serde_json::from_str::<WitnessValues>(&wit_text).unwrap();

let program = satisfy(&prog_text, &witness)?;
let (program_bytes, witness_bytes) = program.simplicity.encode_to_vec();
let satisfied = compiled.satisfy(&witness)?;
let (program_bytes, witness_bytes) = satisfied.redeem().encode_to_vec();
println!(
"Program:\n{}",
Base64Display::new(&program_bytes, &STANDARD)
Expand All @@ -48,9 +49,7 @@ fn run() -> Result<(), String> {
Base64Display::new(&witness_bytes, &STANDARD)
);
} else {
// No second argument is provided. Just compile the program.
let program = compile(&prog_text)?;
let program_bytes = program.encode_to_vec();
let program_bytes = compiled.commit().encode_to_vec();
println!(
"Program:\n{}",
Base64Display::new(&program_bytes, &STANDARD)
Expand Down
15 changes: 7 additions & 8 deletions src/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt;

use serde::{de, Deserialize, Deserializer};

use crate::ast;
use crate::ast::DeclaredWitnesses;
use crate::error::{Error, RichError, WithFile, WithSpan};
use crate::parse::ParseFromStr;
use crate::str::WitnessName;
Expand Down Expand Up @@ -41,7 +41,7 @@ impl WitnessValues {
}
}

/// Check if the witness values are consistent with the witness types as declared in the program.
/// Check if the witness values are consistent with the declared witness types.
///
/// 1. Values that occur in the program are type checked.
/// 2. Values that don't occur in the program are skipped.
Expand All @@ -51,10 +51,9 @@ impl WitnessValues {
/// in the witness map. These witnesses may lie on pruned branches that will not be part of the
/// finalized Simplicity program. However, before the finalization, we cannot know which
/// witnesses will be pruned and which won't be pruned. This check skips unassigned witnesses.
pub fn is_consistent(&self, program: &ast::Program) -> Result<(), Error> {
let declared = program.witnesses();
pub fn is_consistent(&self, witness_types: &DeclaredWitnesses) -> Result<(), Error> {
for name in self.0.keys() {
let declared_ty = match declared.get(name) {
let declared_ty = match witness_types.get(name) {
Some(ty) => ty,
None => continue,
};
Expand Down Expand Up @@ -212,8 +211,8 @@ impl<'de> Deserialize<'de> for WitnessName {
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
use crate::value::ValueConstructible;
use crate::{ast, parse, CompiledProgram, SatisfiedProgram};

#[test]
fn witness_reuse() {
Expand Down Expand Up @@ -241,7 +240,7 @@ mod tests {
let a = WitnessName::parse_from_str("a").unwrap();
witness.insert(a, Value::u16(42)).unwrap();

match crate::satisfy(s, &witness) {
match SatisfiedProgram::new(s, &witness) {
Ok(_) => panic!("Ill-typed witness assignment was falsely accepted"),
Err(error) => assert_eq!(
"Witness `a` was declared with type `u32` but its assigned value is of type `u16`",
Expand Down Expand Up @@ -288,7 +287,7 @@ fn main() {
assert!(jet::is_zero_32(f()));
}"#;

match crate::compile(s) {
match CompiledProgram::new(s) {
Ok(_) => panic!("Witness outside main was falsely accepted"),
Err(error) => {
assert!(error
Expand Down

0 comments on commit 3d2b242

Please sign in to comment.