Skip to content

Commit 7bd86e5

Browse files
Allow block.timestamp, self.program_owner, and Snark::verify in views (#29579)
These read-only ops resolve in snarkVM's view (finalize-register) path, but the type checker rejected them by classifying them as FinalizeWrite. Reclassify them as FinalizeRead so views accept them, matching snarkVM. Co-authored-by: IGI-111 <igi-111@protonmail.com>
1 parent ba2c017 commit 7bd86e5

12 files changed

Lines changed: 192 additions & 36 deletions

crates/compiler/src/run.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,14 @@ fn handle_view(
416416
};
417417
// For an empty in-memory consensus store there is no block 0, so route directly through
418418
// `evaluate_view_with_stack_at_height` with a fabricated `FinalizeGlobalState`. Tests are off-consensus
419-
// by construction, so the values here only matter for queries that read `block.height` / `network.id`.
420-
// `VM::evaluate_view_at_height` cannot be used here: it resolves the program edition from on-chain
421-
// deployments, which an empty store does not have.
419+
// by construction, so the values here only matter for queries that read `block.height` / `block.timestamp`
420+
// / `network.id`; the timestamp is fixed so those reads are deterministic. `VM::evaluate_view_at_height`
421+
// cannot be used here: it resolves the program edition from on-chain deployments, which an empty store
422+
// does not have.
422423
let state = match snarkvm::synthesizer::program::FinalizeGlobalState::new::<CurrentNetwork>(
423424
0,
424425
0,
425-
None,
426+
Some(1234567890i64),
426427
0,
427428
0,
428429
Default::default(),
@@ -431,11 +432,23 @@ fn handle_view(
431432
Ok(s) => s,
432433
Err(e) => return failed(format!("Failed to build FinalizeGlobalState: {e}")),
433434
};
434-
// Evaluate against the live (latest) stack; off-consensus tests have a single edition.
435-
let stack = match vm.process().get_stack(program_id) {
435+
// Off-consensus tests deploy nothing, so the registered stack carries no program owner. Rebuild the
436+
// stack (the only way to a mutable handle — `get_stack` hands out a shared `Arc`) and set the owner to
437+
// the test key's address, mirroring a real deployment, so views can read `self.program_owner`.
438+
let registered = match vm.process().get_stack(program_id) {
436439
Ok(stack) => stack,
437440
Err(e) => return failed(format!("Failed to load stack for `{program_id}`: {e}")),
438441
};
442+
let mut stack = match snarkvm::synthesizer::process::Stack::new(vm.process(), registered.program()) {
443+
Ok(stack) => stack,
444+
Err(e) => return failed(format!("Failed to build stack for `{program_id}`: {e}")),
445+
};
446+
match PrivateKey::<CurrentNetwork>::from_str(leo_ast::TEST_PRIVATE_KEY)
447+
.and_then(|pk| Address::<CurrentNetwork>::try_from(&pk))
448+
{
449+
Ok(owner) => stack.set_program_owner(Some(owner)),
450+
Err(e) => return failed(format!("Failed to derive program owner: {e}")),
451+
};
439452
let response = match catch_unwind(AssertUnwindSafe(|| {
440453
snarkvm::synthesizer::process::evaluate_view_with_stack_at_height(
441454
state,

crates/passes/src/type_checking/visitor.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ use std::ops::Deref;
4545
#[derive(Copy, Clone, Debug)]
4646
pub enum AccessScope {
4747
/// Read-only finalize op: permitted inside `final fn` / `final {}` blocks and inside
48-
/// `view fn` bodies. Examples: `Mapping::get`, `Vector::len`, `block.height`.
48+
/// `view fn` bodies. Examples: `Mapping::get`, `Vector::len`, `block.height`,
49+
/// `block.timestamp`, `Snark::verify`, `self.program_owner`.
4950
FinalizeRead,
5051
/// Mutating finalize op: permitted inside `final fn` / `final {}` blocks only. Examples:
51-
/// `Mapping::set`, `Vector::push`, `Snark::verify`, storage writes.
52+
/// `Mapping::set`, `Vector::push`, storage writes.
5253
FinalizeWrite,
5354
/// Caller-context op: permitted inside regular `fn` and entry-point `fn` bodies only.
5455
/// Examples: `self.caller`, `self.signer`.
@@ -811,8 +812,9 @@ impl TypeCheckingVisitor<'_> {
811812
Type::Boolean
812813
}
813814
Intrinsic::SnarkVerify => {
814-
// Check that the operation is invoked in a `finalize` or `async` block.
815-
self.check_access_allowed("Snark::verify", AccessScope::FinalizeWrite, function_span);
815+
// Check that the operation is invoked in a `finalize` / `async` block or a view.
816+
// `snark.verify` is a pure instruction (no state mutation), so views may call it.
817+
self.check_access_allowed("Snark::verify", AccessScope::FinalizeRead, function_span);
816818

817819
// arg0: [u8; N] — verifying key (1D byte array)
818820
let Type::Array(vk_arr) = &arguments[0].0 else {
@@ -877,8 +879,9 @@ impl TypeCheckingVisitor<'_> {
877879
Type::Boolean
878880
}
879881
Intrinsic::SnarkVerifyBatch => {
880-
// Check that the operation is invoked in a `finalize` or `async` block.
881-
self.check_access_allowed("Snark::verify_batch", AccessScope::FinalizeWrite, function_span);
882+
// Check that the operation is invoked in a `finalize` / `async` block or a view.
883+
// `snark.verify` is a pure instruction (no state mutation), so views may call it.
884+
self.check_access_allowed("Snark::verify_batch", AccessScope::FinalizeRead, function_span);
882885

883886
// arg0: [[u8; N]; M] — verifying keys (2D byte array)
884887
let Type::Array(vks_outer) = &arguments[0].0 else {
@@ -1624,8 +1627,10 @@ impl TypeCheckingVisitor<'_> {
16241627
Intrinsic::SelfEdition => Type::Integer(IntegerType::U16),
16251628
Intrinsic::SelfId => Type::Address,
16261629
Intrinsic::SelfProgramOwner => {
1627-
// Check that the operation is only invoked in a `finalize` block.
1628-
self.check_access_allowed("program_owner", AccessScope::FinalizeWrite, function_span);
1630+
// Check that the operation is invoked in a `finalize` block or a view.
1631+
// The program owner resolves read-only in the finalize register path, so views can read it
1632+
// (matching the externally-addressed `Program::program_owner` form).
1633+
self.check_access_allowed("program_owner", AccessScope::FinalizeRead, function_span);
16291634
Type::Address
16301635
}
16311636
Intrinsic::SelfSigner => {
@@ -1640,8 +1645,9 @@ impl TypeCheckingVisitor<'_> {
16401645
Type::Integer(IntegerType::U32)
16411646
}
16421647
Intrinsic::BlockTimestamp => {
1643-
// Check that the operation is invoked in a `finalize` block. Rejected in view fns.
1644-
self.check_access_allowed("block.timestamp", AccessScope::FinalizeWrite, function_span);
1648+
// Check that the operation is invoked in a `finalize` block or a view.
1649+
// Views see the block timestamp via FinalizeGlobalState, exactly like `block.height`.
1650+
self.check_access_allowed("block.timestamp", AccessScope::FinalizeRead, function_span);
16451651
Type::Integer(IntegerType::I64)
16461652
}
16471653
Intrinsic::NetworkId => {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
program test.aleo;
2+
3+
function dummy:
4+
input r0 as u32.private;
5+
output r0 as u32.private;
6+
7+
view check:
8+
input r0 as [u8; 128u32].public;
9+
input r1 as u8.public;
10+
input r2 as [field; 4u32].public;
11+
input r3 as [u8; 256u32].public;
12+
input r4 as [[u8; 128u32]; 2u32].public;
13+
input r5 as [[[field; 4u32]; 3u32]; 2u32].public;
14+
snark.verify r0 r1 r2 r3 into r6;
15+
snark.verify.batch r4 r1 r5 r3 into r7;
16+
and r6 r7 into r8;
17+
output r8 as boolean.public;
18+
19+
constructor:
20+
assert.eq edition 0u16;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
program test.aleo;
2+
3+
function dummy:
4+
input r0 as u32.private;
5+
output r0 as u32.private;
6+
7+
view read_time:
8+
output block.timestamp as i64.public;
9+
10+
constructor:
11+
assert.eq edition 0u16;

tests/expectations/compiler/view/view_reads_block_timestamp_fail.out

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
program test.aleo;
2+
3+
function dummy_transition:
4+
input r0 as u32.private;
5+
add r0 1u32 into r1;
6+
output r1 as u32.private;
7+
8+
view read_time:
9+
output block.timestamp as i64.public;
10+
11+
view read_height:
12+
output block.height as u32.public;
13+
14+
constructor:
15+
assert.eq edition 0u16;
16+
status: success
17+
output: 1234567890i64
18+
status: success
19+
output: 0u32
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
program test.aleo;
2+
3+
function dummy_transition:
4+
input r0 as u32.private;
5+
add r0 1u32 into r1;
6+
output r1 as u32.private;
7+
8+
view read_owner:
9+
output program_owner as address.public;
10+
11+
constructor:
12+
assert.eq edition 0u16;
13+
status: success
14+
output: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// `Snark::verify` and `Snark::verify_batch` are pure instructions (no state mutation), so they are
2+
// allowed inside a `view fn`, exactly as they are inside a `final fn`. Executing them requires real
3+
// proof bytes (see the compile-only `core/algorithms/snark_verify` test), so this only checks that
4+
// the type checker accepts them in a view and that codegen emits valid Aleo.
5+
program test.aleo {
6+
fn dummy(x: u32) -> u32 { return x; }
7+
8+
view fn check(
9+
vk: [u8; 128],
10+
varuna_version: u8,
11+
inputs: [field; 4],
12+
proof: [u8; 256],
13+
vks: [[u8; 128]; 2],
14+
batch_inputs: [[[field; 4]; 3]; 2],
15+
) -> bool {
16+
let single: bool = Snark::verify(vk, varuna_version, inputs, proof);
17+
let batch: bool = Snark::verify_batch(vks, varuna_version, batch_inputs, proof);
18+
return single && batch;
19+
}
20+
21+
@noupgrade
22+
constructor() {}
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// `block.timestamp` is readable inside a `view fn`, exactly like `block.height` and `network.id`.
2+
// snarkVM populates the view's `FinalizeGlobalState` with the block timestamp (present from
3+
// ConsensusVersion::V12 onward), so views can read it off-consensus.
4+
program test.aleo {
5+
fn dummy(x: u32) -> u32 { return x; }
6+
7+
view fn read_time() -> i64 {
8+
return block.timestamp;
9+
}
10+
11+
@noupgrade
12+
constructor() {}
13+
}

tests/tests/compiler/view/view_reads_block_timestamp_fail.leo

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)