#[cfg(feature = "history")]
use crate::Process;
use crate::{FinalizeRegisters, Stack};
#[cfg(feature = "history")]
use console::program::ProgramID;
use console::{
network::prelude::*,
program::{Identifier, Value},
};
#[cfg(feature = "history")]
use snarkvm_ledger_store::{FinalizeStorage, FinalizeStore};
use snarkvm_synthesizer_program::{
FinalizeGlobalState,
FinalizeRegistersState,
FinalizeStoreTrait,
RegistersTrait,
StackTrait,
};
#[cfg(feature = "history")]
impl<N: Network> Process<N> {
#[inline]
pub fn evaluate_view_at_height<P: FinalizeStorage<N>>(
&self,
state: FinalizeGlobalState,
store: &FinalizeStore<N, P>,
program_id: impl TryInto<ProgramID<N>>,
view_name: impl TryInto<Identifier<N>>,
inputs: Vec<Value<N>>,
height: u32,
) -> Result<Vec<Value<N>>> {
let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
let view_name = view_name.try_into().map_err(|_| anyhow!("Invalid view function name"))?;
let stack = self.get_stack(program_id)?;
evaluate_view_at_height(state, store, &stack, &view_name, inputs, height)
}
}
#[cfg(feature = "history")]
pub fn evaluate_view_at_height<N: Network, P: FinalizeStorage<N>>(
state: FinalizeGlobalState,
store: &FinalizeStore<N, P>,
stack: &Stack<N>,
view_name: &Identifier<N>,
inputs: Vec<Value<N>>,
height: u32,
) -> Result<Vec<Value<N>>> {
let historic = HistoricFinalizeStore { store, height };
evaluate_view_inner(state, &historic, stack, view_name, inputs)
}
#[cfg(feature = "history")]
struct HistoricFinalizeStore<'a, N: Network, P: FinalizeStorage<N>> {
store: &'a FinalizeStore<N, P>,
height: u32,
}
#[cfg(feature = "history")]
impl<N: Network, P: FinalizeStorage<N>> FinalizeStoreTrait<N> for HistoricFinalizeStore<'_, N, P> {
fn contains_mapping_confirmed(
&self,
program_id: &console::program::ProgramID<N>,
mapping_name: &Identifier<N>,
) -> Result<bool> {
self.store.contains_mapping_confirmed(program_id, mapping_name)
}
fn contains_mapping_speculative(
&self,
program_id: &console::program::ProgramID<N>,
mapping_name: &Identifier<N>,
) -> Result<bool> {
self.store.contains_mapping_speculative(program_id, mapping_name)
}
fn contains_key_speculative(
&self,
program_id: console::program::ProgramID<N>,
mapping_name: Identifier<N>,
key: &console::program::Plaintext<N>,
) -> Result<bool> {
Ok(self.store.get_historical_mapping_value(program_id, mapping_name, key.clone(), self.height)?.is_some())
}
fn get_value_speculative(
&self,
program_id: console::program::ProgramID<N>,
mapping_name: Identifier<N>,
key: &console::program::Plaintext<N>,
) -> Result<Option<Value<N>>> {
Ok(self
.store
.get_historical_mapping_value(program_id, mapping_name, key.clone(), self.height)?
.map(|cow| cow.into_owned()))
}
fn insert_key_value(
&self,
_program_id: console::program::ProgramID<N>,
_mapping_name: Identifier<N>,
_key: console::program::Plaintext<N>,
_value: Value<N>,
) -> Result<snarkvm_synthesizer_program::FinalizeOperation<N>> {
bail!("Forbidden operation: view path cannot write to the finalize store ('insert_key_value')")
}
fn update_key_value(
&self,
_program_id: console::program::ProgramID<N>,
_mapping_name: Identifier<N>,
_key: console::program::Plaintext<N>,
_value: Value<N>,
) -> Result<snarkvm_synthesizer_program::FinalizeOperation<N>> {
bail!("Forbidden operation: view path cannot write to the finalize store ('update_key_value')")
}
fn remove_key_value(
&self,
_program_id: console::program::ProgramID<N>,
_mapping_name: Identifier<N>,
_key: &console::program::Plaintext<N>,
) -> Result<Option<snarkvm_synthesizer_program::FinalizeOperation<N>>> {
bail!("Forbidden operation: view path cannot write to the finalize store ('remove_key_value')")
}
}
pub(crate) fn evaluate_view_inner<N: Network>(
state: FinalizeGlobalState,
store: &dyn FinalizeStoreTrait<N>,
stack: &Stack<N>,
view_name: &Identifier<N>,
inputs: Vec<Value<N>>,
) -> Result<Vec<Value<N>>> {
let view = stack.program().get_view_ref(view_name)?;
let types = stack.get_view_types(view_name)?;
let mut registers = FinalizeRegisters::new(state, None, *view.name(), types, None);
ensure!(
view.inputs().len() == inputs.len(),
"View '{}' expects {} inputs, got {}",
view.name(),
view.inputs().len(),
inputs.len(),
);
for (i, value) in inputs.iter().enumerate() {
let kind = match value {
Value::Plaintext(_) => continue,
Value::Record(_) => "record",
Value::Future(_) => "future",
Value::DynamicRecord(_) => "dynamic record",
Value::DynamicFuture(_) => "dynamic future",
};
bail!("View '{}' input #{i} must be a plaintext value, got a {kind}", view.name());
}
for (input_stmt, value) in view.inputs().iter().zip(inputs.into_iter()) {
registers.store(stack, input_stmt.register(), value)?;
}
let mut counter = 0;
let mut finalize_operations: Vec<snarkvm_synthesizer_program::FinalizeOperation<N>> = Vec::new();
while counter < view.commands().len() {
let command = &view.commands()[counter];
crate::finalize::finalize_command_except_await(
Some((*stack.program_id(), *stack.program_edition())),
Some(*registers.function_name()),
store,
stack,
&mut registers,
view.positions(),
command,
&mut counter,
&mut finalize_operations,
view.name(),
)?;
}
ensure!(
finalize_operations.is_empty(),
"view '{}' produced finalize operations: {finalize_operations:?}",
view.name()
);
let mut outputs = Vec::with_capacity(view.outputs().len());
for output in view.outputs() {
outputs.push(registers.load(stack, output.operand())?);
}
Ok(outputs)
}
#[cfg(all(test, feature = "history"))]
mod tests {
use super::*;
use crate::Process;
use console::{
account::PrivateKey,
network::MainnetV0,
program::{Literal, Plaintext},
types::U64,
};
use snarkvm_ledger_store::helpers::memory::FinalizeMemory;
use snarkvm_synthesizer_program::{FinalizeStoreTrait, Program};
type CurrentNetwork = MainnetV0;
fn sample_finalize_state(block_height: u32) -> FinalizeGlobalState {
FinalizeGlobalState::from(block_height as u64, block_height, None, [0u8; 32])
}
#[test]
fn test_evaluate_view_simple() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program token_with_view.aleo;
mapping balances:
key as address.public;
value as u64.public;
mapping staked:
key as address.public;
value as u64.public;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view total_balance:
input r0 as address.public;
get.or_use balances[r0] 0u64 into r1;
get.or_use staked[r0] 0u64 into r2;
add r1 r2 into r3;
output r3 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let program_id = *program.id();
finalize_store.initialize_mapping(program_id, Identifier::from_str("balances")?)?;
finalize_store.initialize_mapping(program_id, Identifier::from_str("staked")?)?;
let mut rng = console::prelude::TestRng::default();
let private_key = PrivateKey::<CurrentNetwork>::new(&mut rng)?;
let address = console::account::Address::try_from(&private_key)?;
let address_key = Plaintext::from(Literal::Address(address));
finalize_store.update_key_value(
program_id,
Identifier::from_str("balances")?,
address_key.clone(),
Value::Plaintext(Plaintext::from(Literal::U64(U64::new(40)))),
)?;
finalize_store.update_key_value(
program_id,
Identifier::from_str("staked")?,
address_key.clone(),
Value::Plaintext(Plaintext::from(Literal::U64(U64::new(2)))),
)?;
let outputs = evaluate_view_at_height(
sample_finalize_state(0),
&finalize_store,
&stack,
&Identifier::from_str("total_balance")?,
vec![Value::Plaintext(address_key.clone())],
0,
)?;
assert_eq!(outputs.len(), 1);
match &outputs[0] {
Value::Plaintext(Plaintext::Literal(Literal::U64(v), _)) => assert_eq!(**v, 42),
other => panic!("unexpected output: {other}"),
}
Ok(())
}
#[test]
fn test_evaluate_view_uses_or_default_when_key_missing() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program token_with_view.aleo;
mapping balances:
key as address.public;
value as u64.public;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view fetch_balance:
input r0 as address.public;
get.or_use balances[r0] 7u64 into r1;
output r1 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
finalize_store.initialize_mapping(*program.id(), Identifier::from_str("balances")?)?;
let mut rng = console::prelude::TestRng::default();
let private_key = PrivateKey::<CurrentNetwork>::new(&mut rng)?;
let address = console::account::Address::try_from(&private_key)?;
let address_key = Plaintext::from(Literal::Address(address));
let outputs = evaluate_view_at_height(
sample_finalize_state(0),
&finalize_store,
&stack,
&Identifier::from_str("fetch_balance")?,
vec![Value::Plaintext(address_key)],
0,
)?;
assert_eq!(outputs.len(), 1);
match &outputs[0] {
Value::Plaintext(Plaintext::Literal(Literal::U64(v), _)) => assert_eq!(**v, 7),
other => panic!("unexpected output: {other}"),
}
Ok(())
}
#[test]
fn test_evaluate_view_errors_when_mapping_not_initialized() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program token_with_view.aleo;
mapping balances:
key as address.public;
value as u64.public;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view fetch_balance:
input r0 as address.public;
get.or_use balances[r0] 7u64 into r1;
output r1 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let mut rng = console::prelude::TestRng::default();
let private_key = PrivateKey::<CurrentNetwork>::new(&mut rng)?;
let address = console::account::Address::try_from(&private_key)?;
let address_key = Plaintext::from(Literal::Address(address));
let result = evaluate_view_at_height(
sample_finalize_state(0),
&finalize_store,
&stack,
&Identifier::from_str("fetch_balance")?,
vec![Value::Plaintext(address_key)],
0,
);
let err = result.expect_err("expected error when mapping is not initialized").to_string();
assert!(err.contains("does not exist"), "unexpected error message: {err}");
Ok(())
}
#[test]
fn test_evaluate_view_rejects_non_plaintext_input() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program vw_input_kind.aleo;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view echo:
input r0 as u64.public;
add r0 0u64 into r1;
output r1 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let future_value =
Value::Future(console::program::Future::new(*program.id(), Identifier::from_str("noop")?, vec![]));
let result = evaluate_view_at_height(
sample_finalize_state(0),
&finalize_store,
&stack,
&Identifier::from_str("echo")?,
vec![future_value],
0,
);
let err = match result {
Ok(_) => panic!("expected error for non-plaintext input"),
Err(err) => err.to_string(),
};
assert!(err.contains("future"), "unexpected error message: {err}");
assert!(err.contains("plaintext"), "unexpected error message: {err}");
Ok(())
}
#[test]
fn test_evaluate_view_ignores_pending_writes() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program vw_pending_isolate.aleo;
mapping balances:
key as address.public;
value as u64.public;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view lookup:
input r0 as address.public;
get.or_use balances[r0] 0u64 into r1;
output r1 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let program_id = *program.id();
let mapping_name = Identifier::from_str("balances")?;
finalize_store.initialize_mapping(program_id, mapping_name)?;
let mut rng = console::prelude::TestRng::default();
let private_key = PrivateKey::<CurrentNetwork>::new(&mut rng)?;
let address = console::account::Address::try_from(&private_key)?;
let address_key = Plaintext::from(Literal::Address(address));
finalize_store.start_atomic();
finalize_store.update_key_value(
program_id,
mapping_name,
address_key.clone(),
Value::Plaintext(Plaintext::from(Literal::U64(U64::new(99)))),
)?;
assert!(finalize_store.is_atomic_in_progress());
let outputs = evaluate_view_at_height(
sample_finalize_state(0),
&finalize_store,
&stack,
&Identifier::from_str("lookup")?,
vec![Value::Plaintext(address_key)],
0,
)?;
finalize_store.abort_atomic();
assert_eq!(outputs.len(), 1);
match &outputs[0] {
Value::Plaintext(Plaintext::Literal(Literal::U64(v), _)) => assert_eq!(
**v, 0,
"view should observe confirmed state (default 0), not the pending in-batch write (99)"
),
other => panic!("unexpected output: {other}"),
}
Ok(())
}
#[test]
fn test_view_can_read_block_timestamp() -> Result<()> {
let program = Program::<CurrentNetwork>::from_str(
r"
program vw_block_ts.aleo;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view reads_ts:
add block.timestamp 0i64 into r0;
output r0 as i64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let state = FinalizeGlobalState::from(1, 1, Some(1234567890), [0u8; 32]);
let outputs =
evaluate_view_at_height(state, &finalize_store, &stack, &Identifier::from_str("reads_ts")?, vec![], 1)?;
assert_eq!(outputs.len(), 1);
match &outputs[0] {
Value::Plaintext(Plaintext::Literal(Literal::I64(v), _)) => assert_eq!(**v, 1234567890),
other => panic!("expected i64 plaintext, got: {other}"),
}
Ok(())
}
#[test]
fn test_evaluate_view_at_height_returns_historic_value() -> Result<()> {
use std::sync::atomic::Ordering;
let program = Program::<CurrentNetwork>::from_str(
r"
program vw_history.aleo;
mapping balances:
key as address.public;
value as u64.public;
function noop:
input r0 as u64.private;
output r0 as u64.private;
view lookup:
input r0 as address.public;
get balances[r0] into r1;
output r1 as u64.public;",
)?;
let process = Process::<CurrentNetwork>::load()?;
let stack = Stack::new(&process, &program)?;
let finalize_store = FinalizeStore::<_, FinalizeMemory<_>>::open(aleo_std::StorageMode::new_test(None))?;
let program_id = *program.id();
let mapping_name = Identifier::from_str("balances")?;
finalize_store.initialize_mapping(program_id, mapping_name)?;
let mut rng = console::prelude::TestRng::default();
let private_key = PrivateKey::<CurrentNetwork>::new(&mut rng)?;
let address = console::account::Address::try_from(&private_key)?;
let address_key = Plaintext::from(Literal::Address(address));
finalize_store.current_block_height().store(1, Ordering::SeqCst);
finalize_store.update_key_value(
program_id,
mapping_name,
address_key.clone(),
Value::Plaintext(Plaintext::from(Literal::U64(U64::new(11)))),
)?;
finalize_store.current_block_height().store(5, Ordering::SeqCst);
finalize_store.update_key_value(
program_id,
mapping_name,
address_key.clone(),
Value::Plaintext(Plaintext::from(Literal::U64(U64::new(55)))),
)?;
let extract = |outputs: Vec<Value<CurrentNetwork>>| match &outputs[0] {
Value::Plaintext(Plaintext::Literal(Literal::U64(v), _)) => **v,
other => panic!("expected u64, got: {other}"),
};
let outputs = evaluate_view_at_height(
sample_finalize_state(5),
&finalize_store,
&stack,
&Identifier::from_str("lookup")?,
vec![Value::Plaintext(address_key.clone())],
5,
)?;
assert_eq!(extract(outputs), 55, "current state should reflect the most recent write");
let outputs = evaluate_view_at_height(
sample_finalize_state(1),
&finalize_store,
&stack,
&Identifier::from_str("lookup")?,
vec![Value::Plaintext(address_key.clone())],
1,
)?;
assert_eq!(extract(outputs), 11, "expected historic value at height 1");
let outputs = evaluate_view_at_height(
sample_finalize_state(5),
&finalize_store,
&stack,
&Identifier::from_str("lookup")?,
vec![Value::Plaintext(address_key.clone())],
5,
)?;
assert_eq!(extract(outputs), 55, "expected historic value at height 5");
let outputs = evaluate_view_at_height(
sample_finalize_state(3),
&finalize_store,
&stack,
&Identifier::from_str("lookup")?,
vec![Value::Plaintext(address_key)],
3,
)?;
assert_eq!(extract(outputs), 11, "expected applicable historic value at intermediate height 3");
Ok(())
}
}