Merge pull request #400 from logos-blockchain/schouhy/add-block-context

Add validity windows to program output
This commit is contained in:
Sergio Chouhy 2026-03-27 10:07:38 -03:00 committed by GitHub
commit 085ca69e42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 817 additions and 239 deletions

16
Cargo.lock generated
View File

@ -1956,7 +1956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
dependencies = [
"data-encoding",
"syn 1.0.109",
"syn 2.0.117",
]
[[package]]
@ -2105,7 +2105,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -2406,7 +2406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -5952,7 +5952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@ -5965,7 +5965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@ -6883,7 +6883,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -7811,7 +7811,7 @@ dependencies = [
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@ -8976,7 +8976,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -46,7 +46,7 @@ impl BedrockClient {
info!("Creating Bedrock client with node URL {node_url}");
let client = Client::builder()
//Add more fields if needed
.timeout(std::time::Duration::from_secs(60))
.timeout(std::time::Duration::from_mins(1))
.build()
.context("Failed to build HTTP client")?;

View File

@ -3,7 +3,7 @@ use log::warn;
use nssa::{AccountId, V03State};
use serde::{Deserialize, Serialize};
use crate::HashType;
use crate::{HashType, block::BlockId};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum NSSATransaction {
@ -68,10 +68,13 @@ impl NSSATransaction {
pub fn execute_check_on_state(
self,
state: &mut V03State,
block_id: BlockId,
) -> Result<Self, nssa::error::NssaError> {
match &self {
Self::Public(tx) => state.transition_from_public_transaction(tx),
Self::PrivacyPreserving(tx) => state.transition_from_privacy_preserving_transaction(tx),
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id),
Self::PrivacyPreserving(tx) => {
state.transition_from_privacy_preserving_transaction(tx, block_id)
}
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
}
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;

View File

@ -1,5 +1,5 @@
use nssa_core::program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
};
// Hello-world example program.
@ -56,5 +56,7 @@ fn main() {
// The output is a proposed state difference. It will only succeed if the pre states coincide
// with the previous values of the accounts, and the transition to the post states conforms
// with the NSSA program rules.
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]);
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write();
}

View File

@ -1,5 +1,5 @@
use nssa_core::program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
};
// Hello-world with authorization example program.
@ -63,5 +63,7 @@ fn main() {
// The output is a proposed state difference. It will only succeed if the pre states coincide
// with the previous values of the accounts, and the transition to the post states conforms
// with the NSSA program rules.
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]);
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write();
}

View File

@ -1,7 +1,7 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
},
};
@ -95,5 +95,7 @@ fn main() {
_ => panic!("invalid params"),
};
write_nssa_outputs(instruction_words, pre_states, post_states);
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(instruction_words, pre_states, post_states).write();
}

View File

@ -1,6 +1,5 @@
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
};
// Tail Call example program.
@ -53,11 +52,10 @@ fn main() {
pda_seeds: vec![],
};
// Write the outputs
write_nssa_outputs_with_chained_call(
instruction_data,
vec![pre_state],
vec![post_state],
vec![chained_call],
);
// Write the outputs.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state])
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -1,6 +1,6 @@
use nssa_core::program::{
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
read_nssa_inputs,
};
// Tail Call with PDA example program.
@ -65,11 +65,10 @@ fn main() {
pda_seeds: vec![PDA_SEED],
};
// Write the outputs
write_nssa_outputs_with_chained_call(
instruction_data,
vec![pre_state],
vec![post_state],
vec![chained_call],
);
// Write the outputs.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state])
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -177,6 +177,7 @@ pub fn TransactionPage() -> impl IntoView {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window
} = message;
let WitnessSet {
signatures_and_public_keys: _,
@ -212,6 +213,10 @@ pub fn TransactionPage() -> impl IntoView {
<span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Validity Window:"</span>
<span class="info-value">{validity_window.to_string()}</span>
</div>
</div>
<h3>"Public Accounts"</h3>

View File

@ -125,7 +125,7 @@ impl IndexerStore {
transaction
.clone()
.transaction_stateless_check()?
.execute_check_on_state(&mut state_guard)?;
.execute_check_on_state(&mut state_guard, block.header.block_id)?;
}
}

View File

@ -7,7 +7,7 @@ use crate::{
CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId,
Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction,
Signature, Transaction, WitnessSet,
Signature, Transaction, ValidityWindow, WitnessSet,
};
// ============================================================================
@ -287,6 +287,7 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
} = value;
Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -301,12 +302,13 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.collect(),
validity_window: validity_window.into(),
}
}
}
impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction::message::Message {
type Error = nssa_core::account::data::DataTooBigError;
type Error = nssa::error::NssaError;
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
let PrivacyPreservingMessage {
@ -316,6 +318,7 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
} = value;
Ok(Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -326,7 +329,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
public_post_states: public_post_states
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?,
.collect::<Result<Vec<_>, _>>()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
encrypted_private_post_states: encrypted_private_post_states
.into_iter()
.map(Into::into)
@ -336,6 +340,9 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.collect(),
validity_window: validity_window
.try_into()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
})
}
}
@ -479,14 +486,7 @@ impl TryFrom<PrivacyPreservingTransaction> for nssa::PrivacyPreservingTransactio
witness_set,
} = value;
Ok(Self::new(
message
.try_into()
.map_err(|err: nssa_core::account::data::DataTooBigError| {
nssa::error::NssaError::InvalidInput(err.to_string())
})?,
witness_set.try_into()?,
))
Ok(Self::new(message.try_into()?, witness_set.try_into()?))
}
}
@ -687,3 +687,21 @@ impl From<HashType> for common::HashType {
Self(value.0)
}
}
// ============================================================================
// ValidityWindow conversions
// ============================================================================
impl From<nssa_core::program::ValidityWindow> for ValidityWindow {
fn from(value: nssa_core::program::ValidityWindow) -> Self {
Self((value.start(), value.end()))
}
}
impl TryFrom<ValidityWindow> for nssa_core::program::ValidityWindow {
type Error = nssa_core::program::InvalidWindow;
fn try_from(value: ValidityWindow) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

View File

@ -235,6 +235,7 @@ pub struct PrivacyPreservingMessage {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
@ -300,6 +301,20 @@ pub struct Nullifier(
pub [u8; 32],
);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct ValidityWindow(pub (Option<BlockId>, Option<BlockId>));
impl Display for ValidityWindow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
(Some(start), Some(end)) => write!(f, "[{start}, {end})"),
(Some(start), None) => write!(f, "[{start}, \u{221e})"),
(None, Some(end)) => write!(f, "(-\u{221e}, {end})"),
(None, None) => write!(f, "(-\u{221e}, \u{221e})"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct CommitmentSetDigest(
#[serde(with = "base64::arr")]

View File

@ -13,7 +13,7 @@ use indexer_service_protocol::{
CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId,
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
Transaction, WitnessSet,
Transaction, ValidityWindow, WitnessSet,
};
use jsonrpsee::{
core::{SubscriptionResult, async_trait},
@ -124,6 +124,7 @@ impl MockIndexerService {
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],

View File

@ -211,7 +211,7 @@ pub fn sequencer_config(
max_block_size,
mempool_max_size,
block_create_timeout,
retry_pending_blocks_timeout: Duration::from_secs(120),
retry_pending_blocks_timeout: Duration::from_mins(2),
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
signing_key: [37; 32],

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
program::{ProgramId, ProgramOutput},
program::{ProgramId, ProgramOutput, ValidityWindow},
};
#[derive(Serialize, Deserialize)]
@ -36,6 +36,7 @@ pub struct PrivacyPreservingCircuitOutput {
pub ciphertexts: Vec<Ciphertext>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
}
#[cfg(feature = "host")]
@ -101,6 +102,7 @@ mod tests {
),
[0xab; 32],
)],
validity_window: (Some(1), None).try_into().unwrap(),
};
let bytes = output.to_bytes();
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();

View File

@ -1,5 +1,7 @@
use std::collections::HashSet;
#[cfg(any(feature = "host", test))]
use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
@ -151,15 +153,163 @@ impl AccountPostState {
}
}
pub type BlockId = u64;
#[derive(Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(
any(feature = "host", test),
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
)]
pub struct ValidityWindow {
from: Option<BlockId>,
to: Option<BlockId>,
}
impl ValidityWindow {
/// Creates a window with no bounds, valid for every block ID.
#[must_use]
pub const fn new_unbounded() -> Self {
Self {
from: None,
to: None,
}
}
/// Returns `true` if `id` falls within the half-open range `[from, to)`.
/// A `None` bound on either side is treated as unbounded in that direction.
#[must_use]
pub fn is_valid_for_block_id(&self, id: BlockId) -> bool {
self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end)
}
/// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`.
const fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from_id), Some(until_id)) = (self.from, self.to)
&& from_id >= until_id
{
Err(InvalidWindow)
} else {
Ok(())
}
}
/// Inclusive lower bound. `None` means the window starts at the beginning of the chain.
#[must_use]
pub const fn start(&self) -> Option<BlockId> {
self.from
}
/// Exclusive upper bound. `None` means the window has no expiry.
#[must_use]
pub const fn end(&self) -> Option<BlockId> {
self.to
}
}
impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
type Error = InvalidWindow;
fn try_from(value: (Option<BlockId>, Option<BlockId>)) -> Result<Self, Self::Error> {
let this = Self {
from: value.0,
to: value.1,
};
this.check_window()?;
Ok(this)
}
}
impl TryFrom<std::ops::Range<BlockId>> for ValidityWindow {
type Error = InvalidWindow;
fn try_from(value: std::ops::Range<BlockId>) -> Result<Self, Self::Error> {
(Some(value.start), Some(value.end)).try_into()
}
}
impl From<std::ops::RangeFrom<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeFrom<BlockId>) -> Self {
Self {
from: Some(value.start),
to: None,
}
}
}
impl From<std::ops::RangeTo<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeTo<BlockId>) -> Self {
Self {
from: None,
to: Some(value.end),
}
}
}
impl From<std::ops::RangeFull> for ValidityWindow {
fn from(_: std::ops::RangeFull) -> Self {
Self::new_unbounded()
}
}
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
#[error("Invalid window")]
pub struct InvalidWindow;
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
#[must_use = "ProgramOutput does nothing unless written"]
pub struct ProgramOutput {
/// The instruction data the program received to produce this output.
pub instruction_data: InstructionData,
/// The account pre states the program received to produce this output.
pub pre_states: Vec<AccountWithMetadata>,
/// The account post states the program execution produced.
pub post_states: Vec<AccountPostState>,
/// The list of chained calls to other programs.
pub chained_calls: Vec<ChainedCall>,
/// The window where the program output is valid.
pub validity_window: ValidityWindow,
}
impl ProgramOutput {
pub const fn new(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) -> Self {
Self {
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
validity_window: ValidityWindow::new_unbounded(),
}
}
pub fn write(self) {
env::commit(&self);
}
pub fn with_chained_calls(mut self, chained_calls: Vec<ChainedCall>) -> Self {
self.chained_calls = chained_calls;
self
}
/// Sets the validity window from an infallible range conversion (`1..`, `..5`, `..`).
pub fn with_validity_window<W: Into<ValidityWindow>>(mut self, window: W) -> Self {
self.validity_window = window.into();
self
}
/// Sets the validity window from a fallible range conversion (`1..5`).
/// Returns `Err` if the range is empty.
pub fn try_with_validity_window<W: TryInto<ValidityWindow, Error = InvalidWindow>>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
self.validity_window = window.try_into()?;
Ok(self)
}
}
/// Representation of a number as `lo + hi * 2^128`.
@ -219,35 +369,6 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
)
}
pub fn write_nssa_outputs(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
};
env::commit(&output);
}
pub fn write_nssa_outputs_with_chained_call(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls,
};
env::commit(&output);
}
/// Validates well-behaved program execution.
///
/// # Parameters
@ -342,6 +463,132 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
mod tests {
use super::*;
#[test]
fn validity_window_unbounded_accepts_any_block() {
let w = ValidityWindow::new_unbounded();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(u64::MAX));
}
#[test]
fn validity_window_bounded_range_includes_from_excludes_to() {
let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(9));
assert!(!w.is_valid_for_block_id(10));
}
#[test]
fn validity_window_only_from_bound() {
let w: ValidityWindow = (Some(5), None).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(u64::MAX));
}
#[test]
fn validity_window_only_to_bound() {
let w: ValidityWindow = (None, Some(5)).try_into().unwrap();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(4));
assert!(!w.is_valid_for_block_id(5));
}
#[test]
fn validity_window_adjacent_bounds_are_invalid() {
// [5, 5) is an empty range — from == to
assert!(ValidityWindow::try_from((Some(5), Some(5))).is_err());
}
#[test]
fn validity_window_inverted_bounds_are_invalid() {
assert!(ValidityWindow::try_from((Some(10), Some(5))).is_err());
}
#[test]
fn validity_window_getters_match_construction() {
let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap();
assert_eq!(w.start(), Some(3));
assert_eq!(w.end(), Some(7));
}
#[test]
fn validity_window_getters_for_unbounded() {
let w = ValidityWindow::new_unbounded();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range() {
let w = ValidityWindow::try_from(5_u64..10).unwrap();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_empty_is_invalid() {
assert!(ValidityWindow::try_from(5_u64..5).is_err());
}
#[test]
fn validity_window_from_range_inverted_is_invalid() {
let from = 10_u64;
let to = 5_u64;
assert!(ValidityWindow::try_from(from..to).is_err());
}
#[test]
fn validity_window_from_range_from() {
let w: ValidityWindow = (5_u64..).into();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range_to() {
let w: ValidityWindow = (..10_u64).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_full() {
let w: ValidityWindow = (..).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn program_output_try_with_validity_window_range() {
let output = ProgramOutput::new(vec![], vec![], vec![])
.try_with_validity_window(10_u64..100)
.unwrap();
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), Some(100));
}
#[test]
fn program_output_with_validity_window_range_from() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(10_u64..);
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), None);
}
#[test]
fn program_output_with_validity_window_range_to() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(..100_u64);
assert_eq!(output.validity_window.start(), None);
assert_eq!(output.validity_window.end(), Some(100));
}
#[test]
fn program_output_try_with_validity_window_empty_range_fails() {
let result = ProgramOutput::new(vec![], vec![], vec![]).try_with_validity_window(5_u64..5);
assert!(result.is_err());
}
#[test]
fn post_state_new_with_claim_constructor() {
let account = Account {

View File

@ -69,6 +69,9 @@ pub enum NssaError {
#[error("Max account nonce reached")]
MaxAccountNonceReached,
#[error("Execution outside of the validity window")]
OutOfValidityWindow,
}
#[cfg(test)]

View File

@ -174,12 +174,13 @@ mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
use nssa_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
};
use super::*;
use crate::{
error::NssaError,
privacy_preserving_transaction::circuit::execute_and_prove,
program::Program,
state::{
@ -364,4 +365,46 @@ mod tests {
.unwrap();
assert_eq!(recipient_post, expected_private_account_2);
}
#[test]
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&account_keys.npk()),
);
let validity_window_chain_caller = Program::validity_window_chain_caller();
let validity_window = Program::validity_window();
let instruction = Program::serialize_instruction((
Some(1_u64),
Some(4_u64),
validity_window.id(),
Some(4_u64),
Some(7_u64),
))
.unwrap();
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
[(validity_window.id(), validity_window)].into(),
);
let result = execute_and_prove(
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&program_with_deps,
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
}

View File

@ -3,6 +3,7 @@ use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
program::ValidityWindow,
};
use sha2::{Digest as _, Sha256};
@ -52,6 +53,7 @@ pub struct Message {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
}
impl std::fmt::Debug for Message {
@ -77,6 +79,7 @@ impl std::fmt::Debug for Message {
)
.field("new_commitments", &self.new_commitments)
.field("new_nullifiers", &nullifiers)
.field("validity_window", &self.validity_window)
.finish()
}
}
@ -109,6 +112,7 @@ impl Message {
encrypted_private_post_states,
new_commitments: output.new_commitments,
new_nullifiers: output.new_nullifiers,
validity_window: output.validity_window,
})
}
}
@ -161,6 +165,7 @@ pub mod tests {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window: (None, None).try_into().unwrap(),
}
}

View File

@ -7,6 +7,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
account::{Account, AccountWithMetadata},
program::{BlockId, ValidityWindow},
};
use sha2::{Digest as _, digest::FixedOutput as _};
@ -35,6 +36,7 @@ impl PrivacyPreservingTransaction {
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = &self.message;
let witness_set = &self.witness_set;
@ -91,6 +93,11 @@ impl PrivacyPreservingTransaction {
}
}
// Verify validity window
if !message.validity_window.is_valid_for_block_id(block_id) {
return Err(NssaError::OutOfValidityWindow);
}
// Build pre_states for proof verification
let public_pre_states: Vec<_> = message
.public_account_ids
@ -112,6 +119,7 @@ impl PrivacyPreservingTransaction {
&message.encrypted_private_post_states,
&message.new_commitments,
&message.new_nullifiers,
&message.validity_window,
)?;
// 5. Commitment freshness
@ -173,6 +181,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
encrypted_private_post_states: &[EncryptedAccountData],
new_commitments: &[Commitment],
new_nullifiers: &[(Nullifier, CommitmentSetDigest)],
validity_window: &ValidityWindow,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
@ -184,6 +193,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
.collect(),
new_commitments: new_commitments.to_vec(),
new_nullifiers: new_nullifiers.to_vec(),
validity_window: validity_window.to_owned(),
};
proof
.is_valid_for(&output)

View File

@ -292,6 +292,20 @@ mod tests {
// `program_methods`
Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap()
}
#[must_use]
pub fn validity_window() -> Self {
use test_program_methods::VALIDITY_WINDOW_ELF;
// This unwrap won't panic since the `VALIDITY_WINDOW_ELF` comes from risc0 build of
// `program_methods`
Self::new(VALIDITY_WINDOW_ELF.to_vec()).unwrap()
}
#[must_use]
pub fn validity_window_chain_caller() -> Self {
use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF;
Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap()
}
}
#[test]

View File

@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest as _, digest::FixedOutput as _};
@ -70,6 +70,7 @@ impl PublicTransaction {
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = self.message();
let witness_set = self.witness_set();
@ -190,6 +191,14 @@ impl PublicTransaction {
NssaError::InvalidProgramBehavior
);
// Verify validity window
ensure!(
program_output
.validity_window
.is_valid_for_block_id(block_id),
NssaError::OutOfValidityWindow
);
for post in program_output
.post_states
.iter_mut()
@ -359,7 +368,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state);
let result = tx.validate_and_produce_public_state_diff(&state, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -379,7 +388,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state);
let result = tx.validate_and_produce_public_state_diff(&state, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -400,7 +409,7 @@ pub mod tests {
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state);
let result = tx.validate_and_produce_public_state_diff(&state, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -420,7 +429,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state);
let result = tx.validate_and_produce_public_state_diff(&state, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -436,7 +445,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state);
let result = tx.validate_and_produce_public_state_diff(&state, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
}

View File

@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
account::{Account, AccountId, Nonce},
program::ProgramId,
program::{BlockId, ProgramId},
};
use crate::{
@ -158,8 +158,9 @@ impl V03State {
pub fn transition_from_public_transaction(
&mut self,
tx: &PublicTransaction,
block_id: BlockId,
) -> Result<(), NssaError> {
let state_diff = tx.validate_and_produce_public_state_diff(self)?;
let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?;
#[expect(
clippy::iter_over_hash_type,
@ -182,9 +183,10 @@ impl V03State {
pub fn transition_from_privacy_preserving_transaction(
&mut self,
tx: &PrivacyPreservingTransaction,
block_id: BlockId,
) -> Result<(), NssaError> {
// 1. Verify the transaction satisfies acceptance criteria
let public_state_diff = tx.validate_and_produce_public_state_diff(self)?;
let public_state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?;
let message = tx.message();
@ -339,7 +341,7 @@ pub mod tests {
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey},
program::{PdaSeed, ProgramId},
program::{BlockId, PdaSeed, ProgramId, ValidityWindow},
};
use crate::{
@ -374,6 +376,7 @@ pub mod tests {
self.insert_program(Program::amm());
self.insert_program(Program::claimer());
self.insert_program(Program::changer_claimer());
self.insert_program(Program::validity_window());
self
}
@ -569,7 +572,7 @@ pub mod tests {
let balance_to_move = 5;
let tx = transfer_transaction(from, &key, 0, to, balance_to_move);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
assert_eq!(state.get_account_by_id(from).balance, 95);
assert_eq!(state.get_account_by_id(to).balance, 5);
@ -590,7 +593,7 @@ pub mod tests {
assert!(state.get_account_by_id(from).balance < balance_to_move);
let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
assert_eq!(state.get_account_by_id(from).balance, 100);
@ -614,7 +617,7 @@ pub mod tests {
let balance_to_move = 8;
let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
assert_eq!(state.get_account_by_id(from).balance, 192);
assert_eq!(state.get_account_by_id(to).balance, 108);
@ -634,10 +637,10 @@ pub mod tests {
let balance_to_move = 5;
let tx = transfer_transaction(account_id1, &key1, 0, account_id2, balance_to_move);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let balance_to_move = 3;
let tx = transfer_transaction(account_id2, &key2, 0, account_id3, balance_to_move);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
assert_eq!(state.get_account_by_id(account_id1).balance, 95);
assert_eq!(state.get_account_by_id(account_id2).balance, 2);
@ -659,7 +662,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -676,7 +679,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -693,7 +696,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -717,7 +720,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -741,7 +744,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -765,7 +768,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -789,7 +792,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -817,7 +820,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -842,7 +845,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -860,7 +863,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -889,7 +892,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -1082,7 +1085,7 @@ pub mod tests {
assert!(!state.private_state.0.contains(&expected_new_commitment));
state
.transition_from_privacy_preserving_transaction(&tx)
.transition_from_privacy_preserving_transaction(&tx, 1)
.unwrap();
let sender_post = state.get_account_by_id(sender_keys.account_id());
@ -1152,7 +1155,7 @@ pub mod tests {
assert!(!state.private_state.1.contains(&expected_new_nullifier));
state
.transition_from_privacy_preserving_transaction(&tx)
.transition_from_privacy_preserving_transaction(&tx, 1)
.unwrap();
assert_eq!(state.public_state, previous_public_state);
@ -1216,7 +1219,7 @@ pub mod tests {
assert!(!state.private_state.1.contains(&expected_new_nullifier));
state
.transition_from_privacy_preserving_transaction(&tx)
.transition_from_privacy_preserving_transaction(&tx, 1)
.unwrap();
let recipient_post = state.get_account_by_id(recipient_keys.account_id());
@ -2144,7 +2147,7 @@ pub mod tests {
);
state
.transition_from_privacy_preserving_transaction(&tx)
.transition_from_privacy_preserving_transaction(&tx, 1)
.unwrap();
let sender_private_account = Account {
@ -2162,7 +2165,7 @@ pub mod tests {
&state,
);
let result = state.transition_from_privacy_preserving_transaction(&tx);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
let NssaError::InvalidInput(error_message) = result.err().unwrap() else {
@ -2239,7 +2242,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let recipient_post = state.get_account_by_id(to);
@ -2282,7 +2285,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2322,7 +2325,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(
result,
Err(NssaError::MaxChainedCallsDepthExceeded)
@ -2363,7 +2366,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2419,7 +2422,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2528,7 +2531,7 @@ pub mod tests {
let transaction = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&transaction)
.transition_from_privacy_preserving_transaction(&transaction, 1)
.unwrap();
// Assert
@ -2584,7 +2587,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
// Execution of winner's token holding account initialization
let instruction = token_core::Instruction::InitializeAccount;
@ -2597,7 +2600,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
// Submit a solution to the pinata program to claim the prize
let solution: u128 = 989_106;
@ -2614,7 +2617,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id);
assert_eq!(
@ -2644,7 +2647,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -2690,7 +2693,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
let tx = PublicTransaction::new(message, witness_set);
let res = state.transition_from_public_transaction(&tx);
let res = state.transition_from_public_transaction(&tx, 1);
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
let sender_post = state.get_account_by_id(sender_id);
@ -2759,7 +2762,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
let result = state.transition_from_privacy_preserving_transaction(&tx);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1);
assert!(result.is_ok());
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
@ -2812,7 +2815,7 @@ pub mod tests {
// Claim should succeed
assert!(
state
.transition_from_privacy_preserving_transaction(&tx)
.transition_from_privacy_preserving_transaction(&tx, 1)
.is_ok()
);
@ -2861,7 +2864,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
@ -2886,7 +2889,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx);
let result = state.transition_from_public_transaction(&tx, 1);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
@ -2998,6 +3001,119 @@ pub mod tests {
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn validity_window_works_in_public_transactions(
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_public_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let account_ids = vec![pre.account_id];
let nonces = vec![];
let program_id = validity_window_program.id();
let message = public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
validity_window,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
PublicTransaction::new(message, witness_set)
};
let result = state.transition_from_public_transaction(&tx, block_id);
let is_inside_validity_window = match (validity_window.start(), validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn validity_window_works_in_privacy_preserving_transactions(
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = circuit::execute_and_prove(
vec![pre],
Program::serialize_instruction(validity_window).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![],
vec![],
vec![(account_keys.npk(), account_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set)
};
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id);
let is_inside_validity_window = match (validity_window.start(), validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test]
fn state_serialization_roundtrip() {
let account_id_1 = AccountId::new([1; 32]);

View File

@ -9,7 +9,7 @@
use std::num::NonZero;
use amm_core::Instruction;
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call};
use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs};
fn main() {
let (
@ -133,10 +133,7 @@ fn main() {
}
};
write_nssa_outputs_with_chained_call(
instruction_words,
pre_states_clone,
post_states,
chained_calls,
);
ProgramOutput::new(instruction_words, pre_states_clone, post_states)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -1,5 +1,5 @@
use ata_core::Instruction;
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs_with_chained_call};
use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs};
fn main() {
let (
@ -56,10 +56,7 @@ fn main() {
}
};
write_nssa_outputs_with_chained_call(
instruction_words,
pre_states_clone,
post_states,
chained_calls,
);
ProgramOutput::new(instruction_words, pre_states_clone, post_states)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -1,7 +1,7 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
},
};
@ -84,5 +84,5 @@ fn main() {
_ => panic!("invalid params"),
};
write_nssa_outputs(instruction_words, pre_states, post_states);
ProgramOutput::new(instruction_words, pre_states, post_states).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
use risc0_zkvm::sha::{Impl, Sha256 as _};
const PRIZE: u128 = 150;
@ -78,12 +78,13 @@ fn main() {
.checked_add(PRIZE)
.expect("Overflow when adding prize to winner");
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pinata, winner],
vec![
AccountPostState::new_claimed_if_default(pinata_post),
AccountPostState::new(winner_post),
],
);
)
.write();
}

View File

@ -1,8 +1,7 @@
use nssa_core::{
account::Data,
program::{
AccountPostState, ChainedCall, PdaSeed, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs,
},
};
use risc0_zkvm::sha::{Impl, Sha256 as _};
@ -97,7 +96,7 @@ fn main() {
)
.with_pda_seeds(vec![PdaSeed::new([0; 32])]);
write_nssa_outputs_with_chained_call(
ProgramOutput::new(
instruction_words,
vec![
pinata_definition,
@ -109,6 +108,7 @@ fn main() {
AccountPostState::new(pinata_token_holding_post),
AccountPostState::new(winner_token_holding_post),
],
vec![chained_call],
);
)
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -11,7 +11,7 @@ use nssa_core::{
compute_digest_for_path,
program::{
AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId,
ProgramOutput, validate_execution,
ProgramOutput, ValidityWindow, validate_execution,
},
};
use risc0_zkvm::{guest::env, serde::to_vec};
@ -20,11 +20,31 @@ use risc0_zkvm::{guest::env, serde::to_vec};
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
post_states: HashMap<AccountId, Account>,
validity_window: ValidityWindow,
}
impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec<ProgramOutput>) -> Self {
let valid_from_id = program_outputs
.iter()
.filter_map(|output| output.validity_window.start())
.max();
let valid_until_id = program_outputs
.iter()
.filter_map(|output| output.validity_window.end())
.min();
let validity_window = (valid_from_id, valid_until_id).try_into().expect(
"There should be non empty intersection in the program output validity windows",
);
let mut execution_state = Self {
pre_states: Vec::new(),
post_states: HashMap::new(),
validity_window,
};
let Some(first_output) = program_outputs.first() else {
panic!("No program outputs provided");
};
@ -37,11 +57,6 @@ impl ExecutionState {
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut execution_state = Self {
pre_states: Vec::new(),
post_states: HashMap::new(),
};
let mut program_outputs_iter = program_outputs.into_iter();
let mut chain_calls_counter = 0;
@ -210,6 +225,7 @@ fn compute_circuit_output(
ciphertexts: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
validity_window: execution_state.validity_window,
};
let states_iter = execution_state.into_states_iter();

View File

@ -6,7 +6,7 @@
//! Token program accepts [`Instruction`] as input, refer to the corresponding documentation
//! for more details.
use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs};
use token_program::core::Instruction;
fn main() {
@ -81,5 +81,5 @@ fn main() {
}
};
write_nssa_outputs(instruction_words, pre_states_clone, post_states);
ProgramOutput::new(instruction_words, pre_states_clone, post_states).write();
}

View File

@ -2733,7 +2733,7 @@ fn simple_amm_remove() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2813,7 +2813,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2897,7 +2897,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2969,7 +2969,7 @@ fn simple_amm_new_definition_uninitialized_pool() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3031,7 +3031,7 @@ fn simple_amm_add() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3088,7 +3088,7 @@ fn simple_amm_swap_1() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3138,7 +3138,7 @@ fn simple_amm_swap_2() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx).unwrap();
state.transition_from_public_transaction(&tx, 1).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());

View File

@ -167,10 +167,12 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
tx: NSSATransaction,
) -> Result<NSSATransaction, nssa::error::NssaError> {
match &tx {
NSSATransaction::Public(tx) => self.state.transition_from_public_transaction(tx),
NSSATransaction::Public(tx) => self
.state
.transition_from_public_transaction(tx, self.next_block_id()),
NSSATransaction::PrivacyPreserving(tx) => self
.state
.transition_from_privacy_preserving_transaction(tx),
.transition_from_privacy_preserving_transaction(tx, self.next_block_id()),
NSSATransaction::ProgramDeployment(tx) => self
.state
.transition_from_program_deployment_transaction(tx),
@ -204,10 +206,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
) -> Result<(SignedMantleTx, MsgId)> {
let now = Instant::now();
let new_block_height = self
.chain_height
.checked_add(1)
.with_context(|| format!("Max block height reached: {}", self.chain_height))?;
let new_block_height = self.next_block_id();
let mut valid_transactions = vec![];
@ -354,6 +353,12 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
pub fn indexer_client(&self) -> IC {
self.indexer_client.clone()
}
fn next_block_id(&self) -> u64 {
self.chain_height
.checked_add(1)
.unwrap_or_else(|| panic!("Max block height reached: {}", self.chain_height))
}
}
/// Load signing key from file or generate a new one if it doesn't exist.
@ -418,7 +423,7 @@ mod tests {
node_url: "http://not-used-in-unit-tests".parse().unwrap(),
auth: None,
},
retry_pending_blocks_timeout: Duration::from_secs(60 * 4),
retry_pending_blocks_timeout: Duration::from_mins(4),
indexer_rpc_url: "ws://localhost:8779".parse().unwrap(),
initial_public_accounts: None,
initial_private_accounts: None,

View File

@ -188,7 +188,7 @@ impl RocksDBIO {
"transaction pre check failed with err {err:?}"
))
})?
.execute_check_on_state(&mut breakpoint)
.execute_check_on_state(&mut breakpoint, block.header.block_id)
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction execution failed with err {err:?}"

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = u128;
@ -19,9 +19,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.balance = account_post.balance.saturating_sub(balance_to_burn);
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
);
)
.write();
}

View File

@ -1,6 +1,6 @@
use nssa_core::program::{
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
read_nssa_inputs,
};
use risc0_zkvm::serde::to_vec;
@ -54,13 +54,14 @@ fn main() {
};
}
write_nssa_outputs_with_chained_call(
ProgramOutput::new(
instruction_words,
vec![sender_pre.clone(), recipient_pre.clone()],
vec![
AccountPostState::new(sender_pre.account),
AccountPostState::new(recipient_pre.account),
],
chained_calls,
);
)
.with_chained_calls(chained_calls)
.write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = (Option<Vec<u8>>, bool);
@ -33,5 +33,5 @@ fn main() {
AccountPostState::new(account_post)
};
write_nssa_outputs(instruction_words, vec![pre], vec![post_state]);
ProgramOutput::new(instruction_words, vec![pre], vec![post_state]).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -17,5 +17,5 @@ fn main() {
let account_post = AccountPostState::new_claimed(pre.account.clone());
write_nssa_outputs(instruction_words, vec![pre], vec![account_post]);
ProgramOutput::new(instruction_words, vec![pre], vec![account_post]).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = Vec<u8>;
@ -22,9 +22,10 @@ fn main() {
.try_into()
.expect("provided data should fit into data limit");
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new_claimed(account_post)],
);
)
.write();
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::Account,
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs},
};
type Instruction = ();
@ -14,12 +14,13 @@ fn main() {
let account_pre = pre.account.clone();
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![
AccountPostState::new(account_pre),
AccountPostState::new(Account::default()),
],
);
)
.write();
}

View File

@ -1,8 +1,7 @@
use nssa_core::{
account::AccountWithMetadata,
program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
},
};
use risc0_zkvm::serde::to_vec;
@ -40,13 +39,14 @@ fn main() {
pda_seeds: vec![],
};
write_nssa_outputs_with_chained_call(
ProgramOutput::new(
instruction_words,
vec![sender.clone(), receiver.clone()],
vec![
AccountPostState::new(sender.account),
AccountPostState::new(receiver.account),
],
vec![chained_call],
);
)
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -16,9 +16,10 @@ fn main() {
.checked_add(1)
.expect("Balance overflow");
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
);
)
.write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -11,9 +11,10 @@ fn main() {
let account_pre1 = pre1.account.clone();
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre1, pre2],
vec![AccountPostState::new(account_pre1)],
);
)
.write();
}

View File

@ -5,7 +5,7 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs},
program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs},
};
/// Initializes a default account under the ownership of this program.
@ -80,5 +80,5 @@ fn main() {
}
_ => panic!("invalid params"),
};
write_nssa_outputs(instruction_data, pre_states, post_states);
ProgramOutput::new(instruction_data, pre_states, post_states).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -13,9 +13,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.nonce.public_account_nonce_increment();
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
);
)
.write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -9,5 +9,5 @@ fn main() {
.iter()
.map(|account| AccountPostState::new(account.account.clone()))
.collect();
write_nssa_outputs(instruction_words, pre_states, post_states);
ProgramOutput::new(instruction_words, pre_states, post_states).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -13,9 +13,10 @@ fn main() {
let mut account_post = account_pre.clone();
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7];
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(account_post)],
);
)
.write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, read_nssa_inputs, write_nssa_outputs};
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = u128;
@ -26,12 +26,13 @@ fn main() {
.checked_add(balance)
.expect("Overflow when adding balance");
write_nssa_outputs(
ProgramOutput::new(
instruction_words,
vec![sender_pre, receiver_pre],
vec![
AccountPostState::new(sender_post),
AccountPostState::new(receiver_post),
],
);
)
.write();
}

View File

@ -0,0 +1,29 @@
use nssa_core::program::{
AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs,
};
type Instruction = ValidityWindow;
fn main() {
let (
ProgramInput {
pre_states,
instruction: validity_window,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let Ok([pre]) = <[_; 1]>::try_from(pre_states) else {
return;
};
let post = pre.account.clone();
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.write();
}

View File

@ -0,0 +1,42 @@
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow,
read_nssa_inputs,
};
use risc0_zkvm::serde::to_vec;
/// A program that sets a validity window on its output and chains to another program with a
/// potentially different validity window.
///
/// Instruction: (`window`, `chained_program_id`, `chained_window`)
/// The initial output uses `window` and chains to `chained_program_id` with `chained_window`.
type Instruction = (ValidityWindow, ProgramId, ValidityWindow);
fn main() {
let (
ProgramInput {
pre_states,
instruction: (validity_window, chained_program_id, chained_validity_window),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let [pre] = <[_; 1]>::try_from(pre_states.clone()).expect("Expected exactly one pre state");
let post = pre.account.clone();
let chained_instruction = to_vec(&chained_validity_window).unwrap();
let chained_call = ChainedCall {
program_id: chained_program_id,
instruction_data: chained_instruction,
pre_states,
pda_seeds: vec![],
};
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -77,9 +77,7 @@ pub unsafe extern "C" fn wallet_ffi_claim_pinata(
match block_on(pinata.claim(pinata_id, winner_id, solution)) {
Ok(tx_hash) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
(*out_result).success = true;
@ -184,8 +182,7 @@ pub unsafe extern "C" fn wallet_ffi_claim_pinata_private_owned_already_initializ
) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -270,8 +267,7 @@ pub unsafe extern "C" fn wallet_ffi_claim_pinata_private_owned_not_initialized(
match block_on(pinata.claim_private_owned_account(pinata_id, winner_id, solution)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;

View File

@ -75,8 +75,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public(
match block_on(transfer.send_public_transfer(from_id, to_id, amount)) {
Ok(tx_hash) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -165,8 +164,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -246,8 +244,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded(
match block_on(transfer.send_deshielded_transfer(from_id, to_id, amount)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -335,8 +332,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
{
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -419,8 +415,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned(
match block_on(transfer.send_shielded_transfer(from_id, to_id, amount)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -503,8 +498,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private_owned(
match block_on(transfer.send_private_transfer_to_owned_account(from_id, to_id, amount)) {
Ok((tx_hash, _shared_keys)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -575,8 +569,7 @@ pub unsafe extern "C" fn wallet_ffi_register_public_account(
match block_on(transfer.register_account(account_id)) {
Ok(tx_hash) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;
@ -647,8 +640,7 @@ pub unsafe extern "C" fn wallet_ffi_register_private_account(
match block_on(transfer.register_account_private(account_id)) {
Ok((tx_hash, _secret)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map(std::ffi::CString::into_raw)
.unwrap_or(ptr::null_mut());
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
unsafe {
(*out_result).tx_hash = tx_hash;