From 7c15248588a4d0319d769e9fc66845d9efb9daa4 Mon Sep 17 00:00:00 2001 From: Rob Knight Date: Fri, 2 Oct 2026 13:25:23 +0000 Subject: [PATCH] Upgrade pod2 for sparse array literals --- Cargo.lock | 2 +- Cargo.toml | 2 +- libs/sdk/README.md | 4 +- libs/sdk/src/fmt_podlang.rs | 17 +++--- libs/sdk/src/lib.rs | 28 ++++----- libs/sdk/src/tests.rs | 25 +++++--- libs/txlib/src/lib.rs | 56 ++++++++++++++---- libs/txlib/src/replay.rs | 111 ++++++++++++++++++++++++++++-------- 8 files changed, 174 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34c6c246..7c09e308 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6096,7 +6096,7 @@ dependencies = [ [[package]] name = "pod2" version = "0.1.0" -source = "git+https://github.com/0xPARC/pod2?rev=1ee11948bcb83f53b689f0a9a14a68b943120c2e#1ee11948bcb83f53b689f0a9a14a68b943120c2e" +source = "git+https://github.com/0xPARC/pod2?rev=a91d87895de0f6d9bda256016572d593216e619a#a91d87895de0f6d9bda256016572d593216e619a" dependencies = [ "annotate-snippets", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 8a823d7b..7d4911e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ resolver = "2" [workspace.dependencies] -pod2 = { git = "https://github.com/0xPARC/pod2", rev = "1ee11948bcb83f53b689f0a9a14a68b943120c2e", default-features = false, features = [ +pod2 = { git = "https://github.com/0xPARC/pod2", rev = "a91d87895de0f6d9bda256016572d593216e619a", default-features = false, features = [ "backend_plonky2", "disk_cache", "zk", diff --git a/libs/sdk/README.md b/libs/sdk/README.md index 479f4a6c..dde88264 100644 --- a/libs/sdk/README.md +++ b/libs/sdk/README.md @@ -169,9 +169,7 @@ representation in this order: set, dictionary, array. Because statement arguments are compared and hashed by raw value, this choice does not affect verification. -The pinned pod2 version requires contiguous indexes in Podlang array literals. -Sparse arrays are rendered as commitments with a warning. Support for rendering -them as literals can follow a dependency update that includes +Sparse arrays are rendered with the indexed Podlang literal syntax added in [pod2 #541](https://github.com/0xPARC/pod2/pull/541). ## Type checking diff --git a/libs/sdk/src/fmt_podlang.rs b/libs/sdk/src/fmt_podlang.rs index 130cda8f..fcdb4638 100644 --- a/libs/sdk/src/fmt_podlang.rs +++ b/libs/sdk/src/fmt_podlang.rs @@ -192,9 +192,8 @@ struct ArgFmt<'a> { /// prefer set, then dictionary, then array. Each representation has the same /// raw value, which is what statement arguments use. /// -/// The pinned pod2 version cannot represent sparse arrays in Podlang, so they -/// are rendered as their commitment. Once the dependency includes -/// , render them with sparse-array syntax. +/// Sparse arrays use the indexed literal syntax added in +/// . struct LiteralFmt<'a>(&'a Value); #[cfg(test)] @@ -240,7 +239,6 @@ impl<'a> fmt::Display for LiteralFmt<'a> { .collect::, _>>() .map_err(|_| fmt::Error)?; slots.sort_by_key(|(index, _)| *index); - // Podlang array literals cannot represent gaps between indexes. if slots.iter().enumerate().all(|(i, (index, _))| i == *index) { write!(f, "[")?; for (i, (_, element)) in slots.iter().enumerate() { @@ -249,11 +247,12 @@ impl<'a> fmt::Display for LiteralFmt<'a> { } return write!(f, "]"); } else { - log::warn!( - "Rendering sparse array as its commitment: the pinned pod2 version \ - does not support sparse-array literals" - ); - return write!(f, "{}", Value::from(value.raw())); + write!(f, "[")?; + for (i, (index, element)) in slots.iter().enumerate() { + let sep = if i == 0 { "" } else { ", " }; + write!(f, "{sep}{index}: {}", LiteralFmt(element))?; + } + return write!(f, "]"); } } write!(f, "{}", Value::from(value.raw())) diff --git a/libs/sdk/src/lib.rs b/libs/sdk/src/lib.rs index e3740bf9..924368ca 100644 --- a/libs/sdk/src/lib.rs +++ b/libs/sdk/src/lib.rs @@ -11,7 +11,7 @@ use itertools::zip_eq; use lt_eq_u256_pod::{LtEqU256Pod, STANDARD_LT_EQ_U256_VD_HASH}; use pod2::{ backends::plonky2::{basetypes::DEFAULT_VD_SET, mainpod::Prover, mock::mainpod::MockProver}, - frontend::{MainPod, MultiPodBuilder, Operation, OperationArg}, + frontend::{MainPod, MultiPodBuilder, Operation, OperationArg, entry}, lang::{Module, load_module}, middleware::{ EMPTY_VALUE, F, Hash, MainPodProver, NativePredicate, OperationAux, OperationType, Params, @@ -968,23 +968,25 @@ impl ActionHandle { // Resolve an Output's pre-identity dict to its slot in the // `Initials` record. let initials_anchor = |obj_name: &str| -> Option { - let (slot, entry) = meta.initials_entry(obj_name)?; - if entry.needs_wildcard { + let (slot, shape) = meta.initials_entry(obj_name)?; + if shape.needs_wildcard { return None; } - Some((initials_array.as_ref()?, slot as i64).into()) + Some(entry(initials_array.as_ref()?, slot as i64).expect("initials slot exists")) }; // Returns the anchored op-arg for the Object's collapsed side at // this ts, or None when the form stays an explicit wildcard. let anchor_at = |obj_name: &str, ts: usize| -> Option { match meta.collapsed_at(obj_name, ts)? { - fmt_podlang::Collapse::IO(fmt_podlang::Side::In) => { - Some((&io_array, meta.in_entry(obj_name).unwrap().0 as i64).into()) - } - fmt_podlang::Collapse::IO(fmt_podlang::Side::Out) => { - Some((&io_array, meta.out_entry(obj_name).unwrap().0 as i64).into()) - } + fmt_podlang::Collapse::IO(fmt_podlang::Side::In) => Some( + entry(&io_array, meta.in_entry(obj_name).unwrap().0 as i64) + .expect("input slot exists"), + ), + fmt_podlang::Collapse::IO(fmt_podlang::Side::Out) => Some( + entry(&io_array, meta.out_entry(obj_name).unwrap().0 as i64) + .expect("output slot exists"), + ), fmt_podlang::Collapse::Initials => { Some(initials_anchor(obj_name).expect("collapsed_at promised an initials slot")) } @@ -1229,7 +1231,7 @@ impl ActionHandle { // for actions with no intermediates. let chain_step_anchor = |ts: usize| -> Option { let slot = fmt_podlang::chain_step_at(ts, chain_max_ts)?; - Some((chain_steps_array.as_ref()?, slot as i64).into()) + Some(entry(chain_steps_array.as_ref()?, slot as i64).expect("chain step slot exists")) }; // Each pending Tx event is first proved with literal args. We then use @@ -1247,14 +1249,14 @@ impl ActionHandle { ObjectIO::Input | ObjectIO::Mutate => meta .in_entry(varname) .filter(|(_, e)| !e.needs_wildcard) - .map(|(idx, _)| (&io_array, idx as i64).into()), + .map(|(idx, _)| entry(&io_array, idx as i64).expect("input slot exists")), 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()), + .map(|(idx, _)| entry(&io_array, idx as i64).expect("output slot exists")), ObjectIO::Input => None, }; let pre_ts = pending.post_ts - 1; diff --git a/libs/sdk/src/tests.rs b/libs/sdk/src/tests.rs index 37234ebb..14a86d3e 100644 --- a/libs/sdk/src/tests.rs +++ b/libs/sdk/src/tests.rs @@ -1517,11 +1517,10 @@ fn test_literal_container_rejects_var() { assert!(err.contains("is a var, not a literal"), "{err}"); } -/// Renders sparse arrays as commitments because the pinned pod2 version has no -/// sparse-array literal syntax. Update this expectation once the dependency -/// includes . +/// Renders sparse arrays with the indexed syntax added in +/// . #[test] -fn test_sparse_array_literal_renders_as_commitment() { +fn test_sparse_array_literal_renders_with_indexes() { let dense = Array::new(vec![Value::from(1), Value::from(2)]); assert_eq!( fmt_podlang::literal_podlang(&Value::from(dense)), @@ -1530,11 +1529,21 @@ fn test_sparse_array_literal_renders_as_commitment() { let mut sparse = Array::empty_with_db(Box::new(pod2::middleware::db::mem::MemDB::new())); sparse.insert(5, Value::from(1)).unwrap(); + sparse.insert(7, Value::from(2)).unwrap(); let sparse = Value::from(sparse); - assert_eq!( - fmt_podlang::literal_podlang(&sparse), - Value::from(sparse.raw()).to_string() - ); + let literal = fmt_podlang::literal_podlang(&sparse); + assert_eq!(literal, "[5: 1, 7: 2]"); + + // The rendered source must lower back to the same container, rather than + // merely pass the Podlang parser. + let source = format!("my_pred(A) = AND(Equal(A, {literal}))"); + let module = pod2::lang::load_module(&source, "sparse_literal", &Params::default(), &[]) + .expect("indexed array literal lowers"); + let arg = &module.batch.predicates()[0].statements()[0].args()[1]; + let pod2::middleware::StatementTmplArg::Literal(lowered) = arg else { + panic!("expected a literal, got {arg:?}"); + }; + assert_eq!(lowered.raw(), sparse.raw()); } /// Reports an execution error when a variable-key lookup misses. diff --git a/libs/txlib/src/lib.rs b/libs/txlib/src/lib.rs index eafb3e07..0033bce2 100644 --- a/libs/txlib/src/lib.rs +++ b/libs/txlib/src/lib.rs @@ -32,7 +32,7 @@ use std::{collections::HashMap, sync::Arc}; use pod2::{ backends::plonky2::primitives::merkletree::MerkleProof, - frontend::Operation, + frontend::{Operation, entry}, middleware::{ EMPTY_VALUE, Hash, NativeOperation, OperationAux, OperationType, Statement, StrKey, Value, containers::{Array, Dictionary, Set}, @@ -788,8 +788,8 @@ impl TxBuilder { let st_eq_stable_identifier = ctx .builder .priv_op(op!(Equal( - (old, STABLE_IDENTIFIER_FIELD), - (new, STABLE_IDENTIFIER_FIELD) + entry(old, STABLE_IDENTIFIER_FIELD).expect("referenced container entry exists"), + entry(new, STABLE_IDENTIFIER_FIELD).expect("referenced container entry exists") ))) .unwrap(); let st_h1 = ctx @@ -910,7 +910,10 @@ impl TxBuilder { let st_inputs_rebound = ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![Some((&before_tx, "live")), None], + vec![ + Some(entry(&before_tx, "live").expect("referenced container entry exists")), + None, + ], self.st_inputs_grounded.clone(), )) .unwrap(); @@ -920,7 +923,10 @@ impl TxBuilder { .priv_op(Operation::replace_value_with_entry( vec![ None, - Some((&state_header_arr, STATE_HEADER_CREATED_SLOT as i64)), + Some( + entry(&state_header_arr, STATE_HEADER_CREATED_SLOT as i64) + .expect("referenced container entry exists"), + ), ], st_inputs_rebound, )) @@ -932,7 +938,11 @@ impl TxBuilder { let st_hash_rebound = ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![Some((&before_tx, "live")), None, None], + vec![ + Some(entry(&before_tx, "live").expect("referenced container entry exists")), + None, + None, + ], st_hash, )) .unwrap(); @@ -958,7 +968,12 @@ impl TxBuilder { let st_dict_insert = ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![None, None, Some((&before_tx, "live")), None], + vec![ + None, + None, + Some(entry(&before_tx, "live").expect("referenced container entry exists")), + None, + ], st_dict_insert_lit, )) .unwrap(); @@ -1421,11 +1436,18 @@ mod tests { let (st_mutate, h_sub) = tx2.mutate(&mut ctx, &pick_new, &pick); let op_gt = ctx .builder - .priv_op(op!(Gt((&pick, "durability"), 0_i64))) + .priv_op(op!(Gt( + entry(&pick, "durability").expect("referenced container entry exists"), + 0_i64 + ))) .unwrap(); let op_sum = ctx .builder - .priv_op(op!(Sum(99_i64, 1_i64, (&pick, "durability")))) + .priv_op(op!(Sum( + 99_i64, + 1_i64, + entry(&pick, "durability").expect("referenced container entry exists") + ))) .unwrap(); let op_du = ctx .builder @@ -1650,14 +1672,26 @@ mod tests { let st_ins_a_anchored = ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![None, None, Some((&initials, "stick_a")), None, None], + vec![ + None, + None, + Some(entry(&initials, "stick_a").expect("referenced container entry exists")), + None, + None, + ], st_ins_a, )) .unwrap(); let st_ins_b_anchored = ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![None, None, Some((&initials, "stick_b")), None, None], + vec![ + None, + None, + Some(entry(&initials, "stick_b").expect("referenced container entry exists")), + None, + None, + ], st_ins_b, )) .unwrap(); diff --git a/libs/txlib/src/replay.rs b/libs/txlib/src/replay.rs index 35fa25bf..962865c9 100644 --- a/libs/txlib/src/replay.rs +++ b/libs/txlib/src/replay.rs @@ -14,7 +14,7 @@ //! method here is a private helper it delegates to. use pod2::{ - frontend::Operation, + frontend::{Operation, entry}, middleware::{ Hash, Statement, Value, containers::{Dictionary, Set}, @@ -563,7 +563,13 @@ impl<'a> Replayer<'a> { .ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![None, None, Some((&pair, "initial")), None, None], + vec![ + None, + None, + Some(entry(&pair, "initial").expect("referenced container entry exists")), + None, + None, + ], tx_stmt, )) .unwrap(); @@ -571,12 +577,21 @@ impl<'a> Replayer<'a> { let op_si = self .ctx .builder - .priv_op(op!(SetInsert((&btx, "live"), new, (&pair, "new_live")))) + .priv_op(op!(SetInsert( + entry(&btx, "live").expect("referenced container entry exists"), + new, + entry(&pair, "new_live").expect("referenced container entry exists") + ))) .unwrap(); let op_du = self .ctx .builder - .priv_op(op!(DictUpdate(btx, "live", (&pair, "new_live"), atx))) + .priv_op(op!(DictUpdate( + btx, + "live", + entry(&pair, "new_live").expect("referenced container entry exists"), + atx + ))) .unwrap(); let rebound_evidence = self .ctx @@ -585,8 +600,8 @@ impl<'a> Replayer<'a> { vec![ None, None, - Some((&btx, "chain_start")), - Some((&btx, "chain_end")), + Some(entry(&btx, "chain_start").expect("referenced container entry exists")), + Some(entry(&btx, "chain_end").expect("referenced container entry exists")), ], guard_evidence, )) @@ -621,8 +636,14 @@ impl<'a> Replayer<'a> { vec![ None, None, - Some((&scratch.btx, "chain_start")), - Some((&scratch.btx, "chain_end")), + Some( + entry(&scratch.btx, "chain_start") + .expect("referenced container entry exists"), + ), + Some( + entry(&scratch.btx, "chain_end") + .expect("referenced container entry exists"), + ), ], guard_evidence, )) @@ -662,7 +683,11 @@ impl<'a> Replayer<'a> { let op_h1 = self .ctx .builder - .priv_op(op!(Hash(old, (old, "key"), okh))) + .priv_op(op!(Hash( + old, + entry(old, "key").expect("referenced container entry exists"), + okh + ))) .unwrap(); let op_h2 = self .ctx @@ -672,7 +697,11 @@ impl<'a> Replayer<'a> { let op_si = self .ctx .builder - .priv_op(op!(SetInsert((mid_tx, "nullifiers"), nul, new_nullifiers))) + .priv_op(op!(SetInsert( + entry(mid_tx, "nullifiers").expect("referenced container entry exists"), + nul, + new_nullifiers + ))) .unwrap(); let op_du_null = self .ctx @@ -722,7 +751,11 @@ impl<'a> Replayer<'a> { let op_sd = self .ctx .builder - .priv_op(op!(SetDelete((btx, "live"), old, live_minus_old))) + .priv_op(op!(SetDelete( + entry(btx, "live").expect("referenced container entry exists"), + old, + live_minus_old + ))) .unwrap(); let op_si = self .ctx @@ -773,7 +806,11 @@ impl<'a> Replayer<'a> { .ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![Some((&pair, "mid_tx")), None, None], + vec![ + Some(entry(&pair, "mid_tx").expect("referenced container entry exists")), + None, + None, + ], st_nullify.clone(), )) .unwrap(); @@ -781,7 +818,11 @@ impl<'a> Replayer<'a> { let op_sd = self .ctx .builder - .priv_op(op!(SetDelete((&btx, "live"), old, (&pair, "new_live")))) + .priv_op(op!(SetDelete( + entry(&btx, "live").expect("referenced container entry exists"), + old, + entry(&pair, "new_live").expect("referenced container entry exists") + ))) .unwrap(); let op_du_live = self .ctx @@ -789,8 +830,8 @@ impl<'a> Replayer<'a> { .priv_op(op!(DictUpdate( btx, "live", - (&pair, "new_live"), - (&pair, "mid_tx") + entry(&pair, "new_live").expect("referenced container entry exists"), + entry(&pair, "mid_tx").expect("referenced container entry exists") ))) .unwrap(); let rebound_evidence = self @@ -800,8 +841,8 @@ impl<'a> Replayer<'a> { vec![ None, None, - Some((&btx, "chain_start")), - Some((&btx, "chain_end")), + Some(entry(&btx, "chain_start").expect("referenced container entry exists")), + Some(entry(&btx, "chain_end").expect("referenced container entry exists")), ], guard_evidence, )) @@ -861,14 +902,14 @@ impl<'a> Replayer<'a> { btx, "chain_start", chain, - (&pair, "scope_mid") + entry(&pair, "scope_mid").expect("referenced container entry exists") ))) .unwrap(); let op_scope2 = self .ctx .builder .priv_op(op!(DictUpdate( - (&pair, "scope_mid"), + entry(&pair, "scope_mid").expect("referenced container entry exists"), "chain_end", chain_after, itx @@ -877,15 +918,20 @@ impl<'a> Replayer<'a> { let op_du1 = self .ctx .builder - .priv_op(op!(DictUpdate(btx, "live", (&etx, "live"), (&pair, "mid")))) + .priv_op(op!(DictUpdate( + btx, + "live", + entry(&etx, "live").expect("referenced container entry exists"), + entry(&pair, "mid").expect("referenced container entry exists") + ))) .unwrap(); let op_du2 = self .ctx .builder .priv_op(op!(DictUpdate( - (&pair, "mid"), + entry(&pair, "mid").expect("referenced container entry exists"), "nullifiers", - (&etx, "nullifiers"), + entry(&etx, "nullifiers").expect("referenced container entry exists"), atx ))) .unwrap(); @@ -951,7 +997,13 @@ impl<'a> Replayer<'a> { .ctx .builder .priv_op(Operation::replace_value_with_entry( - vec![None, None, Some((&pair, "initial")), None, None], + vec![ + None, + None, + Some(entry(&pair, "initial").expect("referenced container entry exists")), + None, + None, + ], tx_stmt.clone(), )) .unwrap(); @@ -959,12 +1011,21 @@ impl<'a> Replayer<'a> { let op_si = self .ctx .builder - .priv_op(op!(SetInsert((&btx, "live"), new, (&pair, "new_live")))) + .priv_op(op!(SetInsert( + entry(&btx, "live").expect("referenced container entry exists"), + new, + entry(&pair, "new_live").expect("referenced container entry exists") + ))) .unwrap(); let op_du = self .ctx .builder - .priv_op(op!(DictUpdate(btx, "live", (&pair, "new_live"), atx))) + .priv_op(op!(DictUpdate( + btx, + "live", + entry(&pair, "new_live").expect("referenced container entry exists"), + atx + ))) .unwrap(); let st = self .ctx