Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Evaluation trace(debug) mode for the interpreter
To help the developer figure out why the script was reduced to false. I'm thinking of some sort of growing trace that includes every op evaluated. At the end of the evaluation this trace should be rendered into a string and returned as a part of the error. 


Discussion / hints:

Fix patch:
diff --git a/ergo-lib/src/chain/transaction/reduced.rs b/ergo-lib/src/chain/transaction/reduced.rs
index c3a19a98c..ab7c84ffe 100644
--- a/ergo-lib/src/chain/transaction/reduced.rs
+++ b/ergo-lib/src/chain/transaction/reduced.rs
@@ -5,7 +5,6 @@ use std::rc::Rc;
 
 use ergotree_interpreter::eval::env::Env;
 use ergotree_interpreter::eval::reduce_to_crypto;
-use ergotree_interpreter::eval::ReductionResult;
 use ergotree_interpreter::sigma_protocol::prover::ContextExtension;
 use ergotree_interpreter::sigma_protocol::prover::ProverError;
 use ergotree_ir::serialization::sigma_byte_reader::SigmaByteRead;
@@ -31,8 +30,10 @@ use super::TxIoVec;
 /// <https://github.com/ergoplatform/eips/blob/f280890a4163f2f2e988a0091c078e36912fc531/eip-0019.md>
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ReducedInput {
-    /// Input box script reduced to SigmaBoolean
-    pub reduction_result: ReductionResult,
+    /// value of SigmaProp type which represents a statement verifiable via sigma protocol.
+    pub sigma_prop: SigmaBoolean,
+    /// estimated cost of expression evaluation
+    pub cost: u64,
     /// ContextExtension for the input
     pub extension: ContextExtension,
 }
@@ -87,8 +88,9 @@ pub fn reduce_tx(
                 .map_err(ProverError::EvalError)
                 .map_err(|e| TxSigningError::ProverError(e, idx))?;
             Ok(ReducedInput {
-                reduction_result,
                 extension: input.extension,
+                sigma_prop: reduction_result.sigma_prop,
+                cost: reduction_result.cost,
             })
         })?;
     Ok(ReducedTransaction {
@@ -104,8 +106,8 @@ impl SigmaSerializable for ReducedTransaction {
         w.put_usize_as_u32_unwrapped(msg.len())?;
         w.write_all(&msg)?;
         self.reduced_inputs.as_vec().iter().try_for_each(|red_in| {
-            red_in.reduction_result.sigma_prop.sigma_serialize(w)?;
-            w.put_u64(red_in.reduction_result.cost)?;
+            red_in.sigma_prop.sigma_serialize(w)?;
+            w.put_u64(red_in.cost)?;
             SigmaSerializeResult::Ok(())
         })?;
         w.put_u32(self.tx_cost)?;
@@ -123,11 +125,8 @@ impl SigmaSerializable for ReducedTransaction {
                 let cost = r.get_u64()?;
                 let extension = input.spending_proof.extension;
                 let reduced_input = ReducedInput {
-                    reduction_result: ReductionResult {
-                        sigma_prop,
-                        cost,
-                        env: Env::empty(),
-                    },
+                    sigma_prop,
+                    cost,
                     extension: extension.clone(),
                 };
                 let unsigned_input = UnsignedInput {
@@ -168,11 +167,8 @@ pub mod arbitrary {
                 .prop_map(|(unsigned_tx, sb, tx_cost)| Self {
                     unsigned_tx: unsigned_tx.clone(),
                     reduced_inputs: unsigned_tx.inputs.mapped(|unsigned_input| ReducedInput {
-                        reduction_result: ReductionResult {
-                            sigma_prop: sb.clone(),
-                            cost: 0,
-                            env: Env::empty(),
-                        },
+                        sigma_prop: sb.clone(),
+                        cost: 0,
                         extension: unsigned_input.extension,
                     }),
                     tx_cost,
diff --git a/ergo-lib/src/wallet.rs b/ergo-lib/src/wallet.rs
index 2e48178dd..b0e1b9fd7 100644
--- a/ergo-lib/src/wallet.rs
+++ b/ergo-lib/src/wallet.rs
@@ -138,7 +138,7 @@ impl Wallet {
             .map(|secret| secret.public_image())
             .collect();
         for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() {
-            let sigma_prop = input.clone().reduction_result.sigma_prop;
+            let sigma_prop = input.clone().sigma_prop;
             let hints = generate_commitments_for(&sigma_prop, &public_keys);
             tx_hints.add_hints_for_input(index, hints);
         }
diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs
index dedaf6c92..83a4cb026 100644
--- a/ergo-lib/src/wallet/signing.rs
+++ b/ergo-lib/src/wallet/signing.rs
@@ -208,7 +208,7 @@ pub fn sign_reduced_transaction(
         }
         prover
             .generate_proof(
-                reduced_input.reduction_result.sigma_prop.clone(),
+                reduced_input.sigma_prop.clone(),
                 message_to_sign.as_slice(),
                 &hints_bag,
             )
diff --git a/ergotree-interpreter/src/eval.rs b/ergotree-interpreter/src/eval.rs
index 84c82474f..bb3853da2 100644
--- a/ergotree-interpreter/src/eval.rs
+++ b/ergotree-interpreter/src/eval.rs
@@ -3,6 +3,7 @@ use ergotree_ir::mir::constant::TryExtractInto;
 use ergotree_ir::pretty_printer::PosTrackingWriter;
 use ergotree_ir::pretty_printer::Print;
 use ergotree_ir::sigma_protocol::sigma_boolean::SigmaProp;
+use std::fmt::Display;
 use std::rc::Rc;
 
 use ergotree_ir::mir::expr::Expr;
@@ -97,6 +98,24 @@ pub(crate) mod xor_of;
 
 pub use error::EvalError;
 
+/// Diagnostic information about the reduction (pretty printed expr and/or env)
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct ReductionDiagnosticInfo {
+    /// environment after the evaluation
+    pub env: Env,
+    /// expression pretty-printed
+    pub pretty_printed_expr: Option<String>,
+}
+
+impl Display for ReductionDiagnosticInfo {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if let Some(expr_str) = &self.pretty_printed_expr {
+            writeln!(f, "Pretty printed expr:\n{}", expr_str)?;
+        }
+        write!(f, "Env:\n{}", self.env)
+    }
+}
+
 /// Result of expression reduction procedure (see `reduce_to_crypto`).
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ReductionResult {
@@ -104,8 +123,8 @@ pub struct ReductionResult {
     pub sigma_prop: SigmaBoolean,
     /// estimated cost of expression evaluation
     pub cost: u64,
-    /// environment after the evaluation
-    pub env: Env,
+    /// Diagnostic information about the reduction (pretty printed expr and/or env)
+    pub diag: ReductionDiagnosticInfo,
 }
 
 /// Evaluate the given expression by reducing it to SigmaBoolean value.
@@ -125,12 +144,18 @@ pub fn reduce_to_crypto(
                     Value::Boolean(b) => Ok(ReductionResult {
                         sigma_prop: SigmaBoolean::TrivialProp(b),
                         cost: 0,
-                        env: env_mut.clone(),
+                        diag: ReductionDiagnosticInfo {
+                            env: env_mut.clone(),
+                            pretty_printed_expr: None,
+                        },
                     }),
                     Value::SigmaProp(sp) => Ok(ReductionResult {
                         sigma_prop: sp.value().clone(),
                         cost: 0,
-                        env: env_mut.clone(),
+                        diag: ReductionDiagnosticInfo {
+                            env: env_mut.clone(),
+                            pretty_printed_expr: None,
+                        },
                     }),
                     _ => Err(EvalError::InvalidResultType),
                 }
@@ -138,16 +163,34 @@ pub fn reduce_to_crypto(
     }
 
     let res = inner(expr, env, ctx);
-    if res.is_ok() {
-        return res;
+    if let Ok(reduction) = res {
+        if reduction.sigma_prop == SigmaBoolean::TrivialProp(false) {
+            let (_, printed_expr_str) = pretty_print(expr)?;
+            let new_reduction = ReductionResult {
+                sigma_prop: SigmaBoolean::TrivialProp(false),
+                cost: reduction.cost,
+                diag: ReductionDiagnosticInfo {
+                    env: reduction.diag.env,
+                    pretty_printed_expr: Some(printed_expr_str),
+                },
+            };
+            return Ok(new_reduction);
+        } else {
+            return Ok(reduction);
+        }
     }
+    let (spanned_expr, printed_expr_str) = pretty_print(expr)?;
+    inner(&spanned_expr, env, ctx_clone)
+        .map_err(|e| e.wrap_spanned_with_src(printed_expr_str.to_string()))
+}
+
+fn pretty_print(expr: &Expr) -> Result<(Expr, String), EvalError> {
     let mut printer = PosTrackingWriter::new();
     let spanned_expr = expr
         .print(&mut printer)
         .map_err(|e| EvalError::Misc(format!("printer error: {}", e)))?;
     let printed_expr_str = printer.get_buf();
-    inner(&spanned_expr, env, ctx_clone)
-        .map_err(|e| e.wrap_spanned_with_src(printed_expr_str.to_string()))
+    Ok((spanned_expr, printed_expr_str.to_owned()))
 }
 
 /// Expects SigmaProp constant value and returns it's value. Otherwise, returns an error.
@@ -330,14 +373,23 @@ fn smethod_eval_fn(method: &SMethod) -> Result<EvalFn, EvalError> {
 #[cfg(test)]
 #[cfg(feature = "arbitrary")]
 #[allow(clippy::unwrap_used)]
+#[allow(clippy::todo)]
 pub(crate) mod tests {
 
     #![allow(dead_code)]
 
     use super::env::Env;
     use super::*;
+    use ergotree_ir::mir::bin_op::BinOp;
+    use ergotree_ir::mir::bin_op::BinOpKind;
+    use ergotree_ir::mir::bin_op::RelationOp;
+    use ergotree_ir::mir::block::BlockValue;
     use ergotree_ir::mir::constant::TryExtractFrom;
     use ergotree_ir::mir::constant::TryExtractInto;
+    use ergotree_ir::mir::val_def::ValDef;
+    use ergotree_ir::mir::val_use::ValUse;
+    use ergotree_ir::types::stype::SType;
+    use expect_test::expect;
     use sigma_test_util::force_any_val;
 
     pub fn eval_out_wo_ctx<T: TryExtractFrom<Value>>(expr: &Expr) -> T {
@@ -370,4 +422,45 @@ pub(crate) mod tests {
         let ctx = Rc::new(force_any_val::<Context>());
         try_eval_out(expr, ctx)
     }
+
+    #[test]
+    fn diag_on_reduced_to_false() {
+        let bin_op: Expr = BinOp {
+            kind: BinOpKind::Relation(RelationOp::Eq),
+            left: Box::new(
+                ValUse {
+                    val_id: 1.into(),
+                    tpe: SType::SInt,
+                }
+                .into(),
+            ),
+            right: Box::new(0i32.into()),
+        }
+        .into();
+        let block: Expr = Expr::BlockValue(
+            BlockValue {
+                items: vec![ValDef {
+                    id: 1.into(),
+                    rhs: Box::new(Expr::Const(1i32.into())),
+                }
+                .into()],
+                result: Box::new(bin_op),
+            }
+            .into(),
+        );
+        let ctx = Rc::new(force_any_val::<Context>());
+        let res = reduce_to_crypto(&block, &Env::empty(), ctx).unwrap();
+        assert!(res.sigma_prop == SigmaBoolean::TrivialProp(false));
+        expect![[r#"
+            Pretty printed expr:
+            {
+              val v1 = 1
+              v1 == 0
+            }
+
+            Env:
+            v1: 1
+        "#]]
+        .assert_eq(&res.diag.to_string());
+    }
 }
diff --git a/ergotree-interpreter/src/eval/env.rs b/ergotree-interpreter/src/eval/env.rs
index 2e5493068..b859d0476 100644
--- a/ergotree-interpreter/src/eval/env.rs
+++ b/ergotree-interpreter/src/eval/env.rs
@@ -1,4 +1,5 @@
 use std::collections::HashMap;
+use std::fmt::Display;
 
 use ergotree_ir::mir::val_def::ValId;
 use ergotree_ir::mir::value::Value;
@@ -34,3 +35,14 @@ impl Env {
         self.store.get(&idx)
     }
 }
+
+impl Display for Env {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut keys: Vec<&ValId> = self.store.keys().collect();
+        keys.sort();
+        for k in keys {
+            writeln!(f, "v{}: {}", k, self.store[k])?;
+        }
+        Ok(())
+    }
+}
diff --git a/ergotree-interpreter/src/eval/error.rs b/ergotree-interpreter/src/eval/error.rs
index 0f4b539ed..6b9e8987f 100644
--- a/ergotree-interpreter/src/eval/error.rs
+++ b/ergotree-interpreter/src/eval/error.rs
@@ -1,5 +1,6 @@
 use miette::miette;
 use miette::LabeledSpan;
+use std::fmt::Debug;
 use std::fmt::Display;
 
 use bounded_vec::BoundedVecOutOfBounds;
@@ -87,7 +88,7 @@ pub struct SpannedEvalError {
 }
 
 /// Wrapped error with source span and source code
-#[derive(PartialEq, Eq, Debug, Clone)]
+#[derive(PartialEq, Eq, Clone)]
 pub struct SpannedWithSourceEvalError {
     /// eval error
     error: Box<EvalError>,
@@ -125,6 +126,33 @@ impl Display for SpannedWithSourceEvalError {
     }
 }
 
+impl Debug for SpannedWithSourceEvalError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        #[allow(clippy::unwrap_used)]
+        miette::set_hook(Box::new(|_| {
+            Box::new(
+                miette::MietteHandlerOpts::new()
+                    .terminal_links(false)
+                    .unicode(false)
+                    .color(false)
+                    .context_lines(5)
+                    .tab_width(2)
+                    .build(),
+            )
+        }))
+        .unwrap();
+        let err_msg = self.error.to_string();
+        let report = miette!(
+            labels = vec![LabeledSpan::at(self.source_span, err_msg,)],
+            // help = "Help msg",
+            "Evaluation error"
+        )
+        .with_source_code(self.source.clone());
+        write!(f, "{:?}", report)?;
+        write!(f, "Env:\n{}", self.env)
+    }
+}
+
 impl EvalError {
     /// Wrap eval error with source span
     pub fn wrap(self, source_span: SourceSpan, env: Env) -> Self {
diff --git a/ergotree-interpreter/src/sigma_protocol/prover.rs b/ergotree-interpreter/src/sigma_protocol/prover.rs
index 3f016b5e9..4dee08edc 100644
--- a/ergotree-interpreter/src/sigma_protocol/prover.rs
+++ b/ergotree-interpreter/src/sigma_protocol/prover.rs
@@ -6,6 +6,7 @@ mod prover_result;
 pub mod hint;
 
 use crate::eval::reduce_to_crypto;
+use crate::eval::ReductionDiagnosticInfo;
 use crate::sigma_protocol::crypto_utils::secure_random_bytes;
 use crate::sigma_protocol::fiat_shamir::fiat_shamir_hash_fn;
 use crate::sigma_protocol::fiat_shamir::fiat_shamir_tree_to_bytes;
@@ -77,6 +78,9 @@ pub enum ProverError {
     /// Script reduced to false
     #[error("Script reduced to false")]
     ReducedToFalse,
+    /// Script reduced to false with diagnostic info
+    #[error("Script reduced to false. Diagnostic info: {0}")]
+    ReducedToFalseWithDiag(ReductionDiagnosticInfo),
     /// Failed on step2(prover does not have enough witnesses to perform the proof)
     #[error("Failed on step2(prover does not have enough witnesses to perform the proof)")]
     TreeRootIsNotReal,
@@ -150,6 +154,12 @@ pub trait Prover {
                 proof: p,
                 extension: ctx_ext,
             })
+            .map_err(|e| match e {
+                ProverError::ReducedToFalse => {
+                    ProverError::ReducedToFalseWithDiag(reduction_result.diag.clone())
+                }
+                _ => e,
+            })
     }
 
     /// Generate proofs for the given message for the given Sigma boolean expression
diff --git a/ergotree-interpreter/src/sigma_protocol/verifier.rs b/ergotree-interpreter/src/sigma_protocol/verifier.rs
index 7219b9e68..798d27a23 100644
--- a/ergotree-interpreter/src/sigma_protocol/verifier.rs
+++ b/ergotree-interpreter/src/sigma_protocol/verifier.rs
@@ -17,8 +17,8 @@ use super::{
 };
 use crate::eval::context::Context;
 use crate::eval::env::Env;
-use crate::eval::reduce_to_crypto;
 use crate::eval::EvalError;
+use crate::eval::{reduce_to_crypto, ReductionDiagnosticInfo};
 use dlog_protocol::FirstDlogProverMessage;
 use ergotree_ir::ergo_tree::ErgoTree;
 use ergotree_ir::ergo_tree::ErgoTreeError;
@@ -49,6 +49,8 @@ pub struct VerificationResult {
     pub result: bool,
     /// estimated cost of contract execution
     pub cost: u64,
+    /// Diagnostic information about the reduction (pretty printed expr and/or env)
+    pub diag: ReductionDiagnosticInfo,
 }
 
 /// Verifier for the proofs generater by [`super::prover::Prover`]
@@ -66,8 +68,8 @@ pub trait Verifier {
         message: &[u8],
     ) -> Result<VerificationResult, VerifierError> {
         let expr = tree.proposition()?;
-        let cprop = reduce_to_crypto(&expr, env, ctx)?.sigma_prop;
-        let res: bool = match cprop {
+        let reduction_result = reduce_to_crypto(&expr, env, ctx)?;
+        let res: bool = match reduction_result.sigma_prop {
             SigmaBoolean::TrivialProp(b) => b,
             sb => {
                 match proof {
@@ -84,6 +86,7 @@ pub trait Verifier {
         Ok(VerificationResult {
             result: res,
             cost: 0,
+            diag: reduction_result.diag,
         })
     }
 }
diff --git a/ergotree-ir/src/mir/val_def.rs b/ergotree-ir/src/mir/val_def.rs
index b2f5c0580..5881f8e08 100644
--- a/ergotree-ir/src/mir/val_def.rs
+++ b/ergotree-ir/src/mir/val_def.rs
@@ -17,7 +17,7 @@ use crate::has_opcode::HasStaticOpCode;
 use proptest_derive::Arbitrary;
 
 /// Variable id
-#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, From, Display)]
+#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy, From, Display, Ord, PartialOrd)]
 #[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
 pub struct ValId(pub u32);
 
diff --git a/ergotree-ir/src/mir/value.rs b/ergotree-ir/src/mir/value.rs
index 45b5416b4..dafec6b75 100644
--- a/ergotree-ir/src/mir/value.rs
+++ b/ergotree-ir/src/mir/value.rs
@@ -1,6 +1,7 @@
 //! Ergo data type
 
 use std::convert::TryInto;
+use std::fmt::Formatter;
 use std::sync::Arc;
 
 use impl_trait_for_tuples::impl_for_tuples;
@@ -242,6 +243,68 @@ impl From<Literal> for Value {
     }
 }
 
+impl std::fmt::Display for Value {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Value::Coll(CollKind::NativeColl(NativeColl::CollByte(i8_bytes))) => {
+                write!(f, "Coll[Byte](")?;
+                for (i, b) in i8_bytes.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    write!(f, "{}", b)?;
+                }
+                write!(f, ")")
+            }
+            Value::Coll(CollKind::WrappedColl { elem_tpe, items }) => {
+                write!(f, "Coll[{}](", elem_tpe)?;
+                for (i, item) in items.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    item.fmt(f)?;
+                }
+                write!(f, ")")
+            }
+            Value::Opt(boxed_opt) => {
+                if let Some(v) = &**boxed_opt {
+                    write!(f, "Some(")?;
+                    v.fmt(f)?;
+                    write!(f, ")")
+                } else {
+                    write!(f, "None")
+                }
+            }
+            Value::Tup(items) => {
+                write!(f, "(")?;
+                for (i, item) in items.iter().enumerate() {
+                    if i > 0 {
+                        write!(f, ", ")?;
+                    }
+                    item.fmt(f)?;
+                }
+                write!(f, ")")
+            }
+            Value::Unit => write!(f, "()"),
+            Value::Boolean(v) => v.fmt(f),
+            Value::Byte(v) => v.fmt(f),
+            Value::Short(v) => v.fmt(f),
+            Value::Int(v) => v.fmt(f),
+            Value::Long(v) => write!(f, "{}L", v),
+            Value::BigInt(v) => v.fmt(f),
+            Value::SigmaProp(v) => v.fmt(f),
+            Value::GroupElement(v) => v.fmt(f),
+            Value::AvlTree(v) => write!(f, "AvlTree({:?})", v),
+            Value::CBox(v) => write!(f, "ErgoBox({:?})", v),
+            Value::Context => write!(f, "CONTEXT"),
+            Value::Header(_) => write!(f, "HEADER"),
+            Value::PreHeader(_) => write!(f, "PREHEADER"),
+            Value::Global => write!(f, "GLOBAL"),
+            Value::Lambda(v) => write!(f, "{v:?}"),
+        }
+    }
+}
+
 /// Marker trait to select types which stored as Vec of wrapped Value's
 pub trait StoreWrapped {}
 


Test patch: