diff --git a/Cargo.lock b/Cargo.lock index 302f0154..7b278b4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2721,6 +2721,7 @@ dependencies = [ "indexmap", "itertools", "k256", + "miette", "num-bigint", "num-integer", "num-traits", diff --git a/crates/aiken-lang/src/uplc.rs b/crates/aiken-lang/src/uplc.rs index ac61732e..be6e086e 100644 --- a/crates/aiken-lang/src/uplc.rs +++ b/crates/aiken-lang/src/uplc.rs @@ -4285,7 +4285,7 @@ impl<'a> CodeGenerator<'a> { program.try_into().unwrap(); let evaluated_term: Term = - eval_program.eval(ExBudget::default()).0.unwrap(); + eval_program.eval(ExBudget::default()).result().unwrap(); arg_stack.push(evaluated_term.try_into().unwrap()); anon_func = false; @@ -5416,7 +5416,7 @@ impl<'a> CodeGenerator<'a> { let eval_program: Program = program.try_into().unwrap(); let evaluated_term: Term = - eval_program.eval(ExBudget::default()).0.unwrap(); + eval_program.eval(ExBudget::default()).result().unwrap(); term = evaluated_term.try_into().unwrap(); } diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index edcf76ac..1df63f19 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -279,13 +279,13 @@ impl Diagnostic for Error { None => None, Some(hint) => { let budget = ExBudget { mem: i64::MAX, cpu: i64::MAX, }; - let left = pretty::boxed("left", &match hint.left.eval(budget) { - (Ok(term), _, _) => format!("{term}"), - (Err(err), _, _) => format!("{err}"), + let left = pretty::boxed("left", &match hint.left.eval(budget).result() { + Ok(term) => format!("{term}"), + Err(err) => format!("{err}"), }); - let right = pretty::boxed("right", &match hint.right.eval(budget) { - (Ok(term), _, _) => format!("{term}"), - (Err(err), _, _) => format!("{err}"), + let right = pretty::boxed("right", &match hint.right.eval(budget).result() { + Ok(term) => format!("{term}"), + Err(err) => format!("{err}"), }); let msg = match hint.bin_op { BinOp::And => Some(format!("{left}\n\nand\n\n{right}\n\nshould both be true.")), diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 354263ad..ef1d7964 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -37,7 +37,7 @@ use std::{ }; use telemetry::EventListener; use uplc::{ - ast::{Constant, DeBruijn, Term}, + ast::{DeBruijn, Term}, machine::cost_model::ExBudget, }; @@ -726,22 +726,16 @@ where scripts .into_par_iter() - .map(|script| match script.program.eval(initial_budget) { - (Ok(result), remaining_budget, logs) => EvalInfo { - success: result != Term::Error - && result != Term::Constant(Constant::Bool(false).into()), + .map(|script| { + let mut eval_result = script.program.eval(initial_budget); + + EvalInfo { + success: !eval_result.failed(), script, - spent_budget: initial_budget - remaining_budget, - output: Some(result), - logs, - }, - (Err(..), remaining_budget, logs) => EvalInfo { - success: false, - script, - spent_budget: initial_budget - remaining_budget, - output: None, - logs, - }, + spent_budget: eval_result.cost(), + logs: eval_result.logs(), + output: eval_result.result().ok(), + } }) .collect() } diff --git a/crates/aiken/src/cmd/tx/simulate.rs b/crates/aiken/src/cmd/tx/simulate.rs index fad6ac01..21cd3f9c 100644 --- a/crates/aiken/src/cmd/tx/simulate.rs +++ b/crates/aiken/src/cmd/tx/simulate.rs @@ -144,7 +144,7 @@ pub fn exec( eprintln!("\n"); println!( "{}", - serde_json::to_string(&total_budget_used) + serde_json::to_string_pretty(&total_budget_used) .map_err(|_| fmt::Error) .into_diagnostic()? ); diff --git a/crates/aiken/src/cmd/uplc/eval.rs b/crates/aiken/src/cmd/uplc/eval.rs index 898f8087..cdddd935 100644 --- a/crates/aiken/src/cmd/uplc/eval.rs +++ b/crates/aiken/src/cmd/uplc/eval.rs @@ -1,5 +1,6 @@ use miette::IntoDiagnostic; -use std::path::PathBuf; +use serde_json::json; +use std::{path::PathBuf, process}; use uplc::{ ast::{FakeNamedDeBruijn, Name, NamedDeBruijn, Program, Term}, machine::cost_model::ExBudget, @@ -63,32 +64,38 @@ pub fn exec( let budget = ExBudget::default(); - let (term, cost, logs) = program.eval(budget); + let mut eval_result = program.eval(budget); - match term { + let cost = eval_result.cost(); + let logs = eval_result.logs(); + + match eval_result.result() { Ok(term) => { let term: Term = term.try_into().into_diagnostic()?; - println!("\nResult\n------\n\n{}\n", term.to_pretty()); + let output = json!({ + "result": term.to_pretty(), + "cpu": cost.cpu, + "mem": cost.mem, + }); + + println!( + "{}", + serde_json::to_string_pretty(&output).into_diagnostic()? + ); + + Ok(()) } Err(err) => { eprintln!("\nError\n-----\n\n{err}\n"); + + eprintln!("\nCosts\n-----\ncpu: {}\nmemory: {}", cost.cpu, cost.mem); + + if !logs.is_empty() { + eprintln!("\nLogs\n----\n{}", logs.join("\n")) + } + + process::exit(1) } } - - println!( - "\nCosts\n-----\ncpu: {}\nmemory: {}", - budget.cpu - cost.cpu, - budget.mem - cost.mem - ); - println!( - "\nBudget\n------\ncpu: {}\nmemory: {}\n", - cost.cpu, cost.mem - ); - - if !logs.is_empty() { - println!("\nLogs\n----\n{}", logs.join("\n")) - } - - Ok(()) } diff --git a/crates/uplc/Cargo.toml b/crates/uplc/Cargo.toml index f3af7ca3..87e367e1 100644 --- a/crates/uplc/Cargo.toml +++ b/crates/uplc/Cargo.toml @@ -20,6 +20,7 @@ hex = "0.4.3" indexmap = "1.9.2" itertools = "0.10.5" k256 = { version = "0.13.0", optional = true } +miette = "5.5.0" num-bigint = "0.4.3" num-integer = "0.1.45" num-traits = "0.2.15" diff --git a/crates/uplc/src/ast.rs b/crates/uplc/src/ast.rs index 118d8cbf..1570ae5f 100644 --- a/crates/uplc/src/ast.rs +++ b/crates/uplc/src/ast.rs @@ -24,6 +24,7 @@ use crate::{ flat::Binder, machine::{ cost_model::{initialize_cost_model, CostModel, ExBudget}, + eval_result::EvalResult, Machine, }, }; @@ -595,14 +596,7 @@ impl From> for Term { } impl Program { - pub fn eval( - &self, - initial_budget: ExBudget, - ) -> ( - Result, crate::machine::Error>, - ExBudget, - Vec, - ) { + pub fn eval(&self, initial_budget: ExBudget) -> EvalResult { let mut machine = Machine::new( Language::PlutusV2, CostModel::default(), @@ -612,22 +606,16 @@ impl Program { let term = machine.run(&self.term); - (term, machine.ex_budget, machine.logs) + EvalResult::new(term, machine.ex_budget, initial_budget, machine.logs) } /// Evaluate a Program as PlutusV1 - pub fn eval_v1( - &self, - ) -> ( - Result, crate::machine::Error>, - ExBudget, - Vec, - ) { + pub fn eval_v1(&self) -> EvalResult { let mut machine = Machine::new(Language::PlutusV1, CostModel::v1(), ExBudget::v1(), 200); let term = machine.run(&self.term); - (term, machine.ex_budget, machine.logs) + EvalResult::new(term, machine.ex_budget, ExBudget::v1(), machine.logs) } pub fn eval_as( @@ -635,11 +623,7 @@ impl Program { version: &Language, costs: &[i64], initial_budget: Option<&ExBudget>, - ) -> ( - Result, crate::machine::Error>, - ExBudget, - Vec, - ) { + ) -> EvalResult { let budget = match initial_budget { Some(b) => *b, None => ExBudget::default(), @@ -654,19 +638,12 @@ impl Program { let term = machine.run(&self.term); - (term, machine.ex_budget, machine.logs) + EvalResult::new(term, machine.ex_budget, budget, machine.logs) } } impl Program { - pub fn eval( - &self, - initial_budget: ExBudget, - ) -> ( - Result, crate::machine::Error>, - ExBudget, - Vec, - ) { + pub fn eval(&self, initial_budget: ExBudget) -> EvalResult { let program: Program = self.clone().into(); program.eval(initial_budget) diff --git a/crates/uplc/src/machine.rs b/crates/uplc/src/machine.rs index c4cb881e..221d74ad 100644 --- a/crates/uplc/src/machine.rs +++ b/crates/uplc/src/machine.rs @@ -8,6 +8,7 @@ use crate::{ pub mod cost_model; mod error; +pub mod eval_result; pub mod runtime; use cost_model::{ExBudget, StepKind}; @@ -785,9 +786,9 @@ mod tests { }, }; - let (eval_result, _, _) = program.eval(ExBudget::default()); + let eval_result = program.eval(ExBudget::default()); - let term = eval_result.unwrap(); + let term = eval_result.result().unwrap(); assert_eq!( term, @@ -834,10 +835,10 @@ mod tests { ]; for (fun, n, m, result) in test_data { - let (eval_result, _, _) = make_program(fun, n, m).eval(ExBudget::default()); + let eval_result = make_program(fun, n, m).eval(ExBudget::default()); assert_eq!( - eval_result.unwrap(), + eval_result.result().unwrap(), Term::Constant(Constant::Integer(result.into()).into()) ); } diff --git a/crates/uplc/src/machine/error.rs b/crates/uplc/src/machine/error.rs index f480b6ab..302fcd4d 100644 --- a/crates/uplc/src/machine/error.rs +++ b/crates/uplc/src/machine/error.rs @@ -6,13 +6,13 @@ use crate::ast::{NamedDeBruijn, Term, Type}; use super::{ExBudget, Value}; -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, miette::Diagnostic)] pub enum Error { #[error("Over budget mem: {} & cpu: {}", .0.mem, .0.cpu)] OutOfExError(ExBudget), #[error("Invalid Stepkind: {0}")] InvalidStepKind(u8), - #[error("Cannot evaluate an open term:\n\n{0}")] + #[error("Cannot evaluate an open term:\\n\\n{}", .0.to_pretty())] OpenTermEvaluated(Term), #[error("The provided Plutus code called 'error'.")] EvaluationFailure, diff --git a/crates/uplc/src/machine/eval_result.rs b/crates/uplc/src/machine/eval_result.rs new file mode 100644 index 00000000..468ebce9 --- /dev/null +++ b/crates/uplc/src/machine/eval_result.rs @@ -0,0 +1,44 @@ +use crate::ast::{Constant, NamedDeBruijn, Term}; + +use super::{cost_model::ExBudget, Error}; + +pub struct EvalResult { + result: Result, Error>, + remaining_budget: ExBudget, + initial_budget: ExBudget, + logs: Vec, +} + +impl EvalResult { + pub fn new( + result: Result, Error>, + remaining_budget: ExBudget, + initial_budget: ExBudget, + logs: Vec, + ) -> EvalResult { + EvalResult { + result, + remaining_budget, + initial_budget, + logs, + } + } + + pub fn cost(&self) -> ExBudget { + self.initial_budget - self.remaining_budget + } + + pub fn logs(&mut self) -> Vec { + std::mem::take(&mut self.logs) + } + + pub fn failed(&self) -> bool { + matches!(self.result, Err(_)) + || matches!(self.result, Ok(Term::Error)) + || matches!(self.result, Ok(Term::Constant(ref con)) if matches!(con.as_ref(), Constant::Bool(false))) + } + + pub fn result(self) -> Result, Error> { + self.result + } +} diff --git a/crates/uplc/src/tx/error.rs b/crates/uplc/src/tx/error.rs index 2192ada2..92fbcef3 100644 --- a/crates/uplc/src/tx/error.rs +++ b/crates/uplc/src/tx/error.rs @@ -1,6 +1,6 @@ use crate::machine::{self, cost_model::ExBudget}; -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, miette::Diagnostic)] pub enum Error { #[error("{0}")] Address(#[from] pallas_addresses::Error), diff --git a/crates/uplc/src/tx/eval.rs b/crates/uplc/src/tx/eval.rs index b62bbaf1..92aff289 100644 --- a/crates/uplc/src/tx/eval.rs +++ b/crates/uplc/src/tx/eval.rs @@ -730,7 +730,7 @@ pub fn eval_redeemer( .apply_data(redeemer.data.clone()) .apply_data(script_context.to_plutus_data()); - let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt { + let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { let costs = if let Some(costs) = &cost_mdls.plutus_v1 { costs } else { @@ -742,9 +742,12 @@ pub fn eval_redeemer( program.eval_v1() }; - match result { + let cost = eval_result.cost(); + let logs = eval_result.logs(); + + match eval_result.result() { Ok(_) => (), - Err(err) => return Err(Error::Machine(err, budget, logs)), + Err(err) => return Err(Error::Machine(err, cost, logs)), } let new_redeemer = Redeemer { @@ -752,8 +755,8 @@ pub fn eval_redeemer( index: redeemer.index, data: redeemer.data.clone(), ex_units: ExUnits { - mem: (initial_budget.mem - budget.mem) as u32, - steps: (initial_budget.cpu - budget.cpu) as u64, + mem: cost.mem as u32, + steps: cost.cpu as u64, }, }; @@ -776,7 +779,7 @@ pub fn eval_redeemer( .apply_data(redeemer.data.clone()) .apply_data(script_context.to_plutus_data()); - let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt { + let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { let costs = if let Some(costs) = &cost_mdls.plutus_v2 { costs } else { @@ -788,9 +791,12 @@ pub fn eval_redeemer( program.eval(ExBudget::default()) }; - match result { + let cost = eval_result.cost(); + let logs = eval_result.logs(); + + match eval_result.result() { Ok(_) => (), - Err(err) => return Err(Error::Machine(err, budget, logs)), + Err(err) => return Err(Error::Machine(err, cost, logs)), } let new_redeemer = Redeemer { @@ -798,8 +804,8 @@ pub fn eval_redeemer( index: redeemer.index, data: redeemer.data.clone(), ex_units: ExUnits { - mem: (initial_budget.mem - budget.mem) as u32, - steps: (initial_budget.cpu - budget.cpu) as u64, + mem: cost.mem as u32, + steps: cost.cpu as u64, }, }; @@ -824,7 +830,7 @@ pub fn eval_redeemer( .apply_data(redeemer.data.clone()) .apply_data(script_context.to_plutus_data()); - let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt { + let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { let costs = if let Some(costs) = &cost_mdls.plutus_v1 { costs } else { @@ -836,9 +842,12 @@ pub fn eval_redeemer( program.eval_v1() }; - match result { + let cost = eval_result.cost(); + let logs = eval_result.logs(); + + match eval_result.result() { Ok(_) => (), - Err(err) => return Err(Error::Machine(err, budget, logs)), + Err(err) => return Err(Error::Machine(err, cost, logs)), } let new_redeemer = Redeemer { @@ -846,8 +855,8 @@ pub fn eval_redeemer( index: redeemer.index, data: redeemer.data.clone(), ex_units: ExUnits { - mem: (initial_budget.mem - budget.mem) as u32, - steps: (initial_budget.cpu - budget.cpu) as u64, + mem: cost.mem as u32, + steps: cost.cpu as u64, }, }; @@ -869,7 +878,7 @@ pub fn eval_redeemer( .apply_data(redeemer.data.clone()) .apply_data(script_context.to_plutus_data()); - let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt { + let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { let costs = if let Some(costs) = &cost_mdls.plutus_v2 { costs } else { @@ -881,9 +890,12 @@ pub fn eval_redeemer( program.eval(ExBudget::default()) }; - match result { + let cost = eval_result.cost(); + let logs = eval_result.logs(); + + match eval_result.result() { Ok(_) => (), - Err(err) => return Err(Error::Machine(err, budget, logs)), + Err(err) => return Err(Error::Machine(err, cost, logs)), } let new_redeemer = Redeemer { @@ -891,8 +903,8 @@ pub fn eval_redeemer( index: redeemer.index, data: redeemer.data.clone(), ex_units: ExUnits { - mem: (initial_budget.mem - budget.mem) as u32, - steps: (initial_budget.cpu - budget.cpu) as u64, + mem: cost.mem as u32, + steps: cost.cpu as u64, }, }; diff --git a/examples/acceptance_tests/script_context/plutus.json b/examples/acceptance_tests/script_context/plutus.json index a6178531..e13d9f5a 100644 --- a/examples/acceptance_tests/script_context/plutus.json +++ b/examples/acceptance_tests/script_context/plutus.json @@ -5,37 +5,6 @@ "plutusVersion": "v2" }, "validators": [ - { - "title": "withdrawals.spend", - "datum": { - "title": "Unit", - "description": "The nullary constructor.", - "schema": { - "anyOf": [ - { - "dataType": "constructor", - "index": 0, - "fields": [] - } - ] - } - }, - "redeemer": { - "title": "Unit", - "description": "The nullary constructor.", - "schema": { - "anyOf": [ - { - "dataType": "constructor", - "index": 0, - "fields": [] - } - ] - } - }, - "compiledCode": "59029101000032323232323232323232222533300632323232323001003300100122533300f00114a226464a66601a0042660080080022940c04c008cdc3a4004601a6ea8c044004c8c8c8cc040ccc02cc8c94ccc034cdc3800a40042c2646466e1c0052054375a6028002600e004601c6ea8004cc004dd5998021802998021802803240009006260126d8799fd8799f581c22222222222222222222222222222222222222222222222222222222ffff004c0103d87a80004c0103d87980003301033300b3232533300d3370e00290010b0991919b8700148070dd6980a000980380118071baa00133001375666008600a66008600a00c90002401898126d8799fd87a9f581cafddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72ffff004c0103d87a80004c0103d87980003301033300b3375e6e9cc8c8c8c008004dd599803180399803180380424000900618008009129998088008a5eb804c8c8c8c8cc058004cc01801800cc04800cdd69809001180a80118098009ba7330104c0126d8799fd8799f581c22222222222222222222222222222222222222222222222222222222ffff00330104c126d8799fd87a9f581cafddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72ffff004bd7026103d87a80004c0103d87980004bd70111980180100098008009112999807801099ba5480092f5c0264646464a66601e66ebc0140044cdd2a4000660286ea00092f5c0266600e00e00600a60200066eb4c040008c04c00cc04400888c8ccc0040052000003222333300c3370e008004024466600800866e0000d200230140010012300a37540022930b180080091129998040010a4c26600a600260140046660060066016004002ae695cdaab9d5573caae7d5d02ba15745", - "hash": "6917ce2313801b854e38507b68d2c61afd70fca721804235e4760056" - }, { "title": "basic.spend", "datum": { @@ -77,6 +46,37 @@ "compiledCode": "590488010000323232323232323232323222533300532323232323001003300100122533300f00114a226464a6660180042660080080022940c04c008cdc3a4004601a6ea8c044004cc034ccc01cc8c8c8c8c8c8c94ccc04cc0580084c8c8cdc78018009bae3016001300932533300f3370e900018091baa0011001153301149012a4173736572746564206f6e20696e636f727265637420636f6e7374727563746f722076617269616e742e00163300830090034800854cc0412401364c6973742f5475706c652f436f6e73747220636f6e7461696e73206d6f7265206974656d73207468616e2069742065787065637465640016375c602800264646464600c00200200264640026644660100040020029110000137566600c600e6600c600e00290002401000e600200244a666020002297ae01323232323301537520026600c00c0066eb8c04400cdd59808801180a0011809000980080091129998078010a5eb7bdb1804c8c8c8c94ccc038cdc7802800880189980a19bb037520026e98008ccc01c01c00c014dd718080019bab3010002301300330110024c103d87a80004c0103d87980003300d333007323232323322323232323253330123370e00290010b0991919b87001483c850dd6980d0009806801180a1baa001332233008002001001488103666f6f0033223233223253330153370e00290010801099190009bab301d00130100033017375400400297adef6c6033223300b002001002001375666012601400690040009bae30150013008533300d3370e900018081baa0021002153300f49012a4173736572746564206f6e20696e636f727265637420636f6e7374727563746f722076617269616e742e001633005300600748008cc014c01801d20003001001222533301100213374a900125eb804c8c8c8c94ccc040cdc7802800899ba548000cc058dd400125eb804ccc01c01c00c014dd718090019bad3012002301500330130023001001222533300f00213374a900125eb804c8c8c8c94ccc038cdc7802800899ba548000cc050dd300125eb804ccc01c01c00c014dd718080019bab3010002301300330110024c103d87a80004c0103d87980003300d3330073232323233223232533300f3375e006002266e1cc8c018004dd5998049805198049805002240009009240042940c054004c020c94ccc038cdc3a400060226ea8004400454cc0412412a4173736572746564206f6e20696e636f727265637420636f6e7374727563746f722076617269616e742e001633223300700200137566600e60106600e60100049000240246600e6010004900100380418008009129998080008a400026466e0120023300300300130130013001001222533300f00213374a900125eb804c8c8c8c94ccc038cdd7802800899ba548000cc0500092f5c0266600e00e00600a6020006602000460260066022004980103d87a80004c0103d87980004bd701119199800800a4000006444666601666e1c0100080488ccc010010cdc0001a40046028002002460146ea8004526163001001222533300900214984cc014c004c02c008ccc00c00cc0300080055cd2b9b5738aae7555cf2ab9f5740ae855d11", "hash": "dbc571e23778572680144ee7065334ab9545cc8111da1ea5dc85ac44" }, + { + "title": "withdrawals.spend", + "datum": { + "title": "Unit", + "description": "The nullary constructor.", + "schema": { + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + } + }, + "redeemer": { + "title": "Unit", + "description": "The nullary constructor.", + "schema": { + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + } + }, + "compiledCode": "59029101000032323232323232323232222533300632323232323001003300100122533300f00114a226464a66601a0042660080080022940c04c008cdc3a4004601a6ea8c044004c8c8c8cc040ccc02cc8c94ccc034cdc3800a40042c2646466e1c0052054375a6028002600e004601c6ea8004cc004dd5998021802998021802803240009006260126d8799fd8799f581c22222222222222222222222222222222222222222222222222222222ffff004c0103d87a80004c0103d87980003301033300b3232533300d3370e00290010b0991919b8700148070dd6980a000980380118071baa00133001375666008600a66008600a00c90002401898126d8799fd87a9f581cafddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72ffff004c0103d87a80004c0103d87980003301033300b3375e6e9cc8c8c8c008004dd599803180399803180380424000900618008009129998088008a5eb804c8c8c8c8cc058004cc01801800cc04800cdd69809001180a80118098009ba7330104c0126d8799fd8799f581c22222222222222222222222222222222222222222222222222222222ffff00330104c126d8799fd87a9f581cafddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72ffff004bd7026103d87a80004c0103d87980004bd70111980180100098008009112999807801099ba5480092f5c0264646464a66601e66ebc0140044cdd2a4000660286ea00092f5c0266600e00e00600a60200066eb4c040008c04c00cc04400888c8ccc0040052000003222333300c3370e008004024466600800866e0000d200230140010012300a37540022930b180080091129998040010a4c26600a600260140046660060066016004002ae695cdaab9d5573caae7d5d02ba15745", + "hash": "6917ce2313801b854e38507b68d2c61afd70fca721804235e4760056" + }, { "title": "deploy.spend", "datum": {