diff --git a/examples/craft-basics/manifest.toml b/examples/craft-basics/manifest.toml index 464f86e8..9ee73dd0 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 = "d7edcb9150d12af76a54fbbac7b00b8bb24fab61bc1395b65da47547ad5d1b42" +module_hash = "bdaa861c0f1fe9293d22b87e77fcb419322cdd4ba49bdbd77d6a15e45a2776f2" [[classes]] name = "Log" diff --git a/examples/craft-rocket/manifest.toml b/examples/craft-rocket/manifest.toml index 3050eda8..6cba7745 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 = "45198abe74c702551d22d7b8cbdf89f042d9cf67d4aa7f09edfe387966909d02" +module_hash = "ca07c2c335a9513eb2ad79e94f69280042ebdd5764fa780c142445a1df8ef523" # ── raw resources ──────────────────────────────────────────────────────────── diff --git a/libs/sdk/src/fmt_podlang.rs b/libs/sdk/src/fmt_podlang.rs index b0622dc3..60a7a53b 100644 --- a/libs/sdk/src/fmt_podlang.rs +++ b/libs/sdk/src/fmt_podlang.rs @@ -661,6 +661,12 @@ fn fmt_class(loader: &Loader, w: &mut dyn fmt::Write, class: &ClassMeta) -> fmt: let bridge_name = bridge_predicate_name(&o.class, action_name, &o.varname, multi); writeln!(w, " {bridge_name}(state, state_header, chain0, chain)")?; } + // Every generated class is transferable. The self-predicate hash binds + // Rekey to this class. + writeln!( + w, + " rk::Rekey(state, chain0, chain, @self_predicate(Is{name}))" + )?; writeln!(w, ")")?; Ok(()) } diff --git a/libs/sdk/src/lib.rs b/libs/sdk/src/lib.rs index e8e98a59..89a466de 100644 --- a/libs/sdk/src/lib.rs +++ b/libs/sdk/src/lib.rs @@ -2,7 +2,6 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt; use std::rc::Rc; -use std::slice; use std::sync::Arc; use std::sync::LazyLock; @@ -2219,10 +2218,10 @@ pub struct ClassMeta { /// The Loader is used to store declarative module information at Load time. struct Loader { - // The frozen chain-primitive batch (TxInsert/TxMutate/TxDelete). - // The only txlib module the rendered plugin source imports, so - // plugin module hashes survive churn in the replay/finalize batch. + // Frozen TxInsert/TxMutate/TxDelete batch imported by generated plugins. tx_events_mod: Arc, + // Frozen Rekey batch imported by every generated class guard. + rekey_mod: Arc, txlib_mod: Arc, dependencies: Vec, actions: Vec, @@ -2261,12 +2260,17 @@ impl Loader { fn new(actions: Vec) -> Result { let tx_events_mod = Arc::new(txlib::predicates::events_module()); + let rekey_mod = Arc::new(txlib::predicates::rekey_module()); let txlib_mod = Arc::new(txlib::predicates::module()); let dependencies = vec![ Dependency::Module { name: "tx".to_string(), hash: tx_events_mod.id(), }, + Dependency::Module { + name: "rk".to_string(), + hash: rekey_mod.id(), + }, Dependency::Intro { pred: "Vdf(count, input, output)".to_string(), hash: *STANDARD_VDF_VD_HASH, @@ -2284,6 +2288,7 @@ impl Loader { let classes = Self::actions_to_classes(&actions_meta); Ok(Self { tx_events_mod, + rekey_mod, txlib_mod, dependencies, actions, @@ -2325,7 +2330,7 @@ impl Loader { podlang_src.as_str(), "root", ¶ms, - slice::from_ref(&self.tx_events_mod), + &[self.tx_events_mod.clone(), self.rekey_mod.clone()], ) .expect("compiles"), ); @@ -2341,6 +2346,7 @@ impl Loader { .collect(); SdkModule { tx_events_mod: self.tx_events_mod, + rekey_mod: self.rekey_mod, txlib_mod: self.txlib_mod, podlang_src, actions: self.actions_meta, @@ -2358,6 +2364,7 @@ impl Loader { /// An SdkModule contains a loaded module and allows executing actions. pub struct SdkModule { tx_events_mod: Arc, + rekey_mod: Arc, txlib_mod: Arc, podlang_src: String, actions: Vec, @@ -2482,9 +2489,10 @@ impl SdkModule { .apply_custom_pred_simple(false, &bridge_name, vec![st_array_contains, st_action]) .expect("apply bridge predicate"); - // Step 3: IsX OR with the bridge at the right branch. + // Step 3: Select the action's IsX branch. The final slot is reserved + // for Rekey and remains empty during normal action execution. let class_meta = self.class_by_name(class); - let mut branch_sts = vec![Statement::None; class_meta.actions.len()]; + let mut branch_sts = vec![Statement::None; class_meta.actions.len() + 1]; let class_st_index = self.object_index_class_st_index[&(action_name.to_string(), object_refs_index)]; branch_sts[class_st_index] = st_bridge; @@ -2595,6 +2603,7 @@ impl Executor { let params = Params::default(); let modules = vec![ module.tx_events_mod.clone(), + module.rekey_mod.clone(), module.txlib_mod.clone(), module.module.clone(), ]; diff --git a/libs/sdk/src/tests.rs b/libs/sdk/src/tests.rs index 84abc3f8..598f6538 100644 --- a/libs/sdk/src/tests.rs +++ b/libs/sdk/src/tests.rs @@ -260,7 +260,7 @@ fn test_sdk_2() { [plugin] name = "test" version = "0.1.0" - module_hash = "75119e01ece11fd33d43bb2f68239c92450d338140f6757dd286abbb628b5712" + module_hash = "ef2deed1853640114f4943592279ddc94ca728cadb86aa5709992465e22af4cc" [[classes]] name = "Log" @@ -386,6 +386,7 @@ IsFooFromJustOutput(state, state_header, chain0, chain, private: io JustOutputIO IsFoo(state, state_header StateHeader, chain0, chain) = OR( IsFooFromJustOutput(state, state_header, chain0, chain) + rk::Rekey(state, chain0, chain, @self_predicate(IsFoo)) )"#; assert!( module.podlang_src.contains(expected), @@ -443,10 +444,12 @@ IsWoodFromLogToWood(state, state_header, chain0, chain, private: io LogToWoodIO) IsLog(state, state_header StateHeader, chain0, chain) = OR( IsLogFromLogToWood(state, state_header, chain0, chain) + rk::Rekey(state, chain0, chain, @self_predicate(IsLog)) ) IsWood(state, state_header StateHeader, chain0, chain) = OR( IsWoodFromLogToWood(state, state_header, chain0, chain) + rk::Rekey(state, chain0, chain, @self_predicate(IsWood)) )"#; assert!( module.podlang_src.contains(expected), @@ -560,6 +563,7 @@ IsFooFromUseFoo(state, state_header, chain0, chain, private: io UseFooIO) = AND( IsFoo(state, state_header StateHeader, chain0, chain) = OR( IsFooFromUseFoo(state, state_header, chain0, chain) + rk::Rekey(state, chain0, chain, @self_predicate(IsFoo)) )"#; assert!( module.podlang_src.contains(expected), @@ -871,7 +875,7 @@ fn test_sdk_state_header() { [plugin] name = "test" version = "0.1.0" - module_hash = "a8ae566dddbe81cdf1f7d15396eadb748cdf4f0a8976936c406199b556d62c10" + module_hash = "34bb2d3c2baeccd532cb02405a01b80a06f8265c3f7346d1e6123b6c0b63fb32" [[classes]] name = "Ticker" @@ -1302,3 +1306,85 @@ fn test_set_guards() { assert!(err.contains(expected), "{action}: {err}"); } } + +/// Prove Rekey end to end through an SDK-generated class guard. +#[allow(clippy::cloned_ref_to_slice_refs)] +#[test] +fn test_generated_class_rekey() { + let craft_src = r#" + fn SpawnFoo(action) { + var foo = action.output("Foo"); + foo.set([["durability", 100]]); + } +"#; + let sdk = Sdk::default(); + let module = sdk + .load_module_from_src_actions(craft_src, &["SpawnFoo"]) + .unwrap(); + + let mut state = TestState::default(); + let executor = module.executor(true, grounding_witness(&state, &[])); + let res = executor.action("SpawnFoo", vec![]).unwrap(); + let foo_tx = res.tx.clone(); + let [foo] = res.objs(); + apply_tx(&mut state, &foo_tx); + + let mut ctx = BuildContext { + builder: MultiPodBuilder::new(&Params::default(), &VDSet::new(&[])), + modules: vec![ + Arc::new(txlib::predicates::events_module()), + Arc::new(txlib::predicates::rekey_module()), + Arc::new(txlib::predicates::module()), + module.module().clone(), + ], + }; + let inputs = vec![foo.obj.clone()]; + let witness = grounding_witness(&state, &[foo.obj.commitment()]); + let mut tx = TxBuilder::new(&mut ctx, &inputs, witness); + + let receiver_key = Value::from(pod2utils::rand_raw_value()); + let scope = tx.begin_action(); + let (moved, st_rekey, h) = tx.rekey(&mut ctx, &foo.obj, receiver_key.clone()); + let n_action_branches = module + .classes() + .iter() + .find(|c| c.name == "Foo") + .unwrap() + .actions + .len(); + let mut premises = vec![Statement::None; n_action_branches + 1]; + premises[n_action_branches] = st_rekey; + let header_array = tx.state_header().array(); + let st_guard = ctx + .apply_custom_pred( + false, + "IsFoo", + map!({"state_header" => header_array}), + premises, + ) + .unwrap(); + tx.set_guard(h, st_guard); + tx.end_action(scope); + + let (st, tx_out, _) = tx.finalize(&mut ctx); + ctx.builder.reveal(&st).unwrap(); + txlib::test_support::solve_and_verify(ctx.builder); + + assert!(tx_out.live.contains(&Value::from(moved.clone())).unwrap()); + assert!( + !tx_out + .live + .contains(&Value::from(foo.obj.commitment())) + .unwrap() + ); + assert!( + tx_out + .nullifiers + .contains(&Value::from(txlib::compute_nullifier(&foo.obj))) + .unwrap() + ); + assert_eq!( + moved.get(&StrKey::from("key")).unwrap().unwrap(), + receiver_key + ); +}