Skip to content

Commit f08b579

Browse files
collect commitments and fetch statePaths concurrently
1 parent c370632 commit f08b579

File tree

1 file changed

+123
-81
lines changed

1 file changed

+123
-81
lines changed

wasm/src/programs/snapshot_query.rs

Lines changed: 123 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,146 +15,188 @@
1515
// along with the Provable SDK library. If not, see <https://www.gnu.org/licenses/>.
1616

1717
use crate::types::native::CurrentNetwork;
18-
use snarkvm_console::{network::Network, program::StatePath, types::Field};
19-
use snarkvm_ledger_query::QueryTrait;
20-
21-
use anyhow::anyhow;
22-
use async_trait::async_trait;
18+
use crate::types::native::{ProgramIDNative, IdentifierNative, RecordPlaintextNative};
19+
use anyhow::{anyhow, bail, Result};
20+
use futures::future::join_all;
2321
use indexmap::IndexMap;
22+
use wasm_bindgen::JsValue;
2423
use serde::{Deserialize, Serialize};
25-
use wasm_bindgen::prelude::wasm_bindgen;
26-
24+
use snarkvm_console::{
25+
network::Network,
26+
program::{Identifier, ProgramID, StatePath},
27+
types::Field,
28+
};
29+
use snarkvm_ledger_query::QueryTrait;
2730
use std::str::FromStr;
2831

2932
/// A snapshot-based query object used to pin the block height, state root,
3033
/// and state paths to a single ledger view during online execution.
31-
#[wasm_bindgen]
3234
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3335
pub struct SnapshotQuery {
3436
block_height: u32,
3537
state_paths: IndexMap<Field<CurrentNetwork>, StatePath<CurrentNetwork>>,
3638
state_root: <CurrentNetwork as Network>::StateRoot,
3739
}
3840

39-
#[wasm_bindgen]
4041
impl SnapshotQuery {
41-
#[wasm_bindgen(constructor)]
42-
pub fn new(block_height: u32, state_root: &str) -> Result<SnapshotQuery, String> {
42+
/// Construct an empty snapshot query with a chosen `(block_height, state_root)`
43+
pub fn new(block_height: u32, state_root: &str) -> anyhow::Result<Self> {
4344
let state_root = <CurrentNetwork as Network>::StateRoot::from_str(state_root)
44-
.map_err(|e| e.to_string())?;
45-
Ok(Self {
46-
block_height,
47-
state_paths: IndexMap::new(),
48-
state_root,
49-
})
45+
.map_err(|e| anyhow::anyhow!(e))?;
46+
Ok(Self { block_height, state_paths: IndexMap::new(), state_root })
5047
}
5148

52-
#[wasm_bindgen(js_name = "addBlockHeight")]
53-
pub fn add_block_height(&mut self, block_height: u32) {
49+
/// Add or update the fixed block height
50+
pub fn set_block_height(&mut self, block_height: u32) {
5451
self.block_height = block_height;
5552
}
5653

57-
#[wasm_bindgen(js_name = "addStatePath")]
58-
pub fn add_state_path(&mut self, commitment: &str, state_path: &str) -> Result<(), String> {
59-
let commitment = Field::from_str(commitment).map_err(|e| e.to_string())?;
60-
let state_path = StatePath::from_str(state_path).map_err(|e| e.to_string())?;
54+
/// Insert one `(commitment -> state_path)` pair (both as strings)
55+
pub fn add_state_path(&mut self, commitment: &str, state_path: &str) -> anyhow::Result<()> {
56+
let commitment = Field::from_str(commitment).map_err(|e| anyhow::anyhow!(e))?;
57+
let state_path = StatePath::from_str(state_path).map_err(|e| anyhow::anyhow!(e))?;
6158
self.state_paths.insert(commitment, state_path);
6259
Ok(())
6360
}
6461

65-
#[wasm_bindgen(js_name = "toString")]
66-
#[allow(clippy::inherent_to_string)]
67-
pub fn to_string(&self) -> String {
68-
serde_json::to_string(&self).unwrap()
69-
}
62+
/// Build a snapshot query directly from inputs.
63+
///
64+
/// Steps:
65+
/// 1) Parse JS inputs, detect record plaintexts (heuristic: contains "_nonce"),
66+
/// and compute their commitments via `to_commitment(program_id, record_name, view_key_field)`
67+
/// 2) Take a snapshot `(state_root, block_height)`.
68+
/// 3) Fetch all state paths **concurrently** for those commitments (anchored to that snapshot).
69+
/// 4) Return a populated `SnapshotQuery`.
70+
///
71+
/// Notes:
72+
/// - `record_name` must be the concrete name expected by the function (e.g. "credits").
73+
/// - `view_key_field` is the sender's record view key as a `Field<Network>`.
74+
pub async fn try_from_inputs(
75+
node_url: &str,
76+
program_id: &ProgramID<CurrentNetwork>,
77+
record_name: &Identifier<CurrentNetwork>,
78+
view_key_field: Field<CurrentNetwork>,
79+
js_inputs: &[JsValue],
80+
) -> Result<Self> {
81+
// 1) Extract commitments from inputs.
82+
let commitments = collect_commitments_from_inputs(program_id, record_name, view_key_field, js_inputs)?;
83+
84+
// Fast path: if there are no record inputs, still pin a snapshot (height + root) for consistency.
85+
let (snap_root, snap_height) = snapshot_head(node_url).await?;
86+
let mut query = SnapshotQuery::new(snap_height, &snap_root)?;
7087

71-
#[wasm_bindgen(js_name = "fromString")]
72-
pub fn from_string(s: &str) -> Result<SnapshotQuery, String> {
73-
serde_json::from_str(s).map_err(|e| e.to_string())
88+
89+
if commitments.is_empty() {
90+
return Ok(query);
91+
}
92+
93+
// 2) Fetch state paths concurrently (anchored to the chosen snapshot).
94+
// If your node cannot fetch-at-root, you can fetch plain paths, parse their embedded roots,
95+
// and ensure they're consistent; otherwise re-snapshot and retry.
96+
// Precompute owned strings to avoid borrowing temporaries into async futures.
97+
let cm_strings: Vec<String> = commitments.iter().map(|c| c.to_string()).collect();
98+
let root_str = snap_root.clone();
99+
let futs = cm_strings
100+
.iter()
101+
.map(|cm_s| fetch_state_path_at_root(node_url, cm_s.as_str(), root_str.as_str()));
102+
let results = join_all(futs).await;
103+
104+
// 3) Insert all paths; verify consistency against the pinned root.
105+
for (cm, res) in commitments.iter().zip(results.into_iter()) {
106+
let sp_str = res?;
107+
// Optional safety: parse and ensure the path's root matches the pinned root.
108+
let sp = StatePath::<CurrentNetwork>::from_str(&sp_str).map_err(|e| anyhow!(e.to_string()))?;
109+
let path_root = sp.global_state_root().to_string();
110+
if path_root != snap_root {
111+
// Strategy: bail and let caller retry; or resnapshot + refetch here.
112+
bail!("State path root mismatch: expected {}, got {}", snap_root, path_root);
113+
}
114+
query.add_state_path(&cm.to_string(), &sp_str)?;
115+
}
116+
117+
Ok(query)
74118
}
75119
}
76120

77-
#[async_trait(?Send)]
121+
#[async_trait::async_trait(?Send)]
78122
impl QueryTrait<CurrentNetwork> for SnapshotQuery {
79-
fn current_state_root(&self) -> anyhow::Result<<CurrentNetwork as Network>::StateRoot> {
123+
fn current_state_root(&self) -> Result<<CurrentNetwork as Network>::StateRoot> {
80124
Ok(self.state_root)
81125
}
82126

83-
async fn current_state_root_async(&self) -> anyhow::Result<<CurrentNetwork as Network>::StateRoot> {
127+
async fn current_state_root_async(&self) -> Result<<CurrentNetwork as Network>::StateRoot> {
84128
Ok(self.state_root)
85129
}
86130

87131
fn get_state_path_for_commitment(
88132
&self,
89133
commitment: &Field<CurrentNetwork>,
90-
) -> anyhow::Result<StatePath<CurrentNetwork>> {
134+
) -> Result<StatePath<CurrentNetwork>> {
91135
self.state_paths
92136
.get(commitment)
93137
.cloned()
94-
.ok_or(anyhow!("State path not found for commitment"))
138+
.ok_or_else(|| anyhow!("State path not found for commitment"))
95139
}
96140

97141
async fn get_state_path_for_commitment_async(
98142
&self,
99143
commitment: &Field<CurrentNetwork>,
100-
) -> anyhow::Result<StatePath<CurrentNetwork>> {
144+
) -> Result<StatePath<CurrentNetwork>> {
101145
self.state_paths
102146
.get(commitment)
103147
.cloned()
104-
.ok_or(anyhow!("State path not found for commitment"))
148+
.ok_or_else(|| anyhow!("State path not found for commitment"))
105149
}
106150

107-
fn current_block_height(&self) -> anyhow::Result<u32> {
151+
fn current_block_height(&self) -> Result<u32> {
108152
Ok(self.block_height)
109153
}
110154

111-
async fn current_block_height_async(&self) -> anyhow::Result<u32> {
155+
async fn current_block_height_async(&self) -> Result<u32> {
112156
Ok(self.block_height)
113157
}
114158
}
115159

116-
117-
mod snapshot_helpers {
118-
use super::*;
119-
type StateRootNative = <CurrentNetwork as snarkvm_console::network::Network>::StateRoot;
120-
121-
pub fn collect_commitments_from_trace<T>(
122-
_trace: &T,
123-
) -> Result<Vec<Field<CurrentNetwork>>, String> {
124-
Ok(vec![])
125-
}
126-
127-
pub async fn build_snapshot_query(
128-
node_url: &str,
129-
commitments: &[Field<CurrentNetwork>],
130-
) -> Result<SnapshotQuery, String> {
131-
let (state_root, block_height) = snapshot_head(node_url).await?;
132-
let mut query = SnapshotQuery::new(block_height, &state_root_to_string(state_root))?;
133-
134-
for c in commitments {
135-
let c_str = c.to_string();
136-
let sp_str = fetch_state_path_at_root(node_url, &c_str, &state_root_to_string(state_root)).await?;
137-
query.add_state_path(&c_str, &sp_str)?;
160+
/* --------------------------- internal helpers --------------------------- */
161+
162+
/// Heuristic: detect plaintext records from JS inputs and compute commitments.
163+
fn collect_commitments_from_inputs(
164+
program_id: &ProgramIDNative,
165+
record_name: &IdentifierNative,
166+
view_key_field: Field<CurrentNetwork>,
167+
js_inputs: &[JsValue],
168+
) -> anyhow::Result<Vec<Field<CurrentNetwork>>> {
169+
let mut out = Vec::new();
170+
171+
for js in js_inputs {
172+
if let Some(s) = js.as_string() {
173+
// Quick filter: record plaintexts include a `_nonce` field.
174+
if !s.contains("_nonce") {
175+
continue;
176+
}
177+
// Try parse as a plaintext record. Skip if not a record.
178+
if let Ok(rec) = RecordPlaintextNative::from_str(&s) {
179+
let cm = rec.to_commitment(program_id, record_name, &view_key_field);
180+
out.push(cm?);
181+
}
138182
}
139-
Ok(query)
140-
}
141-
142-
// Prefer an API that returns both in one call; otherwise fetch root then height immediately.
143-
async fn snapshot_head(node_url: &str) -> Result<(StateRootNative, u32), String> {
144-
// TODO: call existing client / REST: (root, height)
145-
Err("snapshot_head() not implemented".into())
146183
}
184+
Ok(out)
185+
}
147186

148-
async fn fetch_state_path_at_root(
149-
node_url: &str,
150-
commitment: &str,
151-
state_root: &str,
152-
) -> Result<String, String> {
153-
// TODO: call endpoint that returns a StatePath string for (commitment, state_root)
154-
Err("fetch_state_path_at_root() not implemented".into())
155-
}
187+
/// Return a single `(state_root_string, block_height)` snapshot.
188+
/// TODO: replace with real node client calls.
189+
async fn snapshot_head(_node_url: &str) -> Result<(String, u32)> {
190+
// Example (pseudo):
191+
// let head = client.latest_head(_node_url).await?;
192+
// Ok((head.state_root, head.block_height))
193+
Err(anyhow!("snapshot_head() not implemented"))
194+
}
156195

157-
fn state_root_to_string(root: StateRootNative) -> String {
158-
root.to_string()
159-
}
160-
}
196+
/// Fetch a `StatePath` (as string) for `commitment` anchored to `state_root`
197+
/// TODO: replace with real node client call and ensure it returns a path at the requested root
198+
async fn fetch_state_path_at_root(_node_url: &str, _commitment: &str, _state_root: &str) -> anyhow::Result<String> {
199+
// Example (pseudo):
200+
// client.state_path_at_root(_node_url, _commitment, _state_root).await
201+
Err(anyhow!("fetch_state_path_at_root() not implemented"))
202+
}

0 commit comments

Comments
 (0)