diff --git a/CHANGELOG.md b/CHANGELOG.md index 197b6fb2..94e69c6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* Fix `RedeemNode::prune` producing programs that consensus rejects, and add `ExecutionError::PruningFailed` [#391](https://github.com/BlockstreamResearch/rust-simplicity/issues/391). + # 0.9.0 - 2026-09-16 * Update `elements` to `0.27.0`, `bitcoin_hashes` to `1.2.0`, and `hex-conservative` to `1.2.0` [#368](https://github.com/BlockstreamResearch/rust-simplicity/pull/368), [#373](https://github.com/BlockstreamResearch/rust-simplicity/pull/373), [#376](https://github.com/BlockstreamResearch/rust-simplicity/pull/376). diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index d2f8751c..c0ae42e1 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -560,6 +560,8 @@ pub enum ExecutionError { JetFailed(JetFailed), /// Jet type mismatch between program and execution environment JetTypeMismatch, + /// Pruning could not produce a program which consensus accepts + PruningFailed, } impl fmt::Display for ExecutionError { @@ -579,6 +581,9 @@ impl fmt::Display for ExecutionError { ExecutionError::JetTypeMismatch => { f.write_str("Jet type mismatch between program and execution environment") } + ExecutionError::PruningFailed => { + f.write_str("Pruning could not produce a program which consensus accepts") + } } } } @@ -589,7 +594,8 @@ impl error::Error for ExecutionError { Self::InputWrongType(..) | Self::ReachedFailNode(..) | Self::ReachedPrunedBranch(..) - | Self::JetTypeMismatch => None, + | Self::JetTypeMismatch + | Self::PruningFailed => None, Self::LimitExceeded(ref e) => Some(e), Self::JetFailed(ref e) => Some(e), } diff --git a/src/node/mod.rs b/src/node/mod.rs index 4f7dba76..99afe1e1 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -78,6 +78,7 @@ mod disconnect; mod display; mod hiding; mod inner; +mod prune; mod redeem; use crate::value::Word; diff --git a/src/node/prune.rs b/src/node/prune.rs new file mode 100644 index 00000000..302bdfcb --- /dev/null +++ b/src/node/prune.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Pruning of redeem programs +//! +//! See [`RedeemNode::prune`] for how a program is pruned. + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use crate::bit_machine::{ExecTracker, ExecutionError, FrameIter, NodeOutput, PruneTracker}; +use crate::dag::{DagLike, InternalSharing, MaxSharing, PostOrderIterItem}; +use crate::jet::JetEnvironment; +use crate::types; +use crate::{BitIter, BitMachine, Ihr, Value}; + +use super::{ + Construct, ConstructData, ConstructNode, Constructible, Converter, Hide, Inner, Node, Redeem, + RedeemData, RedeemNode, +}; + +/// Prunes `program`, as described in [`RedeemNode::prune`]. +pub(super) fn prune( + program: &RedeemNode, + env: &JE, + tracker: &mut T, +) -> Result, ExecutionError> { + // 1) Merge nodes with equal IHRs. The IHR covers the source and target types of a + // node but not the types inside it, so nodes with equal IHRs can contain case + // nodes with different IHRs, which the tracker would mark separately, while the + // encoding of the pruned program has a single node for all of them. + let program = reduce_to_max_sharing(program); + + // 2) Run the Bit Machine and mark (un)used branches. + // This is the only fallible step in the pruning process. + let mut mac = BitMachine::for_program(&program)?; + mac.exec_with_tracker(&program, env, tracker)?; + + // 3) Find the nodes to keep: those reachable from the root through used branches. + let tracker = &*tracker; + let kept = kept_nodes(&program, |ihr| { + (tracker.contains_left(ihr), tracker.contains_right(ihr)) + }); + + // 4) Rebuild the program from the kept nodes and prune out unused case branches. + let pruned = rebuild(&program, &kept); + + // 5) Check the encoding of the pruned program as consensus does (Consensus does + // not allow any node to be unused). + let arrow = pruned.arrow(); + // If the pruned program is not unit to unit, it is not a valid redeem program. + // This can happen through fuzzing or calling pruning on a non-valid program. + if arrow.source.is_unit() && arrow.target.is_unit() { + check_encoding(&pruned, env)?; + } + Ok(pruned) +} + +fn reduce_to_max_sharing(program: &RedeemNode) -> Arc { + program + .convert::, _, _>(&mut Identity) + .expect("copying a program is infallible") +} + +/// Converts a redeem program into a copy of itself. Converting with [`MaxSharing`] +/// gives a copy with a single node for each IHR, as in the program's encoding. +struct Identity; + +impl Converter for Identity { + type Error = std::convert::Infallible; + + fn convert_witness( + &mut self, + _: &PostOrderIterItem<&RedeemNode>, + witness: &Value, + ) -> Result { + Ok(witness.shallow_clone()) + } + + fn convert_disconnect( + &mut self, + _: &PostOrderIterItem<&RedeemNode>, + right: Option<&Arc>, + _: &Arc, + ) -> Result, Self::Error> { + Ok(Arc::clone(right.expect( + "disconnect node in redeem program has a right child", + ))) + } + + fn convert_data( + &mut self, + data: &PostOrderIterItem<&RedeemNode>, + inner: Inner<&Arc, &Arc, &Value>, + ) -> Result, Self::Error> { + let converted_inner = inner + .map(|node| node.cached_data()) + .map_disconnect(|node| node.cached_data()) + .map_witness(Value::shallow_clone); + Ok(Arc::new(RedeemData::new( + data.node.arrow().shallow_clone(), + converted_inner, + ))) + } +} + +/// Returns which branch of a case node to hide, given whether its left and +/// right branches were used. +fn branch_to_hide(used_left: bool, used_right: bool) -> Hide { + match (used_left, used_right) { + (true, true) => Hide::Neither, + (false, true) => Hide::Left, + (true, false) => Hide::Right, + // This is not possible with the default tracker, but if a custom + // tracker is used with `prune_with_tracker`, we might end up here. It's + // difficult to know the reason, but it is most likely unintended. + (false, false) => panic!("both branches of a case node were unused, which is not possible with the default tracker"), + } +} + +/// Walks the program from the root, descending only into used branches, +/// and returns the IHRs of the nodes to keep together with which branches +/// to hide for each of them. `used` returns whether the left and right +/// branches of the case node with the given IHR were used. +/// +/// `root` must have a single node for each IHR, as `reduce_to_max_sharing` +/// gives: nodes are identified by IHR, so a node whose IHR is already kept is +/// the same node, with the same children. +fn kept_nodes(root: &RedeemNode, mut used: impl FnMut(Ihr) -> (bool, bool)) -> HashMap { + let mut kept = HashMap::new(); + let mut stack = vec![root]; + while let Some(node) = stack.pop() { + if kept.contains_key(&node.ihr()) { + continue; + } + let hide = match node.inner() { + Inner::Case(..) => { + let (used_left, used_right) = used(node.ihr()); + branch_to_hide(used_left, used_right) + } + _ => Hide::Neither, + }; + kept.insert(node.ihr(), hide); + match (node.inner(), hide) { + (Inner::Case(_, right), Hide::Left) => stack.push(right), + (Inner::Case(left, _), Hide::Right) => stack.push(left), + _ => { + stack.extend(node.left_child()); + stack.extend(node.right_child()); + } + } + } + kept +} + +/// Rebuilds the program bottom-up from the kept nodes alone, and turns case +/// nodes with an unused branch into assertions. +/// +/// `program` must be the program which `kept` came from, with a single node +/// for each IHR. +fn rebuild(program: &RedeemNode, kept: &HashMap) -> Arc { + // Nodes in hidden branches are never built, so they cannot constrain the + // types of the pruned program. + // Because the types of the pruned program may change, + // we construct a temporary witness program with unfinalized types. + types::Context::with_context(|inference_context| { + let mut built: HashMap> = HashMap::new(); + for data in program.post_order_iter::>() { + let node = data.node; + let hide = match kept.get(&node.ihr()) { + Some(&hide) => hide, + // Inside a hidden branch. + None => continue, + }; + // The program has one node for each IHR, so the walk kept the same + // children, and children are visited before their parents, so they + // are built. + let child = |child: &Arc| { + Arc::clone( + built + .get(&child.ihr()) + .expect("children of kept nodes are kept"), + ) + }; + let inner = match (node.inner(), hide) { + (Inner::Case(left, right), Hide::Left) => Inner::AssertR(left.cmr(), child(right)), + (Inner::Case(left, right), Hide::Right) => Inner::AssertL(child(left), right.cmr()), + (inner, _) => inner + .as_ref() + .map(&child) + .map_disconnect(|right| Some(child(right))) + // The pruned type is not finalized at this point, + // so we cannot prune the witness value. + .map_witness(|witness| Some(witness.shallow_clone())), + }; + let data = ConstructData::from_inner( + &inference_context, + inner + .as_ref() + .map(|node| node.cached_data()) + .map_witness(Option::::clone), + ) + .expect("pruned types should check out if unpruned types check out"); + built.insert(node.ihr(), Arc::new(Node::from_parts(inner, data))); + } + let pruned_witness_program = built.remove(&program.ihr()).expect("root is kept"); + // Finalize the types of the witness program. + // We obtain the pruned redeem program. + // Once the pruned type is finalized, we can proceed to prune witness values. + pruned_witness_program + .convert::(&mut Finalizer) + .expect("finalization is infallible") + }) +} + +/// Finalizes the types of a pruned program, and prunes its witness values to them. +struct Finalizer; + +impl<'brand> Converter, Redeem> for Finalizer { + type Error = std::convert::Infallible; + + fn convert_witness( + &mut self, + data: &PostOrderIterItem<&ConstructNode>, + witness: &Option, + ) -> Result { + let pruned_target_ty = data + .node + .arrow() + .target + .finalize() + .expect("pruned types should check out if unpruned types check out"); + let pruned_witness = witness + .as_ref() + .expect("witness node that originally stems from redeem program should be populated") + .prune(&pruned_target_ty) + .expect("pruned type should be shrunken version of unpruned type"); + Ok(pruned_witness) + } + + fn convert_disconnect( + &mut self, + _: &PostOrderIterItem<&ConstructNode>, + right: Option<&Arc>, + _: &Option>, + ) -> Result, Self::Error> { + Ok(right.map(Arc::clone).expect( + "disconnect node that originally stems from redeem program should have all branches", + )) + } + + fn convert_data( + &mut self, + data: &PostOrderIterItem<&ConstructNode>, + inner: Inner<&Arc, &Arc, &Value>, + ) -> Result, Self::Error> { + // Finalize target types of witness nodes in advance so we can prune their values. + let final_arrow = data + .node + .arrow() + .finalize() + .expect("pruned types should check out if unpruned types check out"); + let converted_inner = inner + .map(|node| node.cached_data()) + .map_disconnect(|node| node.cached_data()) + .map_witness(Value::shallow_clone); + Ok(Arc::new(RedeemData::new(final_arrow, converted_inner))) + } +} + +/// Records which nodes ran, and which branches of each case node ran. +#[derive(Default)] +struct AntiDosTracker { + ran: HashSet, + left: HashSet, + right: HashSet, +} + +impl ExecTracker for AntiDosTracker { + fn visit_node(&mut self, node: &RedeemNode, mut input: FrameIter, _: NodeOutput) { + self.ran.insert(node.ihr()); + if let Inner::Case(..) = node.inner() { + match input.next() { + Some(false) => self.left.insert(node.ihr()), + Some(true) => self.right.insert(node.ihr()), + None => false, + }; + } + } +} + +/// Checks the encoding of a pruned program as consensus does: that it decodes, +/// runs, and runs every node and both branches of every case node. +fn check_encoding(pruned: &RedeemNode, env: &JE) -> Result<(), ExecutionError> { + let (program, witness) = pruned.to_vec_with_witness(); + let decoded = RedeemNode::decode::<_, _, JE::Jet>( + BitIter::from(&program[..]), + BitIter::from(&witness[..]), + ) + .map_err(|_| ExecutionError::PruningFailed)?; + // A decoder infers the types from the encoding, so different types + // would mean that the witness data is read differently. + if decoded.ihr() != pruned.ihr() { + return Err(ExecutionError::PruningFailed); + } + let mut tracker = AntiDosTracker::default(); + BitMachine::for_program(&decoded)? + .exec_with_tracker(&decoded, env, &mut tracker) + .map_err(|_| ExecutionError::PruningFailed)?; + for data in decoded.as_ref().post_order_iter::() { + let ihr = data.node.ihr(); + let ran = match data.node.inner() { + Inner::Case(..) => tracker.left.contains(&ihr) && tracker.right.contains(&ihr), + _ => tracker.ran.contains(&ihr), + }; + if !ran { + return Err(ExecutionError::PruningFailed); + } + } + Ok(()) +} diff --git a/src/node/redeem.rs b/src/node/redeem.rs index 89202b5d..1607ad1a 100644 --- a/src/node/redeem.rs +++ b/src/node/redeem.rs @@ -3,14 +3,14 @@ use crate::analysis::NodeBounds; use crate::bit_machine::{ExecutionError, PruneTracker, SetTracker}; use crate::dag::{DagLike, InternalSharing, MaxSharing, PostOrderIterItem}; +use crate::encode; use crate::jet::{Jet, JetEnvironment}; use crate::types::{self, FinalArrow}; -use crate::{encode, BitMachine}; use crate::{Amr, BitIter, BitWriter, Cmr, DecodeError, Ihr, Imr, Value}; use super::{ Commit, CommitData, CommitNode, Construct, ConstructData, ConstructNode, Constructible, - Converter, Hide, Inner, Marker, NoDisconnect, NoWitness, Node, + Converter, Inner, Marker, NoDisconnect, NoWitness, Node, }; use std::collections::HashSet; @@ -267,9 +267,14 @@ impl RedeemNode { /// Prune the redeem program for the given transaction environment. /// /// Pruning works as follows: - /// 1) Run the redeem program on the Bit Machine. - /// 2) Mark all (un)used case branches using the IHR of the case node. - /// 3) Rebuild the program and omit unused branches. + /// 1) Merge nodes with equal IHRs, as the encoding of the program does. + /// 2) Run the redeem program on the Bit Machine. + /// 3) Mark all (un)used case branches using the IHR of the case node. + /// 4) Rebuild the program from the nodes reachable through used branches, + /// and omit unused branches. + /// 5) Check the encoding of the pruned program as consensus does: that it decodes, runs, + /// and runs every node and both branches of every case node. This runs the program + /// again. Expressions which are not programs (1 → 1) are not checked. /// /// The pruning result depends on the witness data (which is already part of the redeem program) /// and on the transaction environment. These two inputs determine which case branches are @@ -277,9 +282,13 @@ impl RedeemNode { /// starting from the same original, unpruned program. Pruning is a lossy process, so pruning /// an already pruned program is not sound. /// - /// Pruning fails if the original, unpruned program fails to run on the Bit Machine (step 1). - /// In this case, the witness data needs to be revised. - /// The other pruning steps (2 & 3) never fail. + /// Pruning fails if the program, with its nodes merged in step 1, fails to run on the Bit + /// Machine (step 2). In this case, the witness data needs to be revised, unless merging + /// changed what the program does: nodes with equal IHRs may hide different branches, and + /// no encoding can express such a program. + /// Pruning fails with [`ExecutionError::PruningFailed`] if the pruned program does not pass + /// the check in step 5. + /// The other pruning steps never fail. pub fn prune(&self, env: &JE) -> Result, ExecutionError> { self.prune_with_tracker(env, &mut SetTracker::default()) } @@ -294,158 +303,7 @@ impl RedeemNode { env: &JE, tracker: &mut T, ) -> Result, ExecutionError> { - struct Pruner<'brand, 't, T> { - inference_context: types::Context<'brand>, - tracker: &'t mut T, - } - - impl<'brand, 't, T> Converter> for Pruner<'brand, 't, T> - where - T: PruneTracker, - { - type Error = std::convert::Infallible; - - fn convert_witness( - &mut self, - _: &PostOrderIterItem<&RedeemNode>, - witness: &Value, - ) -> Result, Self::Error> { - // The pruned type is not finalized at this point, - // so we cannot prune the witness value. - Ok(Some(witness.shallow_clone())) - } - - fn convert_disconnect( - &mut self, - _: &PostOrderIterItem<&RedeemNode>, - right: Option<&Arc>>, - _: &Arc, - ) -> Result>>, Self::Error> { - debug_assert!( - right.is_some(), - "disconnected branch should exist in unpruned redeem program" - ); - Ok(right.map(Arc::clone)) - } - - fn prune_case( - &mut self, - data: &PostOrderIterItem<&RedeemNode>, - _left: &Arc, - _right: &Arc, - ) -> Result { - // The IHR of the pruned program may change, - // but the Converter trait gives us access to the unpruned node (`data`). - // The Bit Machine tracked (un)used case branches based on the unpruned IHR. - match ( - self.tracker.contains_left(data.node.ihr()), - self.tracker.contains_right(data.node.ihr()), - ) { - (true, true) => Ok(Hide::Neither), - (false, true) => Ok(Hide::Left), - (true, false) => Ok(Hide::Right), - (false, false) => Ok(Hide::Neither), // case nodes that were never executed will be pruned out by their ancestors - } - } - - fn convert_data( - &mut self, - _: &PostOrderIterItem<&RedeemNode>, - inner: Inner< - &Arc>, - &Option>>, - &Option, - >, - ) -> Result, Self::Error> { - let converted_inner = inner - .map(|node| node.cached_data()) - .map_witness(Option::::clone); - let retyped = ConstructData::from_inner(&self.inference_context, converted_inner) - .expect("pruned types should check out if unpruned types check out"); - Ok(retyped) - } - } - - struct Finalizer; - - impl<'brand> Converter, Redeem> for Finalizer { - type Error = std::convert::Infallible; - - fn convert_witness( - &mut self, - data: &PostOrderIterItem<&ConstructNode>, - witness: &Option, - ) -> Result { - let pruned_target_ty = data - .node - .arrow() - .target - .finalize() - .expect("pruned types should check out if unpruned types check out"); - let pruned_witness = witness - .as_ref() - .expect("witness node that originally stems from redeem program should be populated") - .prune(&pruned_target_ty) - .expect("pruned type should be shrunken version of unpruned type"); - Ok(pruned_witness) - } - - fn convert_disconnect( - &mut self, - _: &PostOrderIterItem<&ConstructNode>, - right: Option<&Arc>, - _: &Option>, - ) -> Result, Self::Error> { - Ok(right - .map(Arc::clone) - .expect("disconnect node that originally stems from redeem program should have all branches")) - } - - fn convert_data( - &mut self, - data: &PostOrderIterItem<&ConstructNode>, - inner: Inner<&Arc, &Arc, &Value>, - ) -> Result, Self::Error> { - // Finalize target types of witness nodes in advance so we can prune their values. - let final_arrow = data - .node - .arrow() - .finalize() - .expect("pruned types should check out if unpruned types check out"); - let converted_inner = inner - .map(|node| node.cached_data()) - .map_disconnect(|node| node.cached_data()) - .map_witness(Value::shallow_clone); - Ok(Arc::new(RedeemData::new(final_arrow, converted_inner))) - } - } - - // 1) Run the Bit Machine and mark (un)used branches. - // This is the only fallible step in the pruning process. - let mut mac = BitMachine::for_program(self)?; - mac.exec_with_tracker(self, env, tracker)?; - - // 2) Prune out unused case branches. - // Because the types of the pruned program may change, - // we construct a temporary witness program with unfinalized types. - // Convert with `MaxSharing`: the tracker keys branch usage by IHR, so - // converting IHR-equal nodes separately makes each copy keep the branches - // that only its twins executed. - types::Context::with_context(|inference_context| { - let pruned_witness_program = self - .convert::, _, _>(&mut Pruner { - inference_context, - tracker, - }) - .expect("pruning unused branches is infallible"); - - // 3) Finalize the types of the witness program. - // We obtain the pruned redeem program. - // Once the pruned type is finalized, we can proceed to prune witness values. - Ok(pruned_witness_program - .convert::(&mut Finalizer) - .expect("finalization is infallible")) - }) + super::prune::prune(self, env, tracker) } /// Decode a Simplicity program from bits, including the witness data. @@ -592,6 +450,7 @@ mod tests { use crate::human_encoding::Forest; use crate::jet::Core; use crate::node::SimpleFinalizer; + use crate::BitMachine; use hex::DisplayHex; use std::fmt; #[cfg(all(feature = "elements", feature = "human_encoding"))] @@ -1047,6 +906,84 @@ mod tests { ); } + #[test] + #[cfg(feature = "human_encoding")] + fn prune_drops_witness_read_only_by_hidden_branch() { + // Only the right branch of the case reads `wit2`. With `wit1` selecting the + // left branch, pruning hides the right branch, nothing else reads `wit2`, and + // a decoder expects no bits for it. Both branches contain `drop iden`, so the + // hidden branch must not constrain the types of the pruned program through it. + use crate::ffi::tests::{ffi::SimplicityErr, run_program, TestUpTo}; + use crate::human_encoding::Forest; + use crate::jet::CoreEnv; + use std::collections::HashMap; + + // `drop iden` is defined once, so both branches point at the same node. + let pointer_shared = r#"wit1 := witness : 1 -> 2 +wit2 := witness : 1 -> 2^8 +input := pair wit1 wit2 : 1 -> 2 * 2^8 +proj := drop iden +skip_wit2 := comp proj unit +read_wit2 := comp proj (comp (pair iden iden) (comp jet_eq_8 jet_verify)) +main := comp input (case skip_wit2 read_wit2) : 1 -> 1"#; + // `drop iden` is written out twice: two pointer-distinct nodes with one IHR. + let ihr_shared = r#"wit1 := witness : 1 -> 2 +wit2 := witness : 1 -> 2^8 +input := pair wit1 wit2 : 1 -> 2 * 2^8 +skip_wit2 := comp (drop iden) unit +read_wit2 := comp (drop iden) (comp (pair iden iden) (comp jet_eq_8 jet_verify)) +main := comp input (case skip_wit2 read_wit2) : 1 -> 1"#; + let wit = HashMap::from([ + (Arc::from("wit1"), Value::u1(0)), + (Arc::from("wit2"), Value::u8(7)), + ]); + + for s in [pointer_shared, ihr_shared] { + let unpruned = types::Context::with_context(|ctx| { + Forest::parse::(s) + .expect("program should parse") + .to_witness_node(&ctx, &wit) + .expect("program should have main") + .finalize_unpruned() + .expect("program should finalize") + }); + + let pruned = unpruned + .prune(&CoreEnv::new()) + .expect("pruning should succeed"); + assert_eq!(pruned.cmr(), unpruned.cmr()); + let (_, witness_bits) = pruned + .encode_with_witness(&mut Vec::::new(), &mut Vec::::new()) + .expect("Vec::write is infallible"); + assert_eq!(witness_bits, 1, "only `wit1` should keep witness bits"); + + let (prog_bytes, witness_bytes) = pruned.to_vec_with_witness(); + let decoded = RedeemNode::decode::<_, _, Core>( + BitIter::from(&prog_bytes[..]), + BitIter::from(&witness_bytes[..]), + ) + .expect("pruned program should decode, so its witness holds no unread bits"); + assert_eq!( + decoded.ihr(), + pruned.ihr(), + "a decoder should infer the same types as pruning did" + ); + + // libsimplicity is the consensus implementation; check that it accepts the + // pruned program. The jets are in the hidden branch, so it can decode the + // program although it expects Elements jet codes. + let c_result = run_program( + &prog_bytes, + &witness_bytes, + TestUpTo::Everything, + None, + None, + ) + .expect("libsimplicity should decode the pruned program"); + assert_eq!(c_result.eval_result, SimplicityErr::NoError); + } + } + #[test] #[cfg(all(feature = "elements", feature = "human_encoding"))] fn prune() { @@ -1165,6 +1102,224 @@ main := comp input comp process jet_verify : 1 -> 1"#; &env, ); } + + /// Prunes `unpruned` and checks the encoding of the result as consensus does. + fn prune_and_check_encoding(unpruned: &RedeemNode) { + use crate::ffi::tests::{ffi::SimplicityErr, run_program, TestUpTo}; + use crate::jet::CoreEnv; + + let env = CoreEnv::new(); + let pruned = unpruned.prune(&env).expect("pruning should succeed"); + assert_eq!(pruned.cmr(), unpruned.cmr()); + let (prog_bytes, witness_bytes) = pruned.to_vec_with_witness(); + let decoded = RedeemNode::decode::<_, _, Core>( + BitIter::from(&prog_bytes[..]), + BitIter::from(&witness_bytes[..]), + ) + .expect("pruned program should decode"); + BitMachine::for_program(&decoded) + .expect("pruned program has reasonable bounds") + .exec(&decoded, &env) + .expect("decoded program should not reach a pruned branch"); + let c_result = run_program( + &prog_bytes, + &witness_bytes, + TestUpTo::Everything, + None, + None, + ) + .expect("libsimplicity should decode the pruned program"); + assert_eq!(c_result.eval_result, SimplicityErr::NoError); + } + + #[test] + fn prune_merges_nodes_with_equal_ihrs() { + // `p1` and `p2` have one IHR, which does not cover the types inside them. Their + // case nodes have different types, and so different IHRs, and one only takes its + // left branch and the other only its right. An encoding has a single node for + // `p1` and `p2`, so its case node must keep both branches. + use crate::node::CoreConstructible; + + type ArcNode<'brand> = Arc>; + + let unpruned = types::Context::with_context(|ctx| { + let unit = || ArcNode::unit(&ctx); + let iden = || ArcNode::iden(&ctx); + let g1 = ArcNode::injl(&unit()); + let g2 = ArcNode::injl(&unit()); + let p1 = ArcNode::comp( + &ArcNode::pair(&iden(), &g1).unwrap(), + &ArcNode::case(&unit(), &unit()).unwrap(), + ) + .unwrap(); + let p2 = ArcNode::comp( + &ArcNode::pair(&iden(), &g2).unwrap(), + &ArcNode::case(&unit(), &unit()).unwrap(), + ) + .unwrap(); + // A branch which never runs makes the type of `g1` 2 -> 1 + 2. + let verify = ArcNode::jet(&ctx, &Core::Verify); + let pins_g1 = ArcNode::comp( + &g1, + &ArcNode::comp( + &ArcNode::pair(&iden(), &iden()).unwrap(), + &ArcNode::case(&unit(), &ArcNode::take(&verify)).unwrap(), + ) + .unwrap(), + ) + .unwrap(); + ArcNode::comp( + &ArcNode::pair( + &ArcNode::pair( + &ArcNode::comp(&ArcNode::injl(&unit()), &p1).unwrap(), + &ArcNode::comp(&ArcNode::injr(&unit()), &p2).unwrap(), + ) + .unwrap(), + &ArcNode::comp(&ArcNode::injl(&unit()), &pins_g1).unwrap(), + ) + .unwrap(), + &unit(), + ) + .unwrap() + .finalize_unpruned() + .expect("program should finalize") + }); + prune_and_check_encoding(&unpruned); + } + + #[test] + fn prune_nodes_with_equal_ihrs_and_different_children() { + // `p1` and `p2` have one IHR, but their children differ in type, because `a1` + // is also used where its target type must be 1 + 2. Pruning must keep and + // build the same one of them. + use crate::node::CoreConstructible; + + type ArcNode<'brand> = Arc>; + + let unpruned = types::Context::with_context(|ctx| { + let unit = || ArcNode::unit(&ctx); + let iden = || ArcNode::iden(&ctx); + let a1 = ArcNode::injl(&unit()); + let p1 = ArcNode::comp(&a1, &unit()).unwrap(); + let a2 = ArcNode::injl(&unit()); + let p2 = ArcNode::comp(&a2, &unit()).unwrap(); + // A branch which never runs makes the target type of `a1` 1 + 2. + let verify = ArcNode::jet(&ctx, &Core::Verify); + let pins_a1 = ArcNode::comp( + &a1, + &ArcNode::comp( + &ArcNode::pair(&iden(), &iden()).unwrap(), + &ArcNode::case(&unit(), &ArcNode::take(&verify)).unwrap(), + ) + .unwrap(), + ) + .unwrap(); + ArcNode::comp( + &ArcNode::pair(&ArcNode::pair(&p1, &p2).unwrap(), &pins_a1).unwrap(), + &unit(), + ) + .unwrap() + .finalize_unpruned() + .expect("program should finalize") + }); + prune_and_check_encoding(&unpruned); + } + + #[test] + fn prune_case_and_assertion_with_equal_ihrs() { + // A case node and an assertion with one IHR, in either order. Pruning must + // keep and build the same one of them. + use crate::node::CoreConstructible; + + type ArcNode<'brand> = Arc>; + + for case_first in [true, false] { + let unpruned = types::Context::with_context(|ctx| { + let unit = || ArcNode::unit(&ctx); + let input = ArcNode::pair(&ArcNode::injl(&unit()), &unit()).unwrap(); + let take_unit = ArcNode::take(&unit()); + let case = + ArcNode::comp(&input, &ArcNode::case(&unit(), &take_unit).unwrap()).unwrap(); + let assertl = + ArcNode::comp(&input, &ArcNode::assertl(&unit(), take_unit.cmr()).unwrap()) + .unwrap(); + let (first, second) = if case_first { + (case, assertl) + } else { + (assertl, case) + }; + ArcNode::comp(&ArcNode::pair(&first, &second).unwrap(), &unit()) + .unwrap() + .finalize_unpruned() + .expect("program should finalize") + }); + prune_and_check_encoding(&unpruned); + } + } + + #[test] + fn prune_fails_rather_than_return_a_rejected_program() { + // A tracker which says that no right branch was taken makes pruning hide the + // right branch of a case node which takes it. Pruning must fail rather than + // return a program which consensus rejects. + use crate::bit_machine::{ExecTracker, FrameIter, NodeOutput}; + use crate::jet::CoreEnv; + use crate::node::CoreConstructible; + + struct NoRightBranches(SetTracker); + + impl ExecTracker for NoRightBranches { + fn visit_node(&mut self, node: &RedeemNode, input: FrameIter, output: NodeOutput) { + self.0.visit_node(node, input, output) + } + } + + impl PruneTracker for NoRightBranches { + fn contains_left(&self, _: Ihr) -> bool { + true + } + + fn contains_right(&self, _: Ihr) -> bool { + false + } + } + + type ArcNode<'brand> = Arc>; + + let unpruned = types::Context::with_context(|ctx| { + let unit = || ArcNode::unit(&ctx); + ArcNode::comp( + &ArcNode::pair(&ArcNode::injr(&unit()), &unit()).unwrap(), + &ArcNode::case(&unit(), &unit()).unwrap(), + ) + .unwrap() + .finalize_unpruned() + .expect("program should finalize") + }); + let mut tracker = NoRightBranches(SetTracker::default()); + assert!(matches!( + unpruned.prune_with_tracker(&CoreEnv::new(), &mut tracker), + Err(ExecutionError::PruningFailed) + )); + } + + #[test] + fn prune_expression_which_is_not_a_program() { + // Pruning keeps the type of an expression which is not 1 -> 1. + use crate::jet::CoreEnv; + use crate::node::CoreConstructible; + + let unpruned = types::Context::with_context(|ctx| { + Arc::::injl(&Arc::::unit(&ctx)) + .finalize_unpruned() + .expect("expression should finalize") + }); + let pruned = unpruned + .prune(&CoreEnv::new()) + .expect("pruning should succeed"); + assert_eq!(pruned.arrow().source, unpruned.arrow().source); + assert_eq!(pruned.arrow().target, unpruned.arrow().target); + } } #[cfg(bench)]