diff --git a/Cargo.lock b/Cargo.lock index d86cc242..b5a999bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6076,7 +6076,7 @@ dependencies = [ [[package]] name = "pod2" version = "0.1.0" -source = "git+https://github.com/0xPARC/pod2?rev=da6c08f3c3341a51aa8f7f0f863ec694bcb9d9a3#da6c08f3c3341a51aa8f7f0f863ec694bcb9d9a3" +source = "git+https://github.com/0xPARC/pod2?rev=73810cbd5b57cd679e25dfad0c34968faec22767#73810cbd5b57cd679e25dfad0c34968faec22767" dependencies = [ "annotate-snippets", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 74c94fa0..7f4a0d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ resolver = "2" [workspace.dependencies] -pod2 = { git = "https://github.com/0xPARC/pod2", rev = "da6c08f3c3341a51aa8f7f0f863ec694bcb9d9a3", default-features = false, features = [ +pod2 = { git = "https://github.com/0xPARC/pod2", rev = "73810cbd5b57cd679e25dfad0c34968faec22767", default-features = false, features = [ "backend_plonky2", "disk_cache", "zk", @@ -63,3 +63,11 @@ time = { version = "0.3.44", features = ["formatting", "local-offset"] } rand = { version = "0.9.2", features = ["std_rng"] } thiserror = "2.0.18" wire-types = { path = "libs/wire-types" } + +# Uncomment to use a local pod2 dependency +# [patch."https://github.com/0xPARC/pod2"] +# pod2 = { path = "../pod2" } + +# Uncomment in order to enable debug information in the release builds. This allows getting panic backtraces with a performance similar to regular release. +# [profile.release] +# debug = true diff --git a/examples/craft-basics/manifest.toml b/examples/craft-basics/manifest.toml index 681b50ae..0211c8b6 100644 --- a/examples/craft-basics/manifest.toml +++ b/examples/craft-basics/manifest.toml @@ -2,7 +2,7 @@ name = "craft-basics" version = "0.1.0" # Rewritten by `cargo run -p pexe -- build examples/craft-basics`. -module_hash = "9db3acf116e6c8d635da722b0a17e7988bbffee10590fffe8fc0161aa0be18d0" +module_hash = "57631b51fb9a921588d391211f94c0bd8f777aff0a16755bc2dfefb52d6ff5b0" [[classes]] name = "Log" diff --git a/examples/craft-rocket/manifest.toml b/examples/craft-rocket/manifest.toml index e7686918..fd183df3 100644 --- a/examples/craft-rocket/manifest.toml +++ b/examples/craft-rocket/manifest.toml @@ -2,7 +2,7 @@ name = "craft-rocket" version = "0.1.0" # Rewritten by `cargo run -p pexe -- build examples/craft-rocket`. -module_hash = "ee913737d2541322c86d4e52c33bbf8bad872970e5e62e336553912b61285349" +module_hash = "f838acbb5b8a196174ec1adb15b2dc19f60faa5ef76ab0ab60b6ee010f0f964f" # ── raw resources ──────────────────────────────────────────────────────────── diff --git a/libs/payload/src/test_state.rs b/libs/payload/src/test_state.rs index 14a9b738..6036b170 100644 --- a/libs/payload/src/test_state.rs +++ b/libs/payload/src/test_state.rs @@ -48,6 +48,12 @@ impl TestState { } } + pub fn next_block(&mut self, time_delta: u64) { + self.block_number += 1; + self.block_timestamp += time_delta as i64; + self.block_hash = Hash([F(0), F(0), F(0), F(self.block_number as u64)]); + } + /// `(created_root, nullifiers_root, prior_state_history_root)`. pub fn roots(&self) -> (Hash, Hash, Hash) { ( diff --git a/libs/pod2utils/src/macros.rs b/libs/pod2utils/src/macros.rs index 169ab240..551a767d 100644 --- a/libs/pod2utils/src/macros.rs +++ b/libs/pod2utils/src/macros.rs @@ -4,7 +4,7 @@ use pod2::{ frontend::MultiPodBuilder, lang::Module, middleware::{ - CustomPredicateRef, Statement, StrKey, Value, + CustomPredicateRef, OperationType, Statement, StrKey, Value, containers::{Dictionary, Set}, }, }; @@ -326,9 +326,16 @@ impl BuildContext { statements: Vec, ) -> anyhow::Result { for module in &self.modules { - if let Some(cpr) = module.predicate_ref_by_name(name) { + if module.predicate_ref_by_name(name).is_some() { return module.apply_predicate_with(name, statements, public, |is_public, op| { let mut wildcard_values: Vec<(usize, Value)> = Vec::new(); + // Get the CustomPredicateRef from the closure because this may be a chain in a + // split predicate where the wildcard indices are different than the top level + // predicate. + let cpr = match &op.0 { + OperationType::Custom(cpr) => cpr, + _ => unreachable!(), + }; for (i, name) in cpr.predicate().wildcard_names().iter().enumerate() { if let Some(value) = wildcard_map.get(name) { wildcard_values.push((i, value.clone())); diff --git a/libs/sdk/README.md b/libs/sdk/README.md index 8da9663f..bf0ba108 100644 --- a/libs/sdk/README.md +++ b/libs/sdk/README.md @@ -164,154 +164,146 @@ use module 0xc2b96ca2c6970e4e950d09408011691c21b6c9c24610e74aec471ea53e0ace65 as use intro Vdf(count, input, output) from 0xab82223f501b5056f458f063eb2fc073f8ac01f2ea178a3a2303394fec6828a0 use intro LtEqU256(lhs, rhs) from 0xe0595e5c75467e5a27bd30fa48a45e1dcc66a327076e5ce7c02ce33dfe357311 -record StateHeader = (block_number, created, nullifiers, prior_state_history) -record FindLogOut = (log) +record StateHeader = (block_number, block_timestamp, block_hash, created, nullifiers, prior_state_history) +record FindLogIO = (out_log) record FindLogInitials = (log) -record CraftWoodIn = (log) -record CraftWoodOut = (wood) -record CraftSticksIn = (wood) -record CraftSticksOut = (stick_a, stick_b) +record CraftWoodIO = (in_log, out_wood) +record CraftSticksIO = (in_wood, out_stick_a, out_stick_b) record CraftSticksChain = (step_0, step_1) record CraftSticksInitials = (stick_a, stick_b) -record CraftWoodPickIn = (wood, stick) -record CraftWoodPickOut = (pick) +record CraftWoodPickIO = (in_wood, in_stick, out_pick) record CraftWoodPickChain = (step_0, step_1) record CraftWoodPickInitials = (pick) -record UseWoodPickIn = (wood_pick) -record UseWoodPickOut = (wood_pick) -record MineStoneWithWoodPickOut = (stone) +record UseWoodPickIO = (in_wood_pick, out_wood_pick) +record MineStoneWithWoodPickIO = (out_stone) record MineStoneWithWoodPickInitials = (stone) // Actions -FindLog(out FindLogOut, chain0, chain, private: log0, work, initials FindLogInitials) = AND( +FindLog(io FindLogIO, state_header StateHeader, chain0, chain, private: log0, work, initials FindLogInitials) = AND( Vdf(3, log0, work) DictUpdate(log0, "work", work, initials.log) - tx::TxInsert(chain0, chain, initials.log, out.log, @self_predicate(IsLog)) + tx::TxInsert(chain0, chain, initials.log, io.out_log, @self_predicate(IsLog)) ) -CraftWood(in CraftWoodIn, out CraftWoodOut, chain0, chain, private: chain1, wood0, wood1, key) = AND( +CraftWood(io CraftWoodIO, state_header StateHeader, chain0, chain, private: chain1, wood0, wood1, key) = AND( DictUpdate(wood0, "key", key, wood1) LtEqU256(wood1, Raw(0x0020000000000000000000000000000000000000000000000000000000000000)) - tx::TxDelete(chain0, chain1, in.log, @self_predicate(IsLog)) - tx::TxInsert(chain1, chain, wood1, out.wood, @self_predicate(IsWood)) + tx::TxDelete(chain0, chain1, io.in_log, @self_predicate(IsLog)) + tx::TxInsert(chain1, chain, wood1, io.out_wood, @self_predicate(IsWood)) ) -CraftSticks(in CraftSticksIn, out CraftSticksOut, chain0, chain, private: chain_steps CraftSticksChain, initials CraftSticksInitials -) = AND( - tx::TxDelete(chain0, chain_steps.step_0, in.wood, @self_predicate(IsWood)) - tx::TxInsert(chain_steps.step_0, chain_steps.step_1, initials.stick_a, out.stick_a, @self_predicate(IsStick)) - tx::TxInsert(chain_steps.step_1, chain, initials.stick_b, out.stick_b, @self_predicate(IsStick)) +CraftSticks(io CraftSticksIO, state_header StateHeader, chain0, chain, private: chain_steps CraftSticksChain, initials CraftSticksInitials) = AND( + tx::TxDelete(chain0, chain_steps.step_0, io.in_wood, @self_predicate(IsWood)) + tx::TxInsert(chain_steps.step_0, chain_steps.step_1, initials.stick_a, io.out_stick_a, @self_predicate(IsStick)) + tx::TxInsert(chain_steps.step_1, chain, initials.stick_b, io.out_stick_b, @self_predicate(IsStick)) ) -CraftWoodPick(in CraftWoodPickIn, out CraftWoodPickOut, chain0, chain, private: chain_steps CraftWoodPickChain, initials CraftWoodPi -ckInitials) = AND( +CraftWoodPick(io CraftWoodPickIO, state_header StateHeader, chain0, chain, private: chain_steps CraftWoodPickChain, initials CraftWoodPickInitials) = AND( DictContains(initials.pick, "durability", 100) - tx::TxDelete(chain0, chain_steps.step_0, in.wood, @self_predicate(IsWood)) - tx::TxDelete(chain_steps.step_0, chain_steps.step_1, in.stick, @self_predicate(IsStick)) - tx::TxInsert(chain_steps.step_1, chain, initials.pick, out.pick, @self_predicate(IsWoodPick)) + tx::TxDelete(chain0, chain_steps.step_0, io.in_wood, @self_predicate(IsWood)) + tx::TxDelete(chain_steps.step_0, chain_steps.step_1, io.in_stick, @self_predicate(IsStick)) + tx::TxInsert(chain_steps.step_1, chain, initials.pick, io.out_pick, @self_predicate(IsWoodPick)) ) -UseWoodPick(in UseWoodPickIn, out UseWoodPickOut, chain0, chain, private: wood_pick0, wood_pick1, wood_pick2, durability, key, work) - = AND( - ArrayContains(in, UseWoodPickIn::wood_pick, wood_pick0) +UseWoodPick(io UseWoodPickIO, state_header StateHeader, chain0, chain, private: wood_pick0, wood_pick1, wood_pick2, durability, key, work) = AND( + ArrayContains(io, UseWoodPickIO::in_wood_pick, wood_pick0) Gt(wood_pick0.durability, 0) Sum(durability, 1, wood_pick0.durability) DictUpdate(wood_pick0, "durability", durability, wood_pick1) DictUpdate(wood_pick1, "key", key, wood_pick2) Vdf(10, wood_pick2, work) - DictUpdate(wood_pick2, "work", work, out.wood_pick) - tx::TxMutate(chain0, chain, wood_pick0, out.wood_pick, @self_predicate(IsWoodPick)) + DictUpdate(wood_pick2, "work", work, io.out_wood_pick) + tx::TxMutate(chain0, chain, wood_pick0, io.out_wood_pick, @self_predicate(IsWoodPick)) ) -MineStoneWithWoodPick(out MineStoneWithWoodPickOut, chain0, chain, private: chain1, _UseWoodPick_in_0 UseWoodPickIn, _UseWoodPick_ou -t_0 UseWoodPickOut, initials MineStoneWithWoodPickInitials) = AND( - UseWoodPick(_UseWoodPick_in_0, _UseWoodPick_out_0, chain0, chain1) - tx::TxInsert(chain1, chain, initials.stone, out.stone, @self_predicate(IsStone)) +MineStoneWithWoodPick(io MineStoneWithWoodPickIO, state_header StateHeader, chain0, chain, private: chain1, _UseWoodPick_io_0 UseWoodPickIO, initials MineStoneWithWoodPickInitials) = AND( + UseWoodPick(_UseWoodPick_io_0, state_header, chain0, chain1) + tx::TxInsert(chain1, chain, initials.stone, io.out_stone, @self_predicate(IsStone)) ) // Bridges -IsLogFromFindLog(state, chain0, chain, private: out FindLogOut) = AND( - ArrayContains(out, FindLogOut::log, state) - FindLog(out, chain0, chain) +IsLogFromFindLog(state, state_header, chain0, chain, private: io FindLogIO) = AND( + ArrayContains(io, FindLogIO::out_log, state) + FindLog(io, state_header, chain0, chain) ) -IsLogFromCraftWood(state, chain0, chain, private: in CraftWoodIn, out CraftWoodOut) = AND( - ArrayContains(in, CraftWoodIn::log, state) - CraftWood(in, out, chain0, chain) +IsLogFromCraftWood(state, state_header, chain0, chain, private: io CraftWoodIO) = AND( + ArrayContains(io, CraftWoodIO::in_log, state) + CraftWood(io, state_header, chain0, chain) ) -IsWoodFromCraftWood(state, chain0, chain, private: in CraftWoodIn, out CraftWoodOut) = AND( - ArrayContains(out, CraftWoodOut::wood, state) - CraftWood(in, out, chain0, chain) +IsWoodFromCraftWood(state, state_header, chain0, chain, private: io CraftWoodIO) = AND( + ArrayContains(io, CraftWoodIO::out_wood, state) + CraftWood(io, state_header, chain0, chain) ) -IsWoodFromCraftSticks(state, chain0, chain, private: in CraftSticksIn, out CraftSticksOut) = AND( - ArrayContains(in, CraftSticksIn::wood, state) - CraftSticks(in, out, chain0, chain) +IsWoodFromCraftSticks(state, state_header, chain0, chain, private: io CraftSticksIO) = AND( + ArrayContains(io, CraftSticksIO::in_wood, state) + CraftSticks(io, state_header, chain0, chain) ) -IsStickFromCraftSticks_stick_a(state, chain0, chain, private: in CraftSticksIn, out CraftSticksOut) = AND( - ArrayContains(out, CraftSticksOut::stick_a, state) - CraftSticks(in, out, chain0, chain) +IsStickFromCraftSticks_stick_a(state, state_header, chain0, chain, private: io CraftSticksIO) = AND( + ArrayContains(io, CraftSticksIO::out_stick_a, state) + CraftSticks(io, state_header, chain0, chain) ) -IsStickFromCraftSticks_stick_b(state, chain0, chain, private: in CraftSticksIn, out CraftSticksOut) = AND( - ArrayContains(out, CraftSticksOut::stick_b, state) - CraftSticks(in, out, chain0, chain) +IsStickFromCraftSticks_stick_b(state, state_header, chain0, chain, private: io CraftSticksIO) = AND( + ArrayContains(io, CraftSticksIO::out_stick_b, state) + CraftSticks(io, state_header, chain0, chain) ) -IsWoodFromCraftWoodPick(state, chain0, chain, private: in CraftWoodPickIn, out CraftWoodPickOut) = AND( - ArrayContains(in, CraftWoodPickIn::wood, state) - CraftWoodPick(in, out, chain0, chain) +IsWoodFromCraftWoodPick(state, state_header, chain0, chain, private: io CraftWoodPickIO) = AND( + ArrayContains(io, CraftWoodPickIO::in_wood, state) + CraftWoodPick(io, state_header, chain0, chain) ) -IsStickFromCraftWoodPick(state, chain0, chain, private: in CraftWoodPickIn, out CraftWoodPickOut) = AND( - ArrayContains(in, CraftWoodPickIn::stick, state) - CraftWoodPick(in, out, chain0, chain) +IsStickFromCraftWoodPick(state, state_header, chain0, chain, private: io CraftWoodPickIO) = AND( + ArrayContains(io, CraftWoodPickIO::in_stick, state) + CraftWoodPick(io, state_header, chain0, chain) ) -IsWoodPickFromCraftWoodPick(state, chain0, chain, private: in CraftWoodPickIn, out CraftWoodPickOut) = AND( - ArrayContains(out, CraftWoodPickOut::pick, state) - CraftWoodPick(in, out, chain0, chain) +IsWoodPickFromCraftWoodPick(state, state_header, chain0, chain, private: io CraftWoodPickIO) = AND( + ArrayContains(io, CraftWoodPickIO::out_pick, state) + CraftWoodPick(io, state_header, chain0, chain) ) -IsWoodPickFromUseWoodPick(state, chain0, chain, private: in UseWoodPickIn, out UseWoodPickOut) = AND( - ArrayContains(out, UseWoodPickOut::wood_pick, state) - UseWoodPick(in, out, chain0, chain) +IsWoodPickFromUseWoodPick(state, state_header, chain0, chain, private: io UseWoodPickIO) = AND( + ArrayContains(io, UseWoodPickIO::out_wood_pick, state) + UseWoodPick(io, state_header, chain0, chain) ) -IsStoneFromMineStoneWithWoodPick(state, chain0, chain, private: out MineStoneWithWoodPickOut) = AND( - ArrayContains(out, MineStoneWithWoodPickOut::stone, state) - MineStoneWithWoodPick(out, chain0, chain) +IsStoneFromMineStoneWithWoodPick(state, state_header, chain0, chain, private: io MineStoneWithWoodPickIO) = AND( + ArrayContains(io, MineStoneWithWoodPickIO::out_stone, state) + MineStoneWithWoodPick(io, state_header, chain0, chain) ) // Classes IsLog(state, state_header StateHeader, chain0, chain) = OR( - IsLogFromFindLog(state, chain0, chain) - IsLogFromCraftWood(state, chain0, chain) + IsLogFromFindLog(state, state_header, chain0, chain) + IsLogFromCraftWood(state, state_header, chain0, chain) ) IsWood(state, state_header StateHeader, chain0, chain) = OR( - IsWoodFromCraftWood(state, chain0, chain) - IsWoodFromCraftSticks(state, chain0, chain) - IsWoodFromCraftWoodPick(state, chain0, chain) + IsWoodFromCraftWood(state, state_header, chain0, chain) + IsWoodFromCraftSticks(state, state_header, chain0, chain) + IsWoodFromCraftWoodPick(state, state_header, chain0, chain) ) IsStick(state, state_header StateHeader, chain0, chain) = OR( - IsStickFromCraftSticks_stick_a(state, chain0, chain) - IsStickFromCraftSticks_stick_b(state, chain0, chain) - IsStickFromCraftWoodPick(state, chain0, chain) + IsStickFromCraftSticks_stick_a(state, state_header, chain0, chain) + IsStickFromCraftSticks_stick_b(state, state_header, chain0, chain) + IsStickFromCraftWoodPick(state, state_header, chain0, chain) ) IsWoodPick(state, state_header StateHeader, chain0, chain) = OR( - IsWoodPickFromCraftWoodPick(state, chain0, chain) - IsWoodPickFromUseWoodPick(state, chain0, chain) + IsWoodPickFromCraftWoodPick(state, state_header, chain0, chain) + IsWoodPickFromUseWoodPick(state, state_header, chain0, chain) ) IsStone(state, state_header StateHeader, chain0, chain) = OR( - IsStoneFromMineStoneWithWoodPick(state, chain0, chain) + IsStoneFromMineStoneWithWoodPick(state, state_header, chain0, chain) ) ``` diff --git a/libs/sdk/src/fmt_podlang.rs b/libs/sdk/src/fmt_podlang.rs index 9a384dff..be336d65 100644 --- a/libs/sdk/src/fmt_podlang.rs +++ b/libs/sdk/src/fmt_podlang.rs @@ -82,7 +82,7 @@ pub(crate) fn chain_schema_name(action_name: &str) -> String { /// record's array layout is `[step_0_value, step_1_value, ...]`, so the /// slot index is `ts - 1` and the step name is `step_{ts-1}`. pub(crate) fn chain_step_at(ts: usize, chain_max_ts: usize) -> Option { - (chain_packed(chain_max_ts) && ts > 0 && ts < chain_max_ts).then_some(ts - 1) + (chain_packed(chain_max_ts) && ts > 0 && ts < chain_max_ts).then(|| ts - 1) } #[derive(Clone, Copy)] @@ -120,7 +120,7 @@ impl<'a> VarNameFmt<'a> { impl<'a> fmt::Display for VarNameFmt<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ns) = self.collapses_at() { - return write!(f, "{}.{}", ns.arg_name(), self.name); + return write!(f, "{}", ns.arg_name(self.name)); } let max_ts = self.meta.max_ts(self.name); if self.name == "chain" @@ -144,42 +144,24 @@ pub(crate) enum Side { /// The record namespace a collapsed Object state dict belongs to. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Collapse { - Side(Side), + IO(Side), Initials, } impl Side { - pub(crate) fn arg_name(self) -> &'static str { + pub(crate) fn arg_name(self, name: &str) -> String { match self { - Side::In => "in", - Side::Out => "out", + Side::In => format!("in_{name}"), + Side::Out => format!("out_{name}"), } } - fn schema_suffix(self) -> &'static str { - match self { - Side::In => "In", - Side::Out => "Out", - } - } -} - -impl From for Collapse { - fn from(side: Side) -> Self { - Collapse::Side(side) - } } impl Collapse { - pub(crate) fn arg_name(self) -> &'static str { + pub(crate) fn arg_name(self, name: &str) -> String { match self { - Collapse::Side(side) => side.arg_name(), - Collapse::Initials => "initials", - } - } - fn schema_suffix(self) -> &'static str { - match self { - Collapse::Side(side) => side.schema_suffix(), - Collapse::Initials => "Initials", + Collapse::IO(side) => format!("io.{}", side.arg_name(name)), + Collapse::Initials => format!("initials.{name}"), } } } @@ -216,9 +198,14 @@ pub(crate) fn dispatch_side(io: &ObjectIO) -> Side { } } -/// Schema name for a (action, namespace) pair, e.g. `LogToWoodIn`. -fn schema_name(action_name: &str, ns: impl Into) -> String { - format!("{action_name}{}", ns.into().schema_suffix()) +/// Initials schema name for a (action, namespace) pair, e.g. `LogToWoodInitials`. +fn schema_name_initials(action_name: &str) -> String { + format!("{action_name}Initials") +} + +/// IO schema name for a (action, namespace) pair, e.g. `LogToWoodIO`. +fn schema_name_io(action_name: &str) -> String { + format!("{action_name}IO") } /// Emit `record = ()` lines for any non-empty @@ -227,21 +214,21 @@ fn schema_name(action_name: &str, ns: impl Into) -> String { fn fmt_record_decls(loader: &Loader, w: &mut dyn fmt::Write) -> fmt::Result { let render = |entries: &[String]| entries.join(", "); for meta in &loader.actions_meta { - if !meta.in_entries.is_empty() { - let names: Vec = meta.in_entries.iter().map(|e| e.varname.clone()).collect(); - writeln!( - w, - "record {} = ({})", - schema_name(&meta.name, Side::In), - render(&names), - )?; - } - if !meta.out_entries.is_empty() { - let names: Vec = meta.out_entries.iter().map(|e| e.varname.clone()).collect(); + if !meta.in_entries.is_empty() || !meta.out_entries.is_empty() { + let names: Vec = meta + .in_entries + .iter() + .map(|e| Side::In.arg_name(&e.varname)) + .chain( + meta.out_entries + .iter() + .map(|e| Side::Out.arg_name(&e.varname)), + ) + .collect(); writeln!( w, "record {} = ({})", - schema_name(&meta.name, Side::Out), + schema_name_io(&meta.name), render(&names), )?; } @@ -261,7 +248,7 @@ fn fmt_record_decls(loader: &Loader, w: &mut dyn fmt::Write) -> fmt::Result { writeln!( w, "record {} = ({})", - schema_name(&meta.name, Collapse::Initials), + schema_name_initials(&meta.name), render(initials), )?; } @@ -274,10 +261,8 @@ fn fmt_record_decls(loader: &Loader, w: &mut dyn fmt::Write) -> fmt::Result { struct SubActionCall { sub_name: String, /// Name of the parent's synthesized private wildcard for the sub's - /// `in` record (None if the sub has no in record). - sub_in_var: Option, - /// Same, for the sub's `out` record. - sub_out_var: Option, + /// `io` record + sub_io_var: String, /// Script-side alias name (the `pick` in `var pick = action.subaction(...)`). /// `None` if the user didn't bind via `var`. Used to skip the alias from /// the parent's private wildcards list. @@ -287,7 +272,7 @@ struct SubActionCall { /// Walk the parent action's Insts and gather one `SubActionCall` per /// `Inst::SubAction`. Looks up each sub's record shape from the loader's /// `actions_meta`. -fn collect_sub_action_calls(action: &ActionContext, loader: &Loader) -> Vec { +fn collect_sub_action_calls(action: &ActionContext) -> Vec { let mut calls = Vec::new(); let mut idx_counter: HashMap = HashMap::new(); for inst in &action.insts { @@ -297,27 +282,10 @@ fn collect_sub_action_calls(action: &ActionContext, loader: &Loader) -> Vec Vec = sub_calls.iter().filter_map(|c| c.alias.clone()).collect(); - // Private wildcards: every (var, ts) except sub-action aliases, - // chain endpoints (public chain0/chain), packed chain intermediates - // (anchored via the `chain_steps` record), Object pre/post-form - // ts on collapsed sides, and Output Objects' script-final ts when - // packed into the `initials` record. Unpacked chain intermediates + // Private wildcards: every (var, ts) except sub-action aliases, state_header, chain + // endpoints (public chain0/chain), packed chain intermediates (anchored via the + // `chain_steps` record), Object pre/post-form ts on collapsed sides, and Output Objects' + // script-final ts when packed into the `initials` record. Unpacked chain intermediates // appear as scalar `chain1, chain2, ...` privates. let mut private_vars: Vec = Vec::new(); for var in &action.vars { @@ -394,6 +353,8 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - for i in 0..=max_ts { let skip = if var == "chain" { i == 0 || i == max_ts || chain_step_at(i, max_ts).is_some() + } else if var == "state_header" { + true } else { meta.collapsed_at(var, i).is_some() }; @@ -405,12 +366,8 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - } // Append synthesized sub-action typed privates last. for c in &sub_calls { - if let Some(name) = &c.sub_in_var { - private_vars.push(format!("{name} {}", schema_name(&c.sub_name, Side::In))); - } - if let Some(name) = &c.sub_out_var { - private_vars.push(format!("{name} {}", schema_name(&c.sub_name, Side::Out))); - } + let name = &c.sub_io_var; + private_vars.push(format!("{name} {}", schema_name_io(&c.sub_name))); } // Append the chain record typed private when packed. if chain_packed(meta.chain_max_ts) { @@ -418,11 +375,7 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - } // Append the initials record typed private when packed. if meta.initials_entries.is_some() { - private_vars.push(format!( - "{} {}", - Collapse::Initials.arg_name(), - schema_name(&action.name, Collapse::Initials), - )); + private_vars.push(format!("initials {}", schema_name_initials(&action.name),)); } if !private_vars.is_empty() { write!(w, ", private: ")?; @@ -464,8 +417,8 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - { writeln!( w, - " ArrayContains(in, {}::{}, {})", - schema_name(&action.name, Side::In), + " ArrayContains(io, {}::in_{}, {})", + schema_name_io(&action.name), o.varname, fmt_var_at(&o.varname, 0, max_ts), )?; @@ -476,8 +429,8 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - { writeln!( w, - " ArrayContains(out, {}::{}, {})", - schema_name(&action.name, Side::Out), + " ArrayContains(io, {}::out_{}, {})", + schema_name_io(&action.name), o.varname, fmt_var_at(&o.varname, max_ts, max_ts), )?; @@ -543,12 +496,8 @@ fn fmt_action(action: &ActionContext, loader: &Loader, w: &mut dyn fmt::Write) - let chain = vars["chain"]; let chain_next = chain.next(); let mut args: Vec = Vec::new(); - if let Some(name) = &call.sub_in_var { - args.push(name.clone()); - } - if let Some(name) = &call.sub_out_var { - args.push(name.clone()); - } + args.push(call.sub_io_var.clone()); + args.push("state_header".to_string()); args.push(format!("{chain}")); args.push(format!("{chain_next}")); writeln!(w, " {sub_name}({})", args.join(", "))?; @@ -627,38 +576,32 @@ fn fmt_bridges(loader: &Loader, w: &mut dyn fmt::Write) -> fmt::Result { let multi = is_multi_class(&meta.object_refs, &o.class); let bridge_name = bridge_predicate_name(&o.class, &meta.name, &o.varname, multi); - // Bridge predicate signature: state, chain0, chain (public); + // Bridge predicate signature: state, state_header, chain0, chain (public); // in , out private as needed. - write!(w, "{bridge_name}(state, chain0, chain")?; + write!(w, "{bridge_name}(state, state_header, chain0, chain")?; let mut priv_parts: Vec = Vec::new(); - if !meta.in_entries.is_empty() { - priv_parts.push(format!("in {}", schema_name(&meta.name, Side::In))); - } - if !meta.out_entries.is_empty() { - priv_parts.push(format!("out {}", schema_name(&meta.name, Side::Out))); + if !meta.in_entries.is_empty() || !meta.out_entries.is_empty() { + priv_parts.push(format!("io {}", schema_name_io(&meta.name))); } if !priv_parts.is_empty() { write!(w, ", private: {}", priv_parts.join(", "))?; } writeln!(w, ") = AND(")?; - // ArrayContains(, ::, state) + // ArrayContains(io, ::, state) writeln!( w, - " ArrayContains({}, {}::{}, state)", - side.arg_name(), - schema_name(&meta.name, side), - o.varname, + " ArrayContains(io, {}::{}, state)", + schema_name_io(&meta.name), + side.arg_name(&o.varname), )?; // Action call. let mut call_args: Vec = Vec::new(); - if !meta.in_entries.is_empty() { - call_args.push("in".to_string()); - } - if !meta.out_entries.is_empty() { - call_args.push("out".to_string()); + if !meta.in_entries.is_empty() || !meta.out_entries.is_empty() { + call_args.push("io".to_string()); } + call_args.push("state_header".to_string()); call_args.push("chain0".to_string()); call_args.push("chain".to_string()); writeln!(w, " {}({})", meta.name, call_args.join(", "))?; @@ -686,7 +629,7 @@ fn fmt_class(loader: &Loader, w: &mut dyn fmt::Write, class: &ClassMeta) -> fmt: let o = &meta.object_refs[*obj_index]; let multi = is_multi_class(&meta.object_refs, &o.class); let bridge_name = bridge_predicate_name(&o.class, action_name, &o.varname, multi); - writeln!(w, " {bridge_name}(state, chain0, chain)")?; + writeln!(w, " {bridge_name}(state, state_header, chain0, chain)")?; } writeln!(w, ")")?; Ok(()) diff --git a/libs/sdk/src/lib.rs b/libs/sdk/src/lib.rs index 4006787c..82c11ece 100644 --- a/libs/sdk/src/lib.rs +++ b/libs/sdk/src/lib.rs @@ -4,6 +4,7 @@ use std::fmt; use std::rc::Rc; use std::slice; use std::sync::Arc; +use std::sync::LazyLock; use anyhow::{Result, anyhow}; use itertools::zip_eq; @@ -167,6 +168,8 @@ enum Type { Raw, Int, Dict, + // The Vec contains the optional Record field names + Array(Arc>), } impl fmt::Display for Type { @@ -214,6 +217,7 @@ impl VarOrValue { Type::Raw => Some(()), Type::Int => v.as_int().map(|_| ()), Type::Dict => v.as_dictionary().map(|_| ()), + Type::Array(_) => v.as_array().map(|_| ()), } .ok_or_else(|| format!("type check: expected {}", typ).into()), Self::Var(Var { @@ -269,16 +273,29 @@ impl VarOrValue { }) => value.clone().expect("has value at exec time"), Self::Var(Var { value, + typ, key: Some(key), .. - }) => { - let dict = value - .as_ref() - .expect("has value at exec time") - .as_dictionary() - .expect("dict"); - dict.get(&StrKey::from(key)).unwrap().expect("key exists") - } + }) => match typ { + Type::Dict => { + let dict = value + .as_ref() + .expect("has value at exec time") + .as_dictionary() + .expect("dict"); + dict.get(&StrKey::from(key)).unwrap().expect("key exists") + } + Type::Array(record) => { + let array = value + .as_ref() + .expect("has value at exec time") + .as_array() + .expect("array"); + let idx = record.iter().position(|k| k == key).unwrap(); + array.get(idx).unwrap().expect("index exists") + } + _ => todo!("implement type {typ}"), + }, } } // Only call this at exec time @@ -299,16 +316,26 @@ impl VarOrValue { fn as_op_arg(&self) -> OperationArg { match self { Self::Value(value) => OperationArg::Literal(value.clone()), - Self::Var(Var { value, key, .. }) => { + Self::Var(Var { + typ, value, key, .. + }) => { let value = value.as_ref().expect("has value at exec time").clone(); if let Some(key) = key { - let dict = value.as_dictionary().expect("dict"); - let value = dict.get(&key.into()).unwrap().unwrap(); - OperationArg::Statement(Statement::Contains( - dict.into(), - key.clone().into(), - value.into(), - )) + let st_contains = match typ { + Type::Dict => { + let dict = value.as_dictionary().expect("dict"); + let value = dict.get(&key.into()).unwrap().unwrap(); + Statement::Contains(dict.into(), key.clone().into(), value.into()) + } + Type::Array(record) => { + let array = value.as_array().expect("array"); + let index = record.iter().position(|k| k == key).unwrap(); + let value = array.get(index).unwrap().unwrap(); + Statement::Contains(array.into(), (index as i64).into(), value.into()) + } + _ => todo!("support other types"), + }; + OperationArg::Statement(st_contains) } else { OperationArg::Literal(value) } @@ -452,6 +479,22 @@ impl ActionContext { } } +static RECORD_STATE_HEADER: LazyLock>> = LazyLock::new(|| { + Arc::new( + [ + "block_number", + "block_timestamp", + "block_hash", + "created", + "nullifiers", + "prior_state_history", + ] + .map(|s| s.to_string()) + .into_iter() + .collect::>(), + ) +}); + impl ActionHandle { // // Internal methods @@ -459,6 +502,23 @@ impl ActionHandle { fn new(name: String, exe_ctx: Option>>) -> Self { Self(Rc::new(RefCell::new(ActionContext::new(name, exe_ctx)))) } + fn state_header(&self) -> ArgHandle { + let mut ctx = self.0.borrow_mut(); + ctx.add_var("state_header".to_string()).unwrap_or(()); + let arg = Rc::new(RefCell::new(VarOrValue::var(Type::Array( + RECORD_STATE_HEADER.clone(), + )))); + arg.borrow_mut() + .set_var_name("state_header".to_string()) + .unwrap(); + if let Some(exe_rc) = ctx.exe_ctx.as_ref() { + let exe_ctx = exe_rc.borrow(); + let state_header = exe_ctx.tx_builder.state_header(); + arg.borrow_mut() + .set_value(Value::from(state_header.array())); + } + ArgHandle::new(self.clone(), arg) + } fn new_obj(exe_ctx: &ExeContext, class: &str) -> Dictionary { let type_hash = exe_ctx .module @@ -528,6 +588,7 @@ impl ActionHandle { (exe_ctx.module.clone(), scope_id, ctx.name.clone()) }; let mut scope = Scope::new(); + scope.push_constant("state_header", self.state_header()); let options = CallFnOptions::new().with_tag(self.clone()); let _result = module.engine.call_fn_with_options::( options, @@ -606,8 +667,9 @@ impl ActionHandle { } } } - let in_array = Array::new(in_dicts); - let out_array = Array::new(out_dicts); + let mut io_dicts = in_dicts; + io_dicts.extend(out_dicts); + let io_array = Array::new(io_dicts); // Build the `Initials` record value (the pre-identity Output // dicts) when the action has one. @@ -625,11 +687,11 @@ impl ActionHandle { // is collapsed; else a literal op-arg for the dict. let anchor_or_literal = |obj_name: &str, dict: &Dictionary, ts: usize| -> OperationArg { match meta.collapsed_at(obj_name, ts) { - Some(fmt_podlang::Collapse::Side(fmt_podlang::Side::In)) => { - (&in_array, meta.in_entry(obj_name).unwrap().0 as i64).into() + Some(fmt_podlang::Collapse::IO(fmt_podlang::Side::In)) => { + (&io_array, meta.in_entry(obj_name).unwrap().0 as i64).into() } - Some(fmt_podlang::Collapse::Side(fmt_podlang::Side::Out)) => { - (&out_array, meta.out_entry(obj_name).unwrap().0 as i64).into() + Some(fmt_podlang::Collapse::IO(fmt_podlang::Side::Out)) => { + (&io_array, meta.out_entry(obj_name).unwrap().0 as i64).into() } Some(fmt_podlang::Collapse::Initials) => { initials_anchor(obj_name).expect("collapsed_at promised an initials slot") @@ -669,7 +731,7 @@ impl ActionHandle { .bld .builder .priv_op(Operation::array_contains( - Value::from(in_array.clone()), + Value::from(io_array.clone()), idx as i64, Value::from(pre_dict.clone()), )) @@ -683,7 +745,7 @@ impl ActionHandle { .bld .builder .priv_op(Operation::array_contains( - Value::from(out_array.clone()), + Value::from(io_array.clone()), idx as i64, Value::from(post_dict.clone()), )) @@ -843,20 +905,20 @@ impl ActionHandle { for pending in pending_object_events { let varname = &pending.varname; let io = pending.io; - let new_anchor: Option = match io { - ObjectIO::Output | ObjectIO::Mutate => meta - .out_entry(varname) - .filter(|(_, e)| !e.needs_wildcard) - .map(|(idx, _)| (&out_array, idx as i64).into()), - ObjectIO::Input => None, - }; let old_anchor: Option = match io { ObjectIO::Input | ObjectIO::Mutate => meta .in_entry(varname) .filter(|(_, e)| !e.needs_wildcard) - .map(|(idx, _)| (&in_array, idx as i64).into()), + .map(|(idx, _)| (&io_array, idx as i64).into()), ObjectIO::Output => None, }; + let new_anchor: Option = match io { + ObjectIO::Output | ObjectIO::Mutate => meta + .out_entry(varname) + .filter(|(_, e)| !e.needs_wildcard) + .map(|(idx, _)| (&io_array, idx as i64).into()), + ObjectIO::Input => None, + }; let pre_ts = pending.post_ts - 1; let post_ts = pending.post_ts; let chain_anchor = chain_step_anchor(post_ts); @@ -980,11 +1042,15 @@ impl ActionHandle { let ts = *current_ts.get(obj).unwrap_or(&0); let dict_arg = anchor_or_literal(obj, &dict, ts); for (key, value) in kvs { - let v = value.borrow().as_value().clone(); + let arg = value.borrow().as_op_arg().clone(); let st = exe_ctx .bld .builder - .priv_op(Operation::dict_contains(dict_arg.clone(), key.clone(), v)) + .priv_op(Operation::dict_contains( + dict_arg.clone(), + key.clone(), + arg, + )) .unwrap(); body_sts.push(st); } @@ -996,17 +1062,22 @@ impl ActionHandle { old_dict, new_dict, } => { - let old = old_dict.clone().expect("Update old_dict captured at Rhai"); - let new = new_dict.clone().expect("Update new_dict captured at Rhai"); - let v = value.borrow().as_value().clone(); + let old_dict = old_dict.clone().expect("Update old_dict captured at Rhai"); + let new_dict = new_dict.clone().expect("Update new_dict captured at Rhai"); + let arg = value.borrow().as_op_arg().clone(); let ts_before = *current_ts.get(obj).unwrap_or(&0); let ts_after = ts_before + 1; - let new_arg = anchor_or_literal(obj, &new, ts_after); - let old_arg = anchor_or_literal(obj, &old, ts_before); + let new_dict_arg = anchor_or_literal(obj, &new_dict, ts_after); + let old_dict_arg = anchor_or_literal(obj, &old_dict, ts_before); let st = exe_ctx .bld .builder - .priv_op(Operation::dict_update(old_arg, key.clone(), v, new_arg)) + .priv_op(Operation::dict_update( + old_dict_arg, + key.clone(), + arg, + new_dict_arg, + )) .unwrap(); body_sts.push(st); if let Some(t) = current_ts.get_mut(obj) { @@ -1028,9 +1099,10 @@ impl ActionHandle { let st_action = { let mut exe_ctx = exe_rc.borrow_mut(); + let state_header = exe_ctx.tx_builder.state_header().array(); exe_ctx .bld - .apply_custom_pred_simple(false, &action, sts) + .apply_custom_pred(false, &action, map!({"state_header" => state_header}), sts) .unwrap() }; @@ -1050,11 +1122,10 @@ impl ActionHandle { // doesn't matter which form we hand the bridge. let obj_ref = &meta.object_refs[object_refs_index]; let varname = &obj_ref.varname; - let (bridge_array, entry_idx) = match fmt_podlang::dispatch_side(&obj_ref.io) { - fmt_podlang::Side::In => (in_array.clone(), meta.in_entry(varname).unwrap().0), - fmt_podlang::Side::Out => { - (out_array.clone(), meta.out_entry(varname).unwrap().0) - } + + let entry_idx = match fmt_podlang::dispatch_side(&obj_ref.io) { + fmt_podlang::Side::In => meta.in_entry(varname).unwrap().0, + fmt_podlang::Side::Out => meta.out_entry(varname).unwrap().0, }; let st_is_x = module.build_is_x( &mut exe_ctx.bld, @@ -1063,7 +1134,7 @@ impl ActionHandle { &class, object_refs_index, st_action.clone(), - &bridge_array, + &io_array, entry_idx, &obj_dict, ); @@ -1412,6 +1483,22 @@ fn arg_sub(a: ArgHandle, b: ArgHandle) -> RuntimeResult { Ok(ArgHandle::new(a.ctx.clone(), value)) } +/// operator+ for maybe-var types +fn arg_add(a: ArgHandle, b: ArgHandle) -> RuntimeResult { + type_check_args([(&a, Type::Int), (&b, Type::Int)])?; + // TODO: Handle the case where a and b are not var + let value = Rc::new(RefCell::new(VarOrValue::var(Type::Int))); + let ctx = a.ctx.0.borrow(); + ctx.assert_unsafe(true)?; + if ctx.exe_ctx.is_some() { + let a = a.arg.borrow().as_value().as_int().expect("int"); + let b = b.arg.borrow().as_value().as_int().expect("int"); + let result = a.checked_add(b).expect("no overflow"); + value.borrow_mut().set_value(Value::from(result)); + } + Ok(ArgHandle::new(a.ctx.clone(), value)) +} + /// Try to get the pod2 Value or promote a native type to it. fn _try_value_from_dynamic(v: Dynamic) -> Result { let v = match v.try_cast_result::() { @@ -1569,15 +1656,6 @@ impl ActionMeta { self.total_outputs.iter() } - /// Find this Object's `in` entry. Returns its slot in the - /// `In` record and the entry shape. - pub(crate) fn in_entry(&self, varname: &str) -> Option<(usize, &EntryShape)> { - self.in_entries - .iter() - .enumerate() - .find(|(_, e)| e.varname == varname) - } - /// Slot for `varname` in the `Initials` record, if such a /// record exists for this action. pub(crate) fn initials_slot(&self, varname: &str) -> Option { @@ -1587,10 +1665,20 @@ impl ActionMeta { .position(|name| name == varname) } + /// Find this Object's `in` entry. Returns its slot in the + /// `In` record and the entry shape. + pub(crate) fn in_entry(&self, varname: &str) -> Option<(usize, &EntryShape)> { + self.in_entries + .iter() + .enumerate() + .find(|(_, e)| e.varname == varname) + } + pub(crate) fn out_entry(&self, varname: &str) -> Option<(usize, &EntryShape)> { self.out_entries .iter() .enumerate() + .map(|(i, e)| (i + self.in_entries.len(), e)) .find(|(_, e)| e.varname == varname) } @@ -1602,9 +1690,8 @@ impl ActionMeta { } /// At a given ts, an object's state might be "collapsed" into a - /// record (in, out, or initials) rather than held in its own - /// wildcard. Returns which record, or None if it is not collapsed - /// at this ts. + /// record (io or initials) rather than held in its own wildcard. Returns which record, or + /// None if it is not collapsed at this ts. pub(crate) fn collapsed_at(&self, varname: &str, ts: usize) -> Option { let max_ts = self.max_ts(varname); if ts == 0 @@ -1612,14 +1699,14 @@ impl ActionMeta { .in_entry(varname) .is_some_and(|(_, e)| !e.needs_wildcard) { - return Some(fmt_podlang::Collapse::Side(fmt_podlang::Side::In)); + return Some(fmt_podlang::Collapse::IO(fmt_podlang::Side::In)); } if ts == max_ts && self .out_entry(varname) .is_some_and(|(_, e)| !e.needs_wildcard) { - return Some(fmt_podlang::Collapse::Side(fmt_podlang::Side::Out)); + return Some(fmt_podlang::Collapse::IO(fmt_podlang::Side::Out)); } // If we have an "initials" record for this var, and we are at // the penultimate ts, then the var must be collapsed into @@ -2478,6 +2565,15 @@ fn new_engine() -> Engine { let ctx = b.ctx.clone(); arg_sub(ArgHandle::literal(ctx, Value::from(a)), b) }) + .register_fn("+", arg_add) + .register_fn("+", |a: ArgHandle, b: i64| -> RuntimeResult { + let ctx = a.ctx.clone(); + arg_add(a, ArgHandle::literal(ctx, Value::from(b))) + }) + .register_fn("+", |a: i64, b: ArgHandle| -> RuntimeResult { + let ctx = b.ctx.clone(); + arg_add(ArgHandle::literal(ctx, Value::from(a)), b) + }) .register_indexer_get(ArgHandle::entry); engine @@ -2505,6 +2601,7 @@ impl Sdk { for action in actions { let action_handle = ActionHandle::new(action.to_string(), None); let mut scope = Scope::new(); + scope.push_constant("state_header", action_handle.state_header()); let options = CallFnOptions::new().with_tag(action_handle.clone()); let _result = self.engine.call_fn_with_options::( options, diff --git a/libs/sdk/src/tests.rs b/libs/sdk/src/tests.rs index 0cee2c9a..80d37727 100644 --- a/libs/sdk/src/tests.rs +++ b/libs/sdk/src/tests.rs @@ -250,7 +250,7 @@ fn test_sdk_2() { [plugin] name = "test" version = "0.1.0" - module_hash = "9e84b0fb084e8be99f74c7788e3c43d13927826f0e0315f99d9b9a678c24103b" + module_hash = "b356b892ed13e203eddd3f0ea85c67e56ac3756ac7fe935bf387767b0dbfc2fd" [[classes]] name = "Log" @@ -316,28 +316,27 @@ fn test_records_form_just_output() { .load_module_from_src_actions(craft_src, &["JustOutput"]) .unwrap(); - let expected = r#"record JustOutputOut = (x) + let expected = r#"record JustOutputIO = (out_x) record JustOutputInitials = (x) // Actions -JustOutput(out JustOutputOut, chain0, chain, private: initials JustOutputInitials) = AND( - tx::TxInsert(chain0, chain, initials.x, out.x, @self_predicate(IsFoo)) +JustOutput(io JustOutputIO, state_header StateHeader, chain0, chain, private: initials JustOutputInitials) = AND( + tx::TxInsert(chain0, chain, initials.x, io.out_x, @self_predicate(IsFoo)) ) // Bridges -IsFooFromJustOutput(state, chain0, chain, private: out JustOutputOut) = AND( - ArrayContains(out, JustOutputOut::x, state) - JustOutput(out, chain0, chain) +IsFooFromJustOutput(state, state_header, chain0, chain, private: io JustOutputIO) = AND( + ArrayContains(io, JustOutputIO::out_x, state) + JustOutput(io, state_header, chain0, chain) ) // Classes IsFoo(state, state_header StateHeader, chain0, chain) = OR( - IsFooFromJustOutput(state, chain0, chain) -) -"#; + IsFooFromJustOutput(state, state_header, chain0, chain) +)"#; assert!( module.podlang_src.contains(expected), "records-form mismatch.\nexpected fragment:\n{expected}\nactual:\n{}", @@ -367,40 +366,38 @@ fn test_records_form_input_output_update() { .load_module_from_src_actions(craft_src, &["LogToWood"]) .unwrap(); - let expected = r#"record LogToWoodIn = (log) -record LogToWoodOut = (wood) + let expected = r#"record LogToWoodIO = (in_log, out_wood) record LogToWoodInitials = (wood) // Actions -LogToWood(in LogToWoodIn, out LogToWoodOut, chain0, chain, private: chain1, wood0, key, initials LogToWoodInitials) = AND( +LogToWood(io LogToWoodIO, state_header StateHeader, chain0, chain, private: chain1, wood0, key, initials LogToWoodInitials) = AND( DictUpdate(wood0, "key", key, initials.wood) - tx::TxDelete(chain0, chain1, in.log, @self_predicate(IsLog)) - tx::TxInsert(chain1, chain, initials.wood, out.wood, @self_predicate(IsWood)) + tx::TxDelete(chain0, chain1, io.in_log, @self_predicate(IsLog)) + tx::TxInsert(chain1, chain, initials.wood, io.out_wood, @self_predicate(IsWood)) ) // Bridges -IsLogFromLogToWood(state, chain0, chain, private: in LogToWoodIn, out LogToWoodOut) = AND( - ArrayContains(in, LogToWoodIn::log, state) - LogToWood(in, out, chain0, chain) +IsLogFromLogToWood(state, state_header, chain0, chain, private: io LogToWoodIO) = AND( + ArrayContains(io, LogToWoodIO::in_log, state) + LogToWood(io, state_header, chain0, chain) ) -IsWoodFromLogToWood(state, chain0, chain, private: in LogToWoodIn, out LogToWoodOut) = AND( - ArrayContains(out, LogToWoodOut::wood, state) - LogToWood(in, out, chain0, chain) +IsWoodFromLogToWood(state, state_header, chain0, chain, private: io LogToWoodIO) = AND( + ArrayContains(io, LogToWoodIO::out_wood, state) + LogToWood(io, state_header, chain0, chain) ) // Classes IsLog(state, state_header StateHeader, chain0, chain) = OR( - IsLogFromLogToWood(state, chain0, chain) + IsLogFromLogToWood(state, state_header, chain0, chain) ) IsWood(state, state_header StateHeader, chain0, chain) = OR( - IsWoodFromLogToWood(state, chain0, chain) -) -"#; + IsWoodFromLogToWood(state, state_header, chain0, chain) +)"#; assert!( module.podlang_src.contains(expected), "records-form mismatch.\nexpected fragment:\n{expected}\nactual:\n{}", @@ -439,11 +436,10 @@ fn test_records_form_subaction() { // Parent action signature + sub-action call body. `bar`'s // out-side collapses (no sub-field reads, no Intro use) so the // wildcard is dropped and body refs render as `out.bar`. - let expected_parent = r#"MineBar(out MineBarOut, chain0, chain, private: chain1, _UseFoo_in_0 UseFooIn, _UseFoo_out_0 UseFooOut, initials MineBarInitials) = AND( - UseFoo(_UseFoo_in_0, _UseFoo_out_0, chain0, chain1) - tx::TxInsert(chain1, chain, initials.bar, out.bar, @self_predicate(IsBar)) -) -"#; + let expected_parent = r#"MineBar(io MineBarIO, state_header StateHeader, chain0, chain, private: chain1, _UseFoo_io_0 UseFooIO, initials MineBarInitials) = AND( + UseFoo(_UseFoo_io_0, state_header, chain0, chain1) + tx::TxInsert(chain1, chain, initials.bar, io.out_bar, @self_predicate(IsBar)) +)"#; assert!( module.podlang_src.contains(expected_parent), "MineBar records-form mismatch.\nexpected:\n{expected_parent}\nactual:\n{}", @@ -452,9 +448,9 @@ fn test_records_form_subaction() { // The bridge for MineBar's direct output (`bar`) should exist. assert!( - module - .podlang_src - .contains("IsBarFromMineBar(state, chain0, chain, private: out MineBarOut) = AND("), + module.podlang_src.contains( + "IsBarFromMineBar(state, state_header, chain0, chain, private: io MineBarIO) = AND(" + ), "missing IsBarFromMineBar bridge:\n{}", module.podlang_src ); @@ -491,32 +487,30 @@ fn test_records_form_mutate() { .load_module_from_src_actions(craft_src, &["UseFoo"]) .unwrap(); - let expected = r#"record UseFooIn = (foo) -record UseFooOut = (foo) + let expected = r#"record UseFooIO = (in_foo, out_foo) // Actions -UseFoo(in UseFooIn, out UseFooOut, chain0, chain, private: foo0, dur) = AND( - ArrayContains(in, UseFooIn::foo, foo0) +UseFoo(io UseFooIO, state_header StateHeader, chain0, chain, private: foo0, dur) = AND( + ArrayContains(io, UseFooIO::in_foo, foo0) Gt(foo0.durability, 0) Sum(dur, 1, foo0.durability) - DictUpdate(foo0, "durability", dur, out.foo) - tx::TxMutate(chain0, chain, foo0, out.foo, @self_predicate(IsFoo)) + DictUpdate(foo0, "durability", dur, io.out_foo) + tx::TxMutate(chain0, chain, foo0, io.out_foo, @self_predicate(IsFoo)) ) // Bridges -IsFooFromUseFoo(state, chain0, chain, private: in UseFooIn, out UseFooOut) = AND( - ArrayContains(out, UseFooOut::foo, state) - UseFoo(in, out, chain0, chain) +IsFooFromUseFoo(state, state_header, chain0, chain, private: io UseFooIO) = AND( + ArrayContains(io, UseFooIO::out_foo, state) + UseFoo(io, state_header, chain0, chain) ) // Classes IsFoo(state, state_header StateHeader, chain0, chain) = OR( - IsFooFromUseFoo(state, chain0, chain) -) -"#; + IsFooFromUseFoo(state, state_header, chain0, chain) +)"#; assert!( module.podlang_src.contains(expected), "records-form mismatch.\nexpected fragment:\n{expected}\nactual:\n{}", @@ -562,3 +556,76 @@ fn Bad(action) {{ } } } + +#[allow(clippy::cloned_ref_to_slice_refs)] +#[test] +fn test_sdk_state_header() { + let manifest_src = r#" + [plugin] + name = "test" + version = "0.1.0" + module_hash = "a8ae566dddbe81cdf1f7d15396eadb748cdf4f0a8976936c406199b556d62c10" + + [[classes]] + name = "Ticker" + emoji = "🌲" + description = "A ticker." + + [[actions]] + name = "MakeTicker" + emoji = "🌲" + description = "Make a ticker." + + [[actions]] + name = "Tick" + emoji = "🪵" + description = "Tick the ticker." + "#; + + let craft_src = r#" + fn MakeTicker(action) { + var ticker = action.output("Ticker"); + ticker.set([ + ["tick", 0], + ["ts", state_header.block_timestamp] + ]); + } + + fn Tick(action) { + var ticker = action.mutate("Ticker"); + var min_ts = unsafe { ticker.ts + 3600 }; + action.st_sum(ticker.ts, 3600, min_ts); + action.st_gt(state_header.block_timestamp, min_ts); + var tick1 = unsafe { ticker.tick + 1 }; + action.st_sum(ticker.tick, 1, tick1); + ticker.update("tick", tick1); + ticker.update("ts", state_header.block_timestamp); + } +"#; + + let manifest: Manifest = toml::from_str(manifest_src).unwrap(); + + let sdk = Sdk::default(); + let module = sdk + .load_module_from_src_manifest(craft_src, &manifest) + .unwrap(); + + println!("{}", module.podlang_src); + + let mut state = TestState::default(); + + println!("exe MakeTicker"); + let executor = module.executor(true, grounding_witness(&state, &[])); + let res = executor.action("MakeTicker", vec![]).unwrap(); + let ticker0_tx = res.tx.clone(); + let [ticker0] = res.objs(); + apply_tx(&mut state, &ticker0_tx); + + println!("exe Tick"); + state.next_block(4000); + let executor = module.executor(true, grounding_witness(&state, &[ticker0.obj.commitment()])); + let res = executor.action("Tick", vec![ticker0]).unwrap(); + let ticker1_tx = res.tx.clone(); + let [_ticker1] = res.objs(); + apply_tx(&mut state, &ticker1_tx); +} diff --git a/libs/txlib/src/lib.rs b/libs/txlib/src/lib.rs index 215a6003..ddc11312 100644 --- a/libs/txlib/src/lib.rs +++ b/libs/txlib/src/lib.rs @@ -51,7 +51,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// root hash and to verify synchronizer-supplied membership proofs. Full /// containers are not carried -- callers prove each input's liveness with a /// per-object Merkle proof packaged in a [`GroundingWitness`]. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StateHeader { /// Execution block number diff --git a/services/synchronizer/src/app_db.rs b/services/synchronizer/src/app_db.rs index d6de416f..a30ade74 100644 --- a/services/synchronizer/src/app_db.rs +++ b/services/synchronizer/src/app_db.rs @@ -4,12 +4,12 @@ use anyhow::{anyhow, Context, Result}; use pod2::{ backends::plonky2::primitives::merkletree::{self, MerkleProof}, middleware::{ - containers::{Array, Set}, - db::DB as PodDb, + containers::{Array, ContainerKind, Set}, + db::{Read, DB as PodDb, TX as PodTx}, Hash, RawValue, Value, EMPTY_HASH, }, }; -use rocksdb::{Options, TransactionDB, TransactionDBOptions}; +use rocksdb::{DBAccess, Options, ReadOptions, Transaction, TransactionDB, TransactionDBOptions}; use crate::head::StateRoots; @@ -27,6 +27,13 @@ fn value_key(raw: RawValue) -> Vec { k } +fn kind_key(root: Hash) -> Vec { + let mut k = Vec::with_capacity(2 + 4); + k.extend_from_slice(b"k/"); + k.extend_from_slice(&RawValue(root.0).to_bytes()); + k +} + /// Whether the created `Array` holds `commitment` at `index`: the leaf there /// must equal it. A prefetched index is only a hint until this confirms the /// array actually holds the commitment at that position, so the read path and @@ -139,52 +146,108 @@ impl AppDb { } } -impl merkletree::db::DB for AppDb { +fn load_node_db(db: &impl DBAccess, hash: Hash) -> Result> { + if hash == EMPTY_HASH { + return Ok(Some(merkletree::Node::Intermediate( + merkletree::Intermediate::new(EMPTY_HASH, EMPTY_HASH), + ))); + } + + let node_key = node_key(hash); + match db + .get_opt(&node_key, &ReadOptions::default()) + .map_err(|e| anyhow!("rocksdb: get failed: {e}"))? + { + None => Ok(None), + Some(bytes) => Ok(Some(merkletree::Node::decode(bytes.as_ref())?)), + } +} + +fn store_node_tx<'a>(tx: &Transaction<'a, TransactionDB>, node: merkletree::Node) -> Result<()> { + let node_key = node_key(node.hash()); + tx.put(&node_key, node.encode()?) + .map_err(|e| anyhow!("rocksdb transaction put failed: {e}")) +} + +impl merkletree::db::Read for AppDb { fn load_node(&self, hash: Hash) -> Result> { - if hash == EMPTY_HASH { - return Ok(Some(merkletree::Node::Intermediate( - merkletree::Intermediate::new(EMPTY_HASH, EMPTY_HASH), - ))); - } + load_node_db(&*self.db, hash) + } +} - match self.db.get(node_key(hash))? { - None => Ok(None), - Some(bytes) => Ok(Some(merkletree::Node::decode(bytes.as_ref())?)), - } +impl merkletree::db::DB for AppDb { + fn tx<'a>(&'a self) -> Box { + PodDb::tx(self) } +} - fn store_node(&mut self, node: merkletree::Node) -> Result<()> { - self.db - .put(node_key(node.hash()), node.encode()?) - .map_err(|err| anyhow!("rocksdb transaction put failed: {err}")) +pub(crate) struct AppTx<'a> { + tx: rocksdb::Transaction<'a, rocksdb::TransactionDB>, + db: AppDb, +} + +impl<'a> merkletree::db::Read for AppTx<'a> { + fn load_node(&self, hash: Hash) -> anyhow::Result> { + load_node_db(&self.tx, hash) } } -impl PodDb for AppDb { +impl<'a> merkletree::db::TX for AppTx<'a> { + fn store_node(&mut self, node: merkletree::Node) -> anyhow::Result<()> { + store_node_tx(&self.tx, node) + } + fn commit(self: Box) -> anyhow::Result<()> { + panic!("use middleware::db::TX::commit") + } +} + +impl<'a> Read for AppTx<'a> { fn load_value(&self, raw: RawValue) -> anyhow::Result> { - match self.db.get(value_key(raw))? { + match self.tx.get(value_key(raw))? { None => Ok(None), Some(bytes) => Ok(Some({ if bytes.is_empty() { Value::from(raw) } else { - Value::from_bytes(bytes.as_ref(), self.clone_box())? + Value::from_bytes(bytes.as_ref(), self.db.clone_box())? } })), } } + fn load_kind(&self, root: Hash) -> anyhow::Result> { + if root == EMPTY_HASH { + return Ok(Some( + *ContainerKind::default() + .set_dictionary() + .set_set() + .set_array(), + )); + } + // We use `get_for_update` because this method is part of a transaction, and it will be + // used by `update_kind`, so we want and exclusive lock after the value is read to + // guarantee no data-races in the merge update. + self.tx + .get_for_update(kind_key(root), true) + .map(|opt| { + opt.map(|bytes| match bytes.len() { + 1 => Ok(ContainerKind(bytes[0])), + l => Err(anyhow!("db: invalid kind len: {}", l)), + }) + })? + .transpose() + } +} +impl<'a> PodTx for AppTx<'a> { fn store_value(&mut self, value: Value) -> anyhow::Result<()> { let value_key = value_key(value.raw()); - let tx = self.db.transaction(); - if let Some(old_value_bytes) = tx.get_for_update(&value_key, true)? { - let is_raw = old_value_bytes.is_empty(); - // If we had a non-RawValue stored don't overwrite it (specially not with a - // RawValue). Also skip redundant RawValue overwrite. - if !is_raw || value.is_raw() { + if let Some(old_value_bytes) = self.tx.get(&value_key)? { + // Never overwrite an old value with a RawValue. Skip overwrite if old value is + // already non-RawValue. + if value.is_raw() || !old_value_bytes.is_empty() { return Ok(()); } - } + }; let value_bytes = if value.is_raw() { // For RawValue we store an empty vector because it's a duplicate of the key. // This way we can easily check for RawValue without decoding. @@ -192,14 +255,59 @@ impl PodDb for AppDb { } else { Value::to_bytes(&value) }; - tx.put(value_key, value_bytes)?; - Ok(tx.commit()?) + Ok(self.tx.put(value_key, value_bytes)?) + } + fn update_kind(&mut self, root: Hash, kind: ContainerKind) -> anyhow::Result<()> { + let kind = match self.load_kind(root).expect("ok") { + Some(old_kind) => ContainerKind(old_kind.0 | kind.0), + None => kind, + }; + let kind_key = kind_key(root); + Ok(self.tx.put(&kind_key, [kind.0])?) } + fn commit(self: Box) -> anyhow::Result<()> { + Ok(self.tx.commit()?) + } +} - fn is_persistent(&self) -> bool { - true +impl Read for AppDb { + fn load_value(&self, raw: RawValue) -> anyhow::Result> { + match self.db.get(value_key(raw))? { + None => Ok(None), + Some(bytes) => Ok(Some({ + if bytes.is_empty() { + Value::from(raw) + } else { + Value::from_bytes(bytes.as_ref(), self.clone_box())? + } + })), + } } + fn load_kind(&self, root: Hash) -> anyhow::Result> { + if root == EMPTY_HASH { + return Ok(Some( + *ContainerKind::default() + .set_dictionary() + .set_set() + .set_array(), + )); + } + Ok(self.db.get(kind_key(root)).map(|opt| { + opt.map(|bytes| { + assert_eq!(1, bytes.len()); + ContainerKind(bytes[0]) + }) + })?) + } +} +impl PodDb for AppDb { + fn tx<'a>(&'a self) -> Box { + Box::new(AppTx { + tx: self.db.transaction(), + db: self.clone(), + }) + } fn clone_box(&self) -> Box { Box::new(self.clone()) }