Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/craft-basics/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion examples/craft-rocket/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────

Expand Down
6 changes: 6 additions & 0 deletions libs/sdk/src/fmt_podlang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
23 changes: 16 additions & 7 deletions libs/sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Module>,
// Frozen Rekey batch imported by every generated class guard.
rekey_mod: Arc<Module>,
txlib_mod: Arc<Module>,
dependencies: Vec<Dependency>,
actions: Vec<ActionHandle>,
Expand Down Expand Up @@ -2261,12 +2260,17 @@ impl Loader {

fn new(actions: Vec<ActionHandle>) -> Result<Self> {
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,
Expand All @@ -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,
Expand Down Expand Up @@ -2325,7 +2330,7 @@ impl Loader {
podlang_src.as_str(),
"root",
&params,
slice::from_ref(&self.tx_events_mod),
&[self.tx_events_mod.clone(), self.rekey_mod.clone()],
)
.expect("compiles"),
);
Expand All @@ -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,
Expand All @@ -2358,6 +2364,7 @@ impl Loader {
/// An SdkModule contains a loaded module and allows executing actions.
pub struct SdkModule {
tx_events_mod: Arc<Module>,
rekey_mod: Arc<Module>,
txlib_mod: Arc<Module>,
podlang_src: String,
actions: Vec<ActionMeta>,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
];
Expand Down
90 changes: 88 additions & 2 deletions libs/sdk/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ fn test_sdk_2() {
[plugin]
name = "test"
version = "0.1.0"
module_hash = "75119e01ece11fd33d43bb2f68239c92450d338140f6757dd286abbb628b5712"
module_hash = "ef2deed1853640114f4943592279ddc94ca728cadb86aa5709992465e22af4cc"

[[classes]]
name = "Log"
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
);
}