Skip to content

Commit 6440df1

Browse files
authored
Merge pull request #3238 from ProvableHQ/mohammadfawaz/query_functions
feat: read-only `query` functions
2 parents f684e36 + cb89bb6 commit 6440df1

35 files changed

Lines changed: 3036 additions & 41 deletions

File tree

.circleci/config.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,10 @@ jobs:
905905
steps:
906906
- run_test:
907907
workspace_member: snarkvm-synthesizer
908+
# `history` enables the per-key historical update map that the `query` evaluation path
909+
# depends on; gating it here ensures the v15 query tests are exercised in CI.
908910
flags: >
909-
--lib --bins --features test
911+
--lib --bins --features test,history
910912
--partition count:1/2
911913
-- --test-threads 8
912914
cache_key_suffix: -test1
@@ -919,7 +921,7 @@ jobs:
919921
- run_test:
920922
workspace_member: snarkvm-synthesizer
921923
flags: >
922-
--lib --bins --features test
924+
--lib --bins --features test,history
923925
--partition count:2/2
924926
-- --test-threads 8
925927
cache_key_suffix: -test2
@@ -933,7 +935,7 @@ jobs:
933935
timeout: 30m # test_vm_execute_and_finalize can take over 10 minutes in the current setup
934936
no_output_timeout: 20m
935937
workspace_member: snarkvm-synthesizer
936-
flags: --test '*' --features test,dev-print -- --test-threads=4
938+
flags: --test '*' --features test,dev-print,history -- --test-threads=4
937939
cache_key_suffix: -integration
938940

939941
synthesizer-process:
@@ -942,6 +944,8 @@ jobs:
942944
steps:
943945
- run_test:
944946
workspace_member: snarkvm-synthesizer-process
947+
# See `synthesizer-test-partition1` for why `history` is enabled.
948+
flags: --features history
945949

946950
synthesizer-process-with-rocksdb:
947951
executor: rust-docker

console/network/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ pub trait Network:
226226
const MAX_RECORDS: usize = 10 * Self::MAX_FUNCTIONS;
227227
/// The maximum number of closures in a program.
228228
const MAX_CLOSURES: usize = 2 * Self::MAX_FUNCTIONS;
229+
/// The maximum number of query functions in a program.
230+
const MAX_QUERIES: usize = 2 * Self::MAX_FUNCTIONS;
229231
/// The maximum number of operands in an instruction.
230232
const MAX_OPERANDS: usize = Self::MAX_INPUTS;
231233
/// The maximum number of instructions in a closure or function.

synthesizer/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ locktick = [
3333
]
3434
async = [ "snarkvm-ledger-query/async", "snarkvm-synthesizer-process/async" ]
3535
cuda = [ "snarkvm-algorithms/cuda" ]
36-
history = [ "snarkvm-ledger-store/history" ]
36+
history = [ "snarkvm-ledger-store/history", "snarkvm-synthesizer-process/history" ]
3737
history-staking-rewards = [ "snarkvm-ledger-store/history-staking-rewards" ]
3838
slipstream-plugins = [ "snarkvm-ledger-store/slipstream-plugins" ]
3939
rocks = [ "snarkvm-ledger-store/rocks" ]

synthesizer/process/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ wasm = [
4444
"snarkvm-synthesizer-snark/wasm"
4545
]
4646
test = [ "snarkvm-console/test", "snarkvm-circuit/test" ]
47+
history = [ "snarkvm-ledger-store/history" ]
4748
timer = [ "aleo-std/timer" ]
4849
dev-print = [ "snarkvm-utilities/dev-print" ]
4950
dev_skip_checks = [ ]

synthesizer/process/src/cost.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,20 @@ pub fn deployment_cost_v2<N: Network>(
259259
);
260260
}
261261

262+
// Bound each query function's worst-case compute. Queries are off-consensus and have no
263+
// dedicated fee component beyond what is already counted in `storage_cost` (their bytes
264+
// contribute to `size_in_bytes`). The bound below is purely a deploy-time sanity check
265+
// to keep pathological queries from being accepted.
266+
for query in deployment.program().queries().values() {
267+
let query_cost = query_cost_for_single_query(&stack, query.name(), ConsensusFeeVersion::V3)?;
268+
ensure!(
269+
query_cost <= N::TRANSACTION_SPEND_LIMIT[1].1,
270+
"Query '{}' has a cost '{query_cost}' which exceeds the transaction spend limit '{}'",
271+
query.name(),
272+
N::TRANSACTION_SPEND_LIMIT[1].1
273+
);
274+
}
275+
262276
// Compute the namespace cost in microcredits: 10^(10 - num_characters) * 1e6
263277
let namespace_cost = 10u64
264278
.checked_pow(10u32.saturating_sub(num_characters))
@@ -1000,6 +1014,30 @@ fn finalize_cost_for_single_function_raw<N: Network>(
10001014
Ok(finalize_cost)
10011015
}
10021016

1017+
/// Returns the maximum compute cost (in microcredits) of a single query function's body.
1018+
///
1019+
/// Queries do not run as part of consensus, so this cost is not paid by anyone — it is only
1020+
/// used as a deploy-time sanity bound (mirrors the per-function `TRANSACTION_SPEND_LIMIT`
1021+
/// check) to prevent deploying queries whose worst-case compute is unreasonable.
1022+
fn query_cost_for_single_query<N: Network>(
1023+
stack: &Stack<N>,
1024+
query_name: &Identifier<N>,
1025+
consensus_fee_version: ConsensusFeeVersion,
1026+
) -> Result<u64> {
1027+
let query = stack.program().get_query_ref(query_name)?;
1028+
1029+
// Query types are not cached on the stack today; recompute them here for the cost walk.
1030+
let query_types = FinalizeTypes::from_query(stack, query)?;
1031+
1032+
let mut query_cost = 0u64;
1033+
for command in query.commands() {
1034+
query_cost = query_cost
1035+
.checked_add(cost_per_command(stack, &query_types, command, consensus_fee_version)?)
1036+
.ok_or(anyhow!("Query cost overflowed"))?;
1037+
}
1038+
Ok(query_cost)
1039+
}
1040+
10031041
/// Returns the total finalize cost for an execution by iterating over all concrete transitions.
10041042
/// This gives an exact cost calculation because we know which functions were actually called.
10051043
/// The complexity is O(MAX_TRANSITIONS * MAX_COMMANDS_PER_FINALIZE) which is bounded.

synthesizer/process/src/finalize.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
use super::*;
1717
use console::program::{FinalizeType, Future, Register};
18-
use snarkvm_synthesizer_program::{Await, FinalizeRegistersState, Operand, RegistersTrait};
18+
use snarkvm_synthesizer_program::{Await, FinalizeRegistersState, FinalizeStoreTrait, Operand, RegistersTrait};
1919
use snarkvm_utilities::try_vm_runtime;
2020

2121
use std::collections::HashSet;
@@ -332,7 +332,8 @@ fn finalize_constructor<N: Network, P: FinalizeStorage<N>>(
332332
let constructor_types = stack.get_constructor_types()?.clone();
333333

334334
// Initialize the finalize registers.
335-
let mut registers = FinalizeRegisters::new(state, transition_id, *program_id.name(), constructor_types, nonce);
335+
let mut registers =
336+
FinalizeRegisters::new(state, Some(transition_id), *program_id.name(), constructor_types, Some(nonce));
336337

337338
// Determine the scope name.
338339
let scope_name = Identifier::<N>::from_str("constructor")?;
@@ -447,8 +448,13 @@ fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
447448
// Otherwise, query the call graph for the child transition ID corresponding to the future that is being awaited.
448449
let consensus_version = N::CONSENSUS_VERSION(state.block_height())?;
449450
let transition_id = if (ConsensusVersion::V1..=ConsensusVersion::V2).contains(&consensus_version) {
450-
// Get the current transition ID.
451-
let transition_id = registers.transition_id();
451+
// Get the current transition ID. The finalize path always initializes
452+
// registers with `Some(transition_id)`; only the query path uses `None`,
453+
// and `await` is forbidden on the query path, so this is unreachable
454+
// there. Treat `None` as a logic error.
455+
let transition_id = registers
456+
.transition_id()
457+
.ok_or_else(|| anyhow!("Cannot resolve a child transition ID without a transition ID"))?;
452458
// Get the child transition ID.
453459
match call_graph.get(transition_id) {
454460
Some(transitions) => match transitions.get(call_counter) {
@@ -570,10 +576,10 @@ fn initialize_finalize_state<N: Network>(
570576
// Initialize the registers.
571577
let mut registers = FinalizeRegisters::new(
572578
state,
573-
transition_id,
579+
Some(transition_id),
574580
*future.function_name(),
575581
stack.get_finalize_types(future.function_name())?.clone(),
576-
nonce,
582+
Some(nonce),
577583
);
578584

579585
// Store the inputs. The argument count is guaranteed to match the finalize's declared inputs
@@ -589,9 +595,12 @@ fn initialize_finalize_state<N: Network>(
589595
}
590596

591597
// A helper function to finalize all commands except `await`, updating the finalize operations and the counter.
598+
//
599+
// Generic over the store so the query evaluator (which passes either the canonical
600+
// `FinalizeStore` or a read-only historic adapter) can reuse this dispatch.
592601
#[inline]
593-
fn finalize_command_except_await<N: Network>(
594-
store: &FinalizeStore<N, impl FinalizeStorage<N>>,
602+
pub(crate) fn finalize_command_except_await<N: Network>(
603+
store: &impl FinalizeStoreTrait<N>,
595604
stack: &impl StackTrait<N>,
596605
registers: &mut FinalizeRegisters<N>,
597606
positions: &HashMap<Identifier<N>, usize>,

synthesizer/process/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ mod deploy;
3636
mod evaluate;
3737
mod execute;
3838
mod finalize;
39+
#[cfg(feature = "history")]
40+
mod query;
41+
#[cfg(feature = "history")]
42+
pub use query::evaluate_query_at_height;
3943
mod verify_deployment;
4044
mod verify_execution;
4145
mod verify_fee;

0 commit comments

Comments
 (0)