Skip to content

Commit

Permalink
Merge #209: Better human
Browse files Browse the repository at this point in the history
05c81ba Test: Witness nodes have exactly one name (Christian Lewe)
61c5227 Test: Human encoding: Refactor parsing (Christian Lewe)
b0c0ef7 Test: Human encoding: Filled holes (Christian Lewe)
e8e82ac Test: Human encoding: Filled witness (Christian Lewe)
b8ccec8 Test: Human encoding: Refactor finalization (Christian Lewe)
9c45abf Test: Human encoding: Move parsing test (Christian Lewe)
8a5fbbc Doc: Forest::to_witness_node (Christian Lewe)
37d728c Human encoding: Store hole name in named disc nodes (Christian Lewe)
f207d6e Derive traits (Christian Lewe)

Pull request description:

  Fixes #181

ACKs for top commit:
  apoelstra:
    ACK 05c81ba

Tree-SHA512: c73d49cf16985d99c2b1a6086be8398a036d1367eaf7e9bd00308c58f1887e54c8bb288b0188470908d443804b3860bd271deb7373daeb29ac2fe82616a906db
apoelstra committed Mar 17, 2024

Verified

This commit was signed with the committer’s verified signature.
apoelstra Andrew Poelstra
2 parents e8cfeeb + 05c81ba commit 7b5b738
Showing 5 changed files with 228 additions and 108 deletions.
3 changes: 3 additions & 0 deletions src/dag.rs
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ use crate::node::{self, Disconnectable, Node};
/// as whether they represent `witness` or `disconnect` combinators, which
/// are treated specially when working with the graph structure of
/// Simplicty programs.
#[derive(Clone, Debug)]
pub enum Dag<T> {
/// Combinator with no children
Nullary,
@@ -347,6 +348,7 @@ pub trait DagLike: Sized {
/// To avoid confusion, this structure cannot be directly costructed.
/// Instead it is implicit in the [`DagLike::rtl_post_order_iter`]
/// method.
#[derive(Clone, Debug)]
pub struct SwapChildren<D>(D);

impl<D: DagLike> DagLike for SwapChildren<D> {
@@ -510,6 +512,7 @@ pub struct PostOrderIter<D, S> {
}

/// A set of data yielded by a `PostOrderIter`
#[derive(Clone, Debug)]
pub struct PostOrderIterItem<D> {
/// The actual node data
pub node: D,
215 changes: 141 additions & 74 deletions src/human_encoding/mod.rs
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ mod serialize;

use crate::dag::{DagLike, MaxSharing};
use crate::jet::Jet;
use crate::node::{self, CommitNode};
use crate::node::{self, CommitNode, NoWitness};
use crate::{Cmr, Imr, Value, WitnessNode};

use std::collections::HashMap;
@@ -22,7 +22,6 @@ use std::sync::Arc;

pub use self::error::{Error, ErrorSet};
pub use self::named_node::NamedCommitNode;
pub use self::parse::parse;

/// Line/column pair
///
@@ -77,6 +76,12 @@ impl WitnessOrHole {
}
}

impl<'a> From<&'a NoWitness> for WitnessOrHole {
fn from(_: &NoWitness) -> Self {
WitnessOrHole::Witness
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Forest<J: Jet> {
roots: HashMap<Arc<str>, Arc<NamedCommitNode<J>>>,
@@ -136,7 +141,6 @@ impl<J: Jet> Forest<J> {
let mut witness_lines = vec![];
let mut const_lines = vec![];
let mut program_lines = vec![];
let mut disc_idx = 0;
// Pass 1: compute string data for every node
for root in self.roots.values() {
for data in root.as_ref().post_order_iter::<MaxSharing<_>>() {
@@ -161,9 +165,8 @@ impl<J: Jet> Forest<J> {
} else if let node::Inner::AssertL(_, cmr) = node.inner() {
expr_str.push_str(" #");
expr_str.push_str(&cmr.to_string());
} else if let node::Inner::Disconnect(_, node::NoDisconnect) = node.inner() {
expr_str.push_str(&format!(" ?disc_expr_{}", disc_idx));
disc_idx += 1;
} else if let node::Inner::Disconnect(_, hole_name) = node.inner() {
expr_str.push_str(&format!(" ?{}", hole_name));
}

let arrow = node.arrow();
@@ -205,6 +208,9 @@ impl<J: Jet> Forest<J> {
ret
}

/// Convert the forest into a witness node.
///
/// Succeeds if the forest contains a "main" root and returns `None` otherwise.
pub fn to_witness_node(
&self,
witness: &HashMap<Arc<str>, Arc<Value>>,
@@ -217,99 +223,160 @@ impl<J: Jet> Forest<J> {
#[cfg(test)]
mod tests {
use crate::human_encoding::Forest;
use crate::jet::Core;
use crate::{Error, Value};
use crate::jet::{Core, Jet};
use crate::{BitMachine, Value};
use std::collections::HashMap;
use std::sync::Arc;

#[test]
fn to_witness_node_missing_witness() {
let s = "
wit1 := witness : 1 -> 2^32
wit2 := witness : 1 -> 2^32
fn assert_finalize_ok<J: Jet>(
s: &str,
witness: &HashMap<Arc<str>, Arc<Value>>,
env: &J::Environment,
) {
let program = Forest::<J>::parse(s)
.expect("Failed to parse human encoding")
.to_witness_node(witness)
.expect("Forest is missing expected root")
.finalize()
.expect("Failed to finalize");
let mut mac = BitMachine::for_program(&program);
mac.exec(&program, env).expect("Failed to run program");
}

wits_are_equal := comp (pair wit1 wit2) jet_eq_32 : 1 -> 2
main := comp wits_are_equal jet_verify : 1 -> 1
";
let forest = Forest::<Core>::parse(s).unwrap();
let mut witness = HashMap::new();
witness.insert(Arc::from("wit1"), Value::u32(1337));

match forest.to_witness_node(&witness).unwrap().finalize() {
Ok(_) => panic!("Insufficient witness map should fail"),
Err(Error::IncompleteFinalization) => {}
Err(error) => panic!("Unexpected error {}", error),
fn assert_finalize_err<J: Jet>(
s: &str,
witness: &HashMap<Arc<str>, Arc<Value>>,
env: &J::Environment,
err_msg: &'static str,
) {
let program = match Forest::<J>::parse(s)
.expect("Failed to parse human encoding")
.to_witness_node(witness)
.expect("Forest is missing expected root")
.finalize()
{
Ok(program) => program,
Err(error) => {
assert_eq!(&error.to_string(), err_msg);
return;
}
};
let mut mac = BitMachine::for_program(&program);
match mac.exec(&program, env) {
Ok(_) => panic!("Execution is expected to fail"),
Err(error) => assert_eq!(&error.to_string(), err_msg),
}
}

#[test]
fn parse_duplicate_witness_in_disconnected_branch() {
fn filled_witness() {
let s = "
wit1 := witness
main := comp wit1 comp disconnect iden ?dis2 unit
wit1 := witness
dis2 := wit1
a := witness
b := witness
main := comp
comp
pair a b
jet_lt_8
jet_verify
";

match Forest::<Core>::parse(s) {
Ok(_) => panic!("Duplicate witness names should fail"),
Err(set) => {
let errors: Vec<_> = set.iter().collect();
assert_eq!(1, errors.len());
match errors[0] {
super::error::Error::NameRepeated { .. } => {}
error => panic!("Unexpected error {}", error),
}
}
}
let a_less_than_b = HashMap::from([
(Arc::from("a"), Value::u8(0x00)),
(Arc::from("b"), Value::u8(0x01)),
]);
assert_finalize_ok::<Core>(s, &a_less_than_b, &());

let b_greater_equal_a = HashMap::from([
(Arc::from("a"), Value::u8(0x01)),
(Arc::from("b"), Value::u8(0x01)),
]);
assert_finalize_err::<Core>(s, &b_greater_equal_a, &(), "Jet failed during execution");
}

#[test]
fn to_witness_node_unfilled_hole() {
let s = "
wit1 := witness
main := comp wit1 comp disconnect iden ?dis2 unit
";
let forest = Forest::<Core>::parse(s).unwrap();
let witness = HashMap::new();

match forest.to_witness_node(&witness).unwrap().finalize() {
Ok(_) => panic!("Duplicate witness names should fail"),
Err(Error::IncompleteFinalization) => {}
Err(error) => panic!("Unexpected error {}", error),
}
fn unfilled_witness() {
let witness = HashMap::from([(Arc::from("wit1"), Value::u32(1337))]);
assert_finalize_err::<Core>(
"
wit1 := witness : 1 -> 2^32
wit2 := witness : 1 -> 2^32
wits_are_equal := comp (pair wit1 wit2) jet_eq_32 : 1 -> 2
main := comp wits_are_equal jet_verify : 1 -> 1
",
&witness,
&(),
"unable to satisfy program",
);
}

#[test]
fn to_witness_node_pruned_witness() {
fn unfilled_witness_pruned() {
let s = "
wit1 := witness
wit2 := witness
main := comp (pair wit1 unit) case unit wit2
";
let forest = Forest::<Core>::parse(s).unwrap();
let mut witness = HashMap::new();
witness.insert(Arc::from("wit1"), Value::u1(0));

match forest.to_witness_node(&witness).unwrap().finalize() {
Ok(_) => {}
Err(e) => panic!("Unexpected error {}", e),
}
let wit2_is_pruned = HashMap::from([(Arc::from("wit1"), Value::u1(0))]);
assert_finalize_ok::<Core>(s, &wit2_is_pruned, &());

let wit2_is_missing = HashMap::from([(Arc::from("wit1"), Value::u1(1))]);
// FIXME The finalization should fail
// This doesn't happen because we don't run the program,
// so we cannot always determine which nodes must be pruned
assert_finalize_err::<Core>(
s,
&wit2_is_missing,
&(),
"Execution reached a pruned branch: bf12681a76fc7c00c63e583c25cc97237337d6aca30d3f4a664075445385c648"
);

let wit2_is_present = HashMap::from([
(Arc::from("wit1"), Value::u1(1)),
(Arc::from("wit2"), Value::unit()),
]);
assert_finalize_ok::<Core>(s, &wit2_is_present, &());
}

witness.insert(Arc::from("wit1"), Value::u1(1));
#[test]
fn filled_hole() {
let empty = HashMap::new();
assert_finalize_ok::<Core>(
"
id1 := iden : 2^256 * 1 -> 2^256 * 1
main := comp (disconnect id1 ?hole) unit
hole := unit
",
&empty,
&(),
);
}

match forest.to_witness_node(&witness).unwrap().finalize() {
Ok(_) => {}
Err(Error::IncompleteFinalization) => {}
Err(e) => panic!("Unexpected error {}", e),
}
#[test]
fn unfilled_hole() {
let empty = HashMap::new();
assert_finalize_err::<Core>(
"
wit1 := witness
main := comp wit1 comp disconnect iden ?dis2 unit
",
&empty,
&(),
"unable to satisfy program",
);
}

witness.insert(Arc::from("wit2"), Value::unit());
#[test]
fn witness_name_override() {
let s = "
wit1 := witness
wit2 := wit1
main := comp wit2 iden
";
let wit1_populated = HashMap::from([(Arc::from("wit1"), Value::unit())]);
assert_finalize_err::<Core>(s, &wit1_populated, &(), "unable to satisfy program");

match forest.to_witness_node(&witness).unwrap().finalize() {
Ok(_) => {}
Err(e) => panic!("Unexpected error {}", e),
}
let wit2_populated = HashMap::from([(Arc::from("wit2"), Value::unit())]);
assert_finalize_ok::<Core>(s, &wit2_populated, &());
}
}
56 changes: 35 additions & 21 deletions src/human_encoding/named_node.rs
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ pub struct Named<N> {
impl<J: Jet> node::Marker for Named<Commit<J>> {
type CachedData = NamedCommitData<J>;
type Witness = <Commit<J> as node::Marker>::Witness;
type Disconnect = <Commit<J> as node::Marker>::Disconnect;
type Disconnect = Arc<str>;
type SharingId = Arc<str>;
type Jet = J;

@@ -90,7 +90,7 @@ impl<J: Jet> NamedCommitNode<J> {
&mut self,
_: &PostOrderIterItem<&NamedCommitNode<J>>,
_: Option<&Arc<CommitNode<J>>>,
_: &NoDisconnect,
_: &Arc<str>,
) -> Result<NoDisconnect, Self::Error> {
Ok(NoDisconnect)
}
@@ -140,16 +140,19 @@ impl<J: Jet> NamedCommitNode<J> {
&mut self,
data: &PostOrderIterItem<&Node<Named<Commit<J>>>>,
maybe_converted: Option<&Arc<Node<Witness<J>>>>,
_: &NoDisconnect,
_: &Arc<str>,
) -> Result<Option<Arc<Node<Witness<J>>>>, Self::Error> {
if let Some(converted) = maybe_converted {
Ok(Some(converted.clone()))
} else {
let name = &data.node.cached_data().name;
let hole_name = match data.node.inner() {
Inner::Disconnect(_, hole_name) => hole_name,
_ => unreachable!(),
};
// We keep the missing disconnected branches empty.
// Like witness nodes (see above), disconnect nodes may be pruned later.
// The finalization will detect missing branches and throw an error.
let maybe_commit = self.1.get(name);
let maybe_commit = self.1.get(hole_name);
// FIXME: Recursive call of to_witness_node
// We cannot introduce a stack
// because we are implementing methods of the trait Converter
@@ -353,26 +356,28 @@ impl<J: Jet> NamedConstructNode<J> {
data: &PostOrderIterItem<&NamedConstructNode<J>>,
_: Option<&Arc<NamedCommitNode<J>>>,
disc: &Arc<NamedConstructNode<J>>,
) -> Result<NoDisconnect, Self::Error> {
if !matches!(
disc.inner(),
node::Inner::Witness(WitnessOrHole::TypedHole(..))
) {
self.errors
.add(data.node.position(), Error::HoleFilledAtCommitTime);
) -> Result<Arc<str>, Self::Error> {
match disc.inner() {
node::Inner::Witness(WitnessOrHole::TypedHole(hole_name)) => {
Ok(hole_name.clone())
}
_ => {
self.errors
.add(data.node.position(), Error::HoleFilledAtCommitTime);
Ok(Arc::from(""))
}
}
Ok(NoDisconnect)
}

fn convert_data(
&mut self,
data: &PostOrderIterItem<&NamedConstructNode<J>>,
inner: node::Inner<&Arc<NamedCommitNode<J>>, J, &NoDisconnect, &NoWitness>,
inner: node::Inner<&Arc<NamedCommitNode<J>>, J, &Arc<str>, &NoWitness>,
) -> Result<NamedCommitData<J>, Self::Error> {
let converted_data = inner
.as_ref()
.map(|node| &node.cached_data().internal)
.map_disconnect(|disc| *disc)
.map_disconnect(|_| &NoDisconnect)
.copy_witness();

if !self.for_main {
@@ -461,6 +466,7 @@ impl<J: Jet> NamedConstructNode<J> {
}
}

#[derive(Clone, Debug)]
pub struct Namer {
const_idx: usize,
wit_idx: usize,
@@ -491,7 +497,7 @@ impl Namer {
}

/// Generate a fresh name for the given node.
pub fn assign_name<C, J, X, W>(&mut self, inner: node::Inner<C, J, X, W>) -> String {
pub fn assign_name<C, J, X>(&mut self, inner: node::Inner<C, J, X, &WitnessOrHole>) -> String {
let prefix = match inner {
node::Inner::Iden => "id",
node::Inner::Unit => "ut",
@@ -505,7 +511,10 @@ impl Namer {
node::Inner::AssertR(..) => "asstr",
node::Inner::Pair(..) => "pr",
node::Inner::Disconnect(..) => "disc",
node::Inner::Witness(..) => "wit",
node::Inner::Witness(WitnessOrHole::Witness) => "wit",
node::Inner::Witness(WitnessOrHole::TypedHole(name)) => {
return name.to_string();
}
node::Inner::Fail(..) => "FAIL",
node::Inner::Jet(..) => "jt",
node::Inner::Word(..) => "const",
@@ -535,14 +544,16 @@ impl<J: Jet> Converter<Commit<J>, Named<Commit<J>>> for Namer {
_: &PostOrderIterItem<&CommitNode<J>>,
_: Option<&Arc<NamedCommitNode<J>>>,
_: &NoDisconnect,
) -> Result<NoDisconnect, Self::Error> {
Ok(NoDisconnect)
) -> Result<Arc<str>, Self::Error> {
let hole_idx = self.other_idx;
self.other_idx += 1;
Ok(Arc::from(format!("hole {hole_idx}")))
}

fn convert_data(
&mut self,
data: &PostOrderIterItem<&CommitNode<J>>,
inner: node::Inner<&Arc<NamedCommitNode<J>>, J, &NoDisconnect, &NoWitness>,
inner: node::Inner<&Arc<NamedCommitNode<J>>, J, &Arc<str>, &NoWitness>,
) -> Result<NamedCommitData<J>, Self::Error> {
// Special-case the root node, which is always called main.
// The CMR of the root node, conveniently, is guaranteed to be
@@ -556,7 +567,10 @@ impl<J: Jet> Converter<Commit<J>, Named<Commit<J>>> for Namer {

Ok(NamedCommitData {
internal: Arc::clone(data.node.cached_data()),
name: Arc::from(self.assign_name(inner).as_str()),
name: Arc::from(
self.assign_name(inner.map_witness(WitnessOrHole::from).as_ref())
.as_str(),
),
})
}
}
51 changes: 38 additions & 13 deletions src/human_encoding/parse/mod.rs
Original file line number Diff line number Diff line change
@@ -583,7 +583,7 @@ mod tests {
fn assert_cmr_witness<J: Jet>(
s: &str,
cmr: &str,
witness: &[(&str, &[u8])],
witness: &HashMap<Arc<str>, Arc<Value>>,
env: &J::Environment,
) {
match parse::<J>(s) {
@@ -592,12 +592,8 @@ mod tests {
let main = &forest["main"];
assert_eq!(main.cmr().to_string(), cmr);

let witness: HashMap<_, _> = witness
.iter()
.map(|(name, value)| (Arc::from(*name), Value::power_of_two(value)))
.collect();
let program = main
.to_witness_node(&witness, &forest)
.to_witness_node(witness, &forest)
.finalize()
.expect("finalize");

@@ -663,13 +659,18 @@ mod tests {

#[test]
fn simple_program() {
let empty = HashMap::new();
assert_cmr_witness::<Core>(
"main := unit",
"62274a89833ece8ba5ff57b28118c0063d3d4a85dd25aae06f87617604402715",
&[],
&empty,
&(),
);

let witness = HashMap::from([
(Arc::from("wit1"), Value::u32(0x00010203)),
(Arc::from("wit2"), Value::u32(0x00010203)),
]);
assert_cmr_witness::<Core>(
"
wit1 := witness : 1 -> 2^32
@@ -679,7 +680,7 @@ mod tests {
main := comp wits_are_equal jet_verify : 1 -> 1
",
"2d170e731b6d6856e69f3c6ee04b368302f7f71b2270a26276d98ea494bbebd7",
&[("wit1", &[0, 1, 2, 3]), ("wit2", &[0, 1, 2, 3])],
&witness,
&(),
);
}
@@ -704,10 +705,16 @@ mod tests {
use crate::jet::elements::ElementsEnv;
use crate::jet::Elements;

parse::<Elements>("main := unit").unwrap();
let empty = HashMap::new();
let dummy = ElementsEnv::dummy();
assert_cmr_witness::<Elements>(
"main := unit",
"62274a89833ece8ba5ff57b28118c0063d3d4a85dd25aae06f87617604402715",
&empty,
&dummy,
);

// See https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
let env = ElementsEnv::dummy();
let sig = [
0xe9, 0x07, 0x83, 0x1f, 0x80, 0x84, 0x8d, 0x10, 0x69, 0xa5, 0x37, 0x1b, 0x40, 0x24,
0x10, 0x36, 0x4b, 0xdf, 0x1c, 0x5f, 0x83, 0x07, 0xb0, 0x08, 0x4c, 0x55, 0xf1, 0xce,
@@ -716,7 +723,9 @@ mod tests {
0x7d, 0xf4, 0x90, 0x0d, 0x31, 0x05, 0x36, 0xc0,
];

assert_cmr_witness::<Elements>("
let signature = HashMap::from([(Arc::from("wit1"), Value::power_of_two(sig.as_ref()))]);
assert_cmr_witness::<Elements>(
"
-- Witnesses
wit1 := witness : 1 -> 2^512
@@ -730,8 +739,8 @@ mod tests {
main := comp pr1 jt2 : 1 -> 1 -- 3f6422da
",
"dacbdfcf64122edf8efda2b34fe353cac4424dd455a9204fc92af258b465bbc4",
&[("wit1", &sig)],
&env
&signature,
&dummy
);
}

@@ -762,4 +771,20 @@ mod tests {
assert_const::<Core>(s.as_str(), value);
}
}

#[test]
fn duplicate_witness_in_disconnected_branch() {
error_contains(
parse::<Core>(
"
wit1 := witness
main := comp wit1 comp disconnect iden ?dis2 unit
wit1 := witness
dis2 := wit1
",
),
"name `wit1` occured mulitple times",
);
}
}
11 changes: 11 additions & 0 deletions src/node/disconnect.rs
Original file line number Diff line number Diff line change
@@ -73,3 +73,14 @@ impl<L> Disconnectable<L> for Option<Arc<L>> {
}
}
}

// `Arc<str>` is like `NoDisconnect` but it names the expression that should fill the hole
impl<L> Disconnectable<L> for Arc<str> {
fn disconnect_dag_arc(self, other: Arc<L>) -> Dag<Arc<L>> {
Dag::Unary(other)
}

fn disconnect_dag_ref<'s>(&'s self, other: &'s L) -> Dag<&'s L> {
Dag::Unary(other)
}
}

0 comments on commit 7b5b738

Please sign in to comment.