diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 11260b46..4c4a4eae 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 6ad55423..ec04613e 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index f3e126a4..25b90260 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 153d64fa..b7da464a 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 2f2cc820..0c26e6a2 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index bc4583b6..d708a10f 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index a00f2991..2c162dfc 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 6402d170..79826a4f 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index 3f44174a..91240905 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 20fcad17..e34786fe 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 2099a180..f44c9045 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 30fb2670..7f7612a3 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 1cf2536e..8798992a 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 908082e6..161e427c 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 439ab0fa..20ce3ac4 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index e666047e..5ff38952 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index d82d7cb2..9f63259f 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index b97a64f7..36efd493 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 760a6a17..7acb622b 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 1603437b..6177443f 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin new file mode 100644 index 00000000..ce2f40bc Binary files /dev/null and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/bedrock_client/src/lib.rs b/bedrock_client/src/lib.rs index fdd14f72..4e9bfffd 100644 --- a/bedrock_client/src/lib.rs +++ b/bedrock_client/src/lib.rs @@ -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")?; diff --git a/common/src/sequencer_client.rs b/common/src/sequencer_client.rs new file mode 100644 index 00000000..b75bbe04 --- /dev/null +++ b/common/src/sequencer_client.rs @@ -0,0 +1,361 @@ +use std::{collections::HashMap, ops::RangeInclusive}; + +use anyhow::Result; +use nssa::AccountId; +use nssa_core::program::ProgramId; +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; +use url::Url; + +use super::rpc_primitives::requests::{ + GetAccountBalanceRequest, GetAccountBalanceResponse, GetBlockDataRequest, GetBlockDataResponse, + GetGenesisIdRequest, GetGenesisIdResponse, GetInitialTestnetAccountsRequest, +}; +use crate::{ + HashType, + config::BasicAuth, + error::{SequencerClientError, SequencerRpcError}, + rpc_primitives::{ + self, + requests::{ + GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, + GetAccountsNoncesResponse, GetBlockRangeDataRequest, GetBlockRangeDataResponse, + GetInitialTestnetAccountsResponse, GetLastBlockRequest, GetLastBlockResponse, + GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, + GetProofForCommitmentResponse, GetTransactionByHashRequest, + GetTransactionByHashResponse, SendTxRequest, SendTxResponse, + }, + }, + transaction::NSSATransaction, +}; + +#[derive(Debug, Clone, Deserialize)] +struct SequencerRpcResponse { + #[serde(rename = "jsonrpc")] + _jsonrpc: String, + result: serde_json::Value, + #[serde(rename = "id")] + _id: u64, +} + +#[derive(Clone)] +pub struct SequencerClient { + pub client: reqwest::Client, + pub sequencer_addr: Url, + pub basic_auth: Option, +} + +impl SequencerClient { + pub fn new(sequencer_addr: Url) -> Result { + Self::new_with_auth(sequencer_addr, None) + } + + pub fn new_with_auth(sequencer_addr: Url, basic_auth: Option) -> Result { + Ok(Self { + client: Client::builder() + // Add more fields if needed + .timeout(std::time::Duration::from_mins(1)) + // Should be kept in sync with server keep-alive settings + .pool_idle_timeout(std::time::Duration::from_secs(5)) + .build()?, + sequencer_addr, + basic_auth, + }) + } + + pub async fn call_method_with_payload( + &self, + method: &str, + payload: Value, + ) -> Result { + let request = + rpc_primitives::message::Request::from_payload_version_2_0(method.to_owned(), payload); + + log::debug!( + "Calling method {method} with payload {request:?} to sequencer at {}", + self.sequencer_addr + ); + + let strategy = tokio_retry::strategy::FixedInterval::from_millis(10000).take(60); + + let response_vall = tokio_retry::Retry::spawn(strategy, || async { + let mut call_builder = self.client.post(self.sequencer_addr.clone()); + + if let Some(BasicAuth { username, password }) = &self.basic_auth { + call_builder = call_builder.basic_auth(username, password.as_deref()); + } + + let call_res_res = call_builder.json(&request).send().await; + + match call_res_res { + Err(err) => Err(err), + Ok(call_res) => call_res.json::().await, + } + }) + .await?; + + if let Ok(response) = serde_json::from_value::(response_vall.clone()) + { + Ok(response.result) + } else { + let err_resp = serde_json::from_value::(response_vall)?; + + Err(err_resp.into()) + } + } + + /// Get block data at `block_id` from sequencer. + pub async fn get_block( + &self, + block_id: u64, + ) -> Result { + let block_req = GetBlockDataRequest { block_id }; + + let req = serde_json::to_value(block_req)?; + + let resp = self.call_method_with_payload("get_block", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + pub async fn get_block_range( + &self, + range: RangeInclusive, + ) -> Result { + let block_req = GetBlockRangeDataRequest { + start_block_id: *range.start(), + end_block_id: *range.end(), + }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_block_range", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get last known `blokc_id` from sequencer. + pub async fn get_last_block(&self) -> Result { + let block_req = GetLastBlockRequest {}; + + let req = serde_json::to_value(block_req)?; + + let resp = self.call_method_with_payload("get_last_block", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get account public balance for `account_id`. `account_id` must be a valid hex-string for 32 + /// bytes. + pub async fn get_account_balance( + &self, + account_id: AccountId, + ) -> Result { + let block_req = GetAccountBalanceRequest { account_id }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_account_balance", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get accounts nonces for `account_ids`. `account_ids` must be a list of valid hex-strings for + /// 32 bytes. + pub async fn get_accounts_nonces( + &self, + account_ids: Vec, + ) -> Result { + let block_req = GetAccountsNoncesRequest { account_ids }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_accounts_nonces", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + pub async fn get_account( + &self, + account_id: AccountId, + ) -> Result { + let block_req = GetAccountRequest { account_id }; + + let req = serde_json::to_value(block_req)?; + + let resp = self.call_method_with_payload("get_account", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get transaction details for `hash`. + pub async fn get_transaction_by_hash( + &self, + hash: HashType, + ) -> Result { + let block_req = GetTransactionByHashRequest { hash }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_transaction_by_hash", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Send transaction to sequencer. + pub async fn send_tx_public( + &self, + transaction: nssa::PublicTransaction, + ) -> Result { + let transaction = NSSATransaction::Public(transaction); + + let tx_req = SendTxRequest { + transaction: borsh::to_vec(&transaction).unwrap(), + }; + + let req = serde_json::to_value(tx_req)?; + + let resp = self.call_method_with_payload("send_tx", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Send transaction to sequencer. + pub async fn send_tx_private( + &self, + transaction: nssa::PrivacyPreservingTransaction, + ) -> Result { + let transaction = NSSATransaction::PrivacyPreserving(transaction); + + let tx_req = SendTxRequest { + transaction: borsh::to_vec(&transaction).unwrap(), + }; + + let req = serde_json::to_value(tx_req)?; + + let resp = self.call_method_with_payload("send_tx", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get genesis id from sequencer. + pub async fn get_genesis_id(&self) -> Result { + let genesis_req = GetGenesisIdRequest {}; + + let req = serde_json::to_value(genesis_req).unwrap(); + + let resp = self + .call_method_with_payload("get_genesis", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value(resp).unwrap(); + + Ok(resp_deser) + } + + /// Get initial testnet accounts from sequencer. + pub async fn get_initial_testnet_accounts( + &self, + ) -> Result, SequencerClientError> { + let acc_req = GetInitialTestnetAccountsRequest {}; + + let req = serde_json::to_value(acc_req).unwrap(); + + let resp = self + .call_method_with_payload("get_initial_testnet_accounts", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value(resp).unwrap(); + + Ok(resp_deser) + } + + /// Get proof for commitment. + pub async fn get_proof_for_commitment( + &self, + commitment: nssa_core::Commitment, + ) -> Result, SequencerClientError> { + let acc_req = GetProofForCommitmentRequest { commitment }; + + let req = serde_json::to_value(acc_req).unwrap(); + + let resp = self + .call_method_with_payload("get_proof_for_commitment", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value::(resp) + .unwrap() + .membership_proof; + + Ok(resp_deser) + } + + pub async fn send_tx_program( + &self, + transaction: nssa::ProgramDeploymentTransaction, + ) -> Result { + let transaction = NSSATransaction::ProgramDeployment(transaction); + + let tx_req = SendTxRequest { + transaction: borsh::to_vec(&transaction).unwrap(), + }; + + let req = serde_json::to_value(tx_req)?; + + let resp = self.call_method_with_payload("send_tx", req).await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + /// Get Ids of the programs used by the node. + pub async fn get_program_ids( + &self, + ) -> Result, SequencerClientError> { + let acc_req = GetProgramIdsRequest {}; + + let req = serde_json::to_value(acc_req).unwrap(); + + let resp = self + .call_method_with_payload("get_program_ids", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value::(resp) + .unwrap() + .program_ids; + + Ok(resp_deser) + } +} diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 9563251a..1862dcc8 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -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 { 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:#?}"))?; diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs index c0fc0296..b549b1f8 100644 --- a/explorer_service/src/pages/transaction_page.rs +++ b/explorer_service/src/pages/transaction_page.rs @@ -177,11 +177,18 @@ pub fn TransactionPage() -> impl IntoView { encrypted_private_post_states, new_commitments, new_nullifiers, + validity_window } = message; let WitnessSet { signatures_and_public_keys: _, proof, } = witness_set; + let validity_window_formatted = match validity_window.0 { + (Some(start), Some(end)) => format!("from {start} to {end}"), + (Some(start), None) => format!("from {start}"), + (None, Some(end)) => format!("until {end}"), + (None, None) => "unbounded".to_owned(), + }; let proof_len = proof.map_or(0, |p| p.0.len()); view! { @@ -212,6 +219,10 @@ pub fn TransactionPage() -> impl IntoView { "Proof Size:" {format!("{proof_len} bytes")} +
+ "Validity Window:" + {validity_window_formatted} +

"Public Accounts"

diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index f3722b17..e4534f76 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -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)?; } } diff --git a/indexer/service/protocol/src/convert.rs b/indexer/service/protocol/src/convert.rs index 6114620f..ec85d7fb 100644 --- a/indexer/service/protocol/src/convert.rs +++ b/indexer/service/protocol/src/convert.rs @@ -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 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 for PrivacyPre .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + validity_window: ValidityWindow((validity_window.from(), validity_window.to())), } } } impl TryFrom 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 { let PrivacyPreservingMessage { @@ -316,6 +318,7 @@ impl TryFrom 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 for nssa::privacy_preserving_transaction: public_post_states: public_post_states .into_iter() .map(TryInto::try_into) - .collect::, _>>()?, + .collect::, _>>() + .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,10 @@ impl TryFrom for nssa::privacy_preserving_transaction: .into_iter() .map(|(n, d)| (n.into(), d.into())) .collect(), + validity_window: validity_window + .0 + .try_into() + .map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?, }) } } @@ -479,14 +487,7 @@ impl TryFrom 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()?)) } } diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index d61f62a6..a8f6da2c 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -235,6 +235,7 @@ pub struct PrivacyPreservingMessage { pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, + pub validity_window: ValidityWindow, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] @@ -300,6 +301,9 @@ pub struct Nullifier( pub [u8; 32], ); +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +pub struct ValidityWindow(pub (Option, Option)); + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct CommitmentSetDigest( #[serde(with = "base64::arr")] diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index eb6f11f7..c5891b41 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -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![], diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index 22444458..e7f12fc1 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -210,7 +210,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_accounts: initial_data.sequencer_initial_accounts(), initial_commitments: initial_data.sequencer_initial_commitments(), signing_key: [37; 32], diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 56d63022..f9cd9239 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -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, pub new_commitments: Vec, 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(); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 31b76b0f..5cd46432 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,5 +1,7 @@ use std::collections::HashSet; +#[cfg(feature = "host")] +use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; @@ -151,6 +153,70 @@ impl AccountPostState { } } +pub type BlockId = u64; + +#[derive(Serialize, Deserialize, Clone, Copy)] +#[cfg_attr( + any(feature = "host", test), + derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize) +)] +pub struct ValidityWindow { + from: Option, + to: Option, +} + +impl ValidityWindow { + #[must_use] + pub const fn new_unbounded() -> Self { + Self { + from: None, + to: None, + } + } + + /// Valid for block IDs in the range [from, to), where `from` is included and `to` is excluded. + #[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) + } + + 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(()) + } + } + + #[must_use] + pub const fn from(&self) -> Option { + self.from + } + + #[must_use] + pub const fn to(&self) -> Option { + self.to + } +} +impl TryFrom<(Option, Option)> for ValidityWindow { + type Error = InvalidWindow; + + fn try_from(value: (Option, Option)) -> Result { + let this = Self { + from: value.0, + to: value.1, + }; + this.check_window()?; + Ok(this) + } +} + +#[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))] pub struct ProgramOutput { @@ -158,8 +224,53 @@ pub struct ProgramOutput { pub instruction_data: InstructionData, /// The account pre states the program received to produce this output. pub pre_states: Vec, + /// The account post states the program execution produced. pub post_states: Vec, + /// The list of chained calls to other programs. pub chained_calls: Vec, + /// The window where the program output is valid. + /// Valid for block IDs in the range [from, to), where `from` is included and `to` is excluded. + /// `None` means unbounded on that side. + pub validity_window: ValidityWindow, +} + +impl ProgramOutput { + #[must_use] + pub const fn new( + instruction_data: InstructionData, + pre_states: Vec, + post_states: Vec, + ) -> Self { + Self { + instruction_data, + pre_states, + post_states, + chained_calls: Vec::new(), + validity_window: ValidityWindow::new_unbounded(), + } + } + + pub fn write(self) { + env::commit(&self); + } + + #[must_use] + pub fn with_chained_calls(mut self, chained_calls: Vec) -> Self { + self.chained_calls = chained_calls; + self + } + + pub fn valid_from_id(mut self, id: Option) -> Result { + self.validity_window.from = id; + self.validity_window.check_window()?; + Ok(self) + } + + pub fn valid_until_id(mut self, id: Option) -> Result { + self.validity_window.to = id; + self.validity_window.check_window()?; + Ok(self) + } } /// Representation of a number as `lo + hi * 2^128`. @@ -224,13 +335,7 @@ pub fn write_nssa_outputs( pre_states: Vec, post_states: Vec, ) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls: Vec::new(), - }; - env::commit(&output); + ProgramOutput::new(instruction_data, pre_states, post_states).write(); } pub fn write_nssa_outputs_with_chained_call( @@ -239,13 +344,9 @@ pub fn write_nssa_outputs_with_chained_call( post_states: Vec, chained_calls: Vec, ) { - let output = ProgramOutput { - instruction_data, - pre_states, - post_states, - chained_calls, - }; - env::commit(&output); + ProgramOutput::new(instruction_data, pre_states, post_states) + .with_chained_calls(chained_calls) + .write(); } /// Validates well-behaved program execution. diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 3576b366..15d4f044 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -69,6 +69,9 @@ pub enum NssaError { #[error("Max account nonce reached")] MaxAccountNonceReached, + + #[error("Execution outside of the validity window")] + OutOfValidityWindow, } #[cfg(test)] diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 02c7a614..755b54f3 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -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, pub new_commitments: Vec, 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(), } } diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index aafbe0cb..b1c30109 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -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, 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) diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 3b372a22..fa5e7b42 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -284,6 +284,14 @@ 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() + } } #[test] diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 8151f8cf..8aaf039e 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -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, 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(_)))); } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 6dec4f35..7f2a0ec8 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -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::{ @@ -157,8 +157,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, @@ -181,9 +182,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(); @@ -338,7 +340,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}, }; use crate::{ @@ -373,6 +375,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 } @@ -567,7 +570,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); @@ -588,7 +591,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); @@ -612,7 +615,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); @@ -632,10 +635,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); @@ -657,7 +660,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))); } @@ -674,7 +677,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))); } @@ -691,7 +694,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))); } @@ -715,7 +718,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))); } @@ -739,7 +742,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))); } @@ -763,7 +766,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))); } @@ -787,7 +790,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))); } @@ -815,7 +818,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))); } @@ -840,7 +843,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))); } @@ -858,7 +861,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))); } @@ -887,7 +890,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))); } @@ -1080,7 +1083,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()); @@ -1150,7 +1153,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); @@ -1214,7 +1217,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()); @@ -2142,7 +2145,7 @@ pub mod tests { ); state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1) .unwrap(); let sender_private_account = Account { @@ -2160,7 +2163,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 { @@ -2237,7 +2240,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); @@ -2280,7 +2283,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); @@ -2320,7 +2323,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) @@ -2361,7 +2364,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); @@ -2417,7 +2420,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); @@ -2526,7 +2529,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 @@ -2582,7 +2585,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; @@ -2595,7 +2598,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; @@ -2612,7 +2615,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!( @@ -2642,7 +2645,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))); } @@ -2688,7 +2691,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); @@ -2757,7 +2760,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()); @@ -2810,7 +2813,7 @@ pub mod tests { // Claim should succeed assert!( state - .transition_from_privacy_preserving_transaction(&tx) + .transition_from_privacy_preserving_transaction(&tx, 1) .is_ok() ); @@ -2859,7 +2862,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()); @@ -2884,7 +2887,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))); @@ -2996,6 +2999,117 @@ 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, Option), + block_id: BlockId, + ) { + 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 { + (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, Option), + block_id: BlockId, + ) { + 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 { + (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]); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 99782d7f..08872564 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -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, post_states: HashMap, + 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) -> Self { + let valid_from_id = program_outputs + .iter() + .filter_map(|output| output.validity_window.from()) + .max(); + let valid_until_id = program_outputs + .iter() + .filter_map(|output| output.validity_window.to()) + .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(); diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index d5c8e7b0..86fdb4ff 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -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()); diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 21e63740..cd20467b 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -147,10 +147,12 @@ impl SequencerCore Result { 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), @@ -184,10 +186,7 @@ impl SequencerCore 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![]; @@ -334,6 +333,12 @@ impl SequencerCore 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. @@ -406,7 +411,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(), } } diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs new file mode 100644 index 00000000..2fe9bed1 --- /dev/null +++ b/sequencer_rpc/src/process.rs @@ -0,0 +1,786 @@ +use std::collections::HashMap; + +use actix_web::Error as HttpError; +use base64::{Engine as _, engine::general_purpose}; +use common::{ + block::{AccountInitialData, HashableBlockData}, + rpc_primitives::{ + errors::RpcError, + message::{Message, Request}, + parser::RpcRequest as _, + requests::{ + GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountRequest, + GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, + GetBlockDataRequest, GetBlockDataResponse, GetBlockRangeDataRequest, + GetBlockRangeDataResponse, GetGenesisIdRequest, GetGenesisIdResponse, + GetInitialTestnetAccountsRequest, GetLastBlockRequest, GetLastBlockResponse, + GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, + GetProofForCommitmentResponse, GetTransactionByHashRequest, + GetTransactionByHashResponse, HelloRequest, HelloResponse, SendTxRequest, + SendTxResponse, + }, + }, + transaction::{NSSATransaction, TransactionMalformationError}, +}; +use itertools::Itertools as _; +use log::warn; +use nssa::{self, program::Program}; +use sequencer_core::{ + block_settlement_client::BlockSettlementClientTrait, indexer_client::IndexerClientTrait, +}; +use serde_json::Value; + +use super::{JsonHandler, respond, types::err_rpc::RpcErr}; + +pub const HELLO: &str = "hello"; +pub const SEND_TX: &str = "send_tx"; +pub const GET_BLOCK: &str = "get_block"; +pub const GET_BLOCK_RANGE: &str = "get_block_range"; +pub const GET_GENESIS: &str = "get_genesis"; +pub const GET_LAST_BLOCK: &str = "get_last_block"; +pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance"; +pub const GET_TRANSACTION_BY_HASH: &str = "get_transaction_by_hash"; +pub const GET_ACCOUNTS_NONCES: &str = "get_accounts_nonces"; +pub const GET_ACCOUNT: &str = "get_account"; +pub const GET_PROOF_FOR_COMMITMENT: &str = "get_proof_for_commitment"; +pub const GET_PROGRAM_IDS: &str = "get_program_ids"; + +pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER"; + +pub const TRANSACTION_SUBMITTED: &str = "Transaction submitted"; + +pub const GET_INITIAL_TESTNET_ACCOUNTS: &str = "get_initial_testnet_accounts"; + +pub trait Process: Send + Sync + 'static { + fn process(&self, message: Message) -> impl Future> + Send; +} + +impl< + BC: BlockSettlementClientTrait + Send + Sync + 'static, + IC: IndexerClientTrait + Send + Sync + 'static, +> Process for JsonHandler +{ + async fn process(&self, message: Message) -> Result { + let id = message.id(); + if let Message::Request(request) = message { + let message_inner = self + .process_request_internal(request) + .await + .map_err(|e| e.0); + Ok(Message::response(id, message_inner)) + } else { + Ok(Message::error(RpcError::parse_error( + "JSON RPC Request format was expected".to_owned(), + ))) + } + } +} + +impl JsonHandler { + /// Example of request processing. + fn process_temp_hello(request: Request) -> Result { + let _hello_request = HelloRequest::parse(Some(request.params))?; + + let response = HelloResponse { + greeting: HELLO_FROM_SEQUENCER.to_owned(), + }; + + respond(response) + } + + async fn process_send_tx(&self, request: Request) -> Result { + // Check transaction size against block size limit + // Reserve ~200 bytes for block header overhead + const BLOCK_HEADER_OVERHEAD: usize = 200; + + let send_tx_req = SendTxRequest::parse(Some(request.params))?; + let tx = borsh::from_slice::(&send_tx_req.transaction).unwrap(); + + let tx_hash = tx.hash(); + + let tx_size = send_tx_req.transaction.len(); + + let max_tx_size = self.max_block_size.saturating_sub(BLOCK_HEADER_OVERHEAD); + + if tx_size > max_tx_size { + return Err(TransactionMalformationError::TransactionTooLarge { + size: tx_size, + max: max_tx_size, + } + .into()); + } + + let authenticated_tx = tx + .transaction_stateless_check() + .inspect_err(|err| warn!("Error at pre_check {err:#?}"))?; + + // TODO: Do we need a timeout here? It will be usable if we have too many transactions to + // process + self.mempool_handle + .push(authenticated_tx) + .await + .expect("Mempool is closed, this is a bug"); + + let response = SendTxResponse { + status: TRANSACTION_SUBMITTED.to_owned(), + tx_hash, + }; + + respond(response) + } + + async fn process_get_block_data(&self, request: Request) -> Result { + let get_block_req = GetBlockDataRequest::parse(Some(request.params))?; + + let block = { + let state = self.sequencer_state.lock().await; + + state + .block_store() + .get_block_at_id(get_block_req.block_id)? + }; + + let response = GetBlockDataResponse { + block: borsh::to_vec(&HashableBlockData::from(block)).unwrap(), + }; + + respond(response) + } + + async fn process_get_block_range_data(&self, request: Request) -> Result { + let get_block_req = GetBlockRangeDataRequest::parse(Some(request.params))?; + + let blocks = { + let state = self.sequencer_state.lock().await; + (get_block_req.start_block_id..=get_block_req.end_block_id) + .map(|block_id| state.block_store().get_block_at_id(block_id)) + .map_ok(|block| { + borsh::to_vec(&HashableBlockData::from(block)) + .expect("derived BorshSerialize should never fail") + }) + .collect::, _>>()? + }; + + let response = GetBlockRangeDataResponse { blocks }; + + respond(response) + } + + async fn process_get_genesis(&self, request: Request) -> Result { + let _get_genesis_req = GetGenesisIdRequest::parse(Some(request.params))?; + + let genesis_id = { + let state = self.sequencer_state.lock().await; + + state.block_store().genesis_id() + }; + + let response = GetGenesisIdResponse { genesis_id }; + + respond(response) + } + + async fn process_get_last_block(&self, request: Request) -> Result { + let _get_last_block_req = GetLastBlockRequest::parse(Some(request.params))?; + + let last_block = { + let state = self.sequencer_state.lock().await; + + state.chain_height() + }; + + let response = GetLastBlockResponse { last_block }; + + respond(response) + } + + /// Returns the initial accounts for testnet. + /// `ToDo`: Useful only for testnet and needs to be removed later. + async fn get_initial_testnet_accounts(&self, request: Request) -> Result { + let _get_initial_testnet_accounts_request = + GetInitialTestnetAccountsRequest::parse(Some(request.params))?; + + let initial_accounts: Vec = { + let state = self.sequencer_state.lock().await; + + state.sequencer_config().initial_accounts.clone() + }; + + respond(initial_accounts) + } + + /// Returns the balance of the account at the given `account_id`. + /// The `account_id` must be a valid hex string of the correct length. + async fn process_get_account_balance(&self, request: Request) -> Result { + let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; + let account_id = get_account_req.account_id; + + let balance = { + let state = self.sequencer_state.lock().await; + let account = state.state().get_account_by_id(account_id); + account.balance + }; + + let response = GetAccountBalanceResponse { balance }; + + respond(response) + } + + /// Returns the nonces of the accounts at the given `account_ids`. + /// Each `account_id` must be a valid hex string of the correct length. + async fn process_get_accounts_nonces(&self, request: Request) -> Result { + let get_account_nonces_req = GetAccountsNoncesRequest::parse(Some(request.params))?; + let account_ids = get_account_nonces_req.account_ids; + + let nonces = { + let state = self.sequencer_state.lock().await; + + account_ids + .into_iter() + .map(|account_id| state.state().get_account_by_id(account_id).nonce.0) + .collect() + }; + + let response = GetAccountsNoncesResponse { nonces }; + + respond(response) + } + + /// Returns account struct for given `account_id`. + /// `AccountId` must be a valid hex string of the correct length. + async fn process_get_account(&self, request: Request) -> Result { + let get_account_nonces_req = GetAccountRequest::parse(Some(request.params))?; + + let account_id = get_account_nonces_req.account_id; + + let account = { + let state = self.sequencer_state.lock().await; + + state.state().get_account_by_id(account_id) + }; + + let response = GetAccountResponse { account }; + + respond(response) + } + + /// Returns the transaction corresponding to the given hash, if it exists in the blockchain. + /// The hash must be a valid hex string of the correct length. + async fn process_get_transaction_by_hash(&self, request: Request) -> Result { + let get_transaction_req = GetTransactionByHashRequest::parse(Some(request.params))?; + let hash = get_transaction_req.hash; + + let transaction = { + let state = self.sequencer_state.lock().await; + state + .block_store() + .get_transaction_by_hash(hash) + .map(|tx| borsh::to_vec(&tx).unwrap()) + }; + let base64_encoded = transaction.map(|tx| general_purpose::STANDARD.encode(tx)); + let response = GetTransactionByHashResponse { + transaction: base64_encoded, + }; + respond(response) + } + + /// Returns the commitment proof, corresponding to commitment. + async fn process_get_proof_by_commitment(&self, request: Request) -> Result { + let get_proof_req = GetProofForCommitmentRequest::parse(Some(request.params))?; + + let membership_proof = { + let state = self.sequencer_state.lock().await; + state + .state() + .get_proof_for_commitment(&get_proof_req.commitment) + }; + let response = GetProofForCommitmentResponse { membership_proof }; + respond(response) + } + + fn process_get_program_ids(request: Request) -> Result { + let _get_proof_req = GetProgramIdsRequest::parse(Some(request.params))?; + + let mut program_ids = HashMap::new(); + program_ids.insert( + "authenticated_transfer".to_owned(), + Program::authenticated_transfer_program().id(), + ); + program_ids.insert("token".to_owned(), Program::token().id()); + program_ids.insert("pinata".to_owned(), Program::pinata().id()); + program_ids.insert("amm".to_owned(), Program::amm().id()); + program_ids.insert( + "privacy_preserving_circuit".to_owned(), + nssa::PRIVACY_PRESERVING_CIRCUIT_ID, + ); + let response = GetProgramIdsResponse { program_ids }; + respond(response) + } + + pub async fn process_request_internal(&self, request: Request) -> Result { + match request.method.as_ref() { + HELLO => Self::process_temp_hello(request), + SEND_TX => self.process_send_tx(request).await, + GET_BLOCK => self.process_get_block_data(request).await, + GET_BLOCK_RANGE => self.process_get_block_range_data(request).await, + GET_GENESIS => self.process_get_genesis(request).await, + GET_LAST_BLOCK => self.process_get_last_block(request).await, + GET_INITIAL_TESTNET_ACCOUNTS => self.get_initial_testnet_accounts(request).await, + GET_ACCOUNT_BALANCE => self.process_get_account_balance(request).await, + GET_ACCOUNTS_NONCES => self.process_get_accounts_nonces(request).await, + GET_ACCOUNT => self.process_get_account(request).await, + GET_TRANSACTION_BY_HASH => self.process_get_transaction_by_hash(request).await, + GET_PROOF_FOR_COMMITMENT => self.process_get_proof_by_commitment(request).await, + GET_PROGRAM_IDS => Self::process_get_program_ids(request), + _ => Err(RpcErr(RpcError::method_not_found(request.method))), + } + } +} + +#[cfg(test)] +mod tests { + use std::{str::FromStr as _, sync::Arc, time::Duration}; + + use base58::ToBase58 as _; + use base64::{Engine as _, engine::general_purpose}; + use bedrock_client::BackoffConfig; + use common::{ + block::AccountInitialData, config::BasicAuth, test_utils::sequencer_sign_key_for_testing, + transaction::NSSATransaction, + }; + use nssa::AccountId; + use sequencer_core::{ + config::{BedrockConfig, SequencerConfig}, + mock::{MockBlockSettlementClient, MockIndexerClient, SequencerCoreWithMockClients}, + }; + use serde_json::Value; + use tempfile::tempdir; + use tokio::sync::Mutex; + + use crate::rpc_handler; + + type JsonHandlerWithMockClients = + crate::JsonHandler; + + fn sequencer_config_for_tests() -> SequencerConfig { + let tempdir = tempdir().unwrap(); + let home = tempdir.path().to_path_buf(); + let acc1_id: Vec = vec![ + 148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, 112, 83, + 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102, + ]; + + let acc2_id: Vec = vec![ + 30, 145, 107, 3, 207, 73, 192, 230, 160, 63, 238, 207, 18, 69, 54, 216, 103, 244, 92, + 94, 124, 248, 42, 16, 141, 19, 119, 18, 14, 226, 140, 204, + ]; + + let initial_acc1 = AccountInitialData { + account_id: AccountId::from_str(&acc1_id.to_base58()).unwrap(), + balance: 10000, + }; + + let initial_acc2 = AccountInitialData { + account_id: AccountId::from_str(&acc2_id.to_base58()).unwrap(), + balance: 20000, + }; + + let initial_accounts = vec![initial_acc1, initial_acc2]; + + SequencerConfig { + home, + override_rust_log: Some("info".to_owned()), + genesis_id: 1, + is_genesis_random: false, + max_num_tx_in_block: 10, + max_block_size: bytesize::ByteSize::mib(1), + mempool_max_size: 1000, + block_create_timeout: Duration::from_secs(1), + port: 8080, + initial_accounts, + initial_commitments: vec![], + signing_key: *sequencer_sign_key_for_testing().value(), + retry_pending_blocks_timeout: Duration::from_mins(4), + bedrock_config: BedrockConfig { + backoff: BackoffConfig { + start_delay: Duration::from_millis(100), + max_retries: 5, + }, + channel_id: [42; 32].into(), + node_url: "http://localhost:8080".parse().unwrap(), + auth: Some(BasicAuth { + username: "user".to_owned(), + password: None, + }), + }, + indexer_rpc_url: "ws://localhost:8779".parse().unwrap(), + } + } + + async fn components_for_tests() -> ( + JsonHandlerWithMockClients, + Vec, + NSSATransaction, + ) { + let config = sequencer_config_for_tests(); + + let (mut sequencer_core, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + let initial_accounts = sequencer_core.sequencer_config().initial_accounts.clone(); + + let signing_key = nssa::PrivateKey::try_new([1; 32]).unwrap(); + let balance_to_move = 10; + let tx = common::test_utils::create_transaction_native_token_transfer( + AccountId::from_str( + &[ + 148, 179, 206, 253, 199, 51, 82, 86, 232, 2, 152, 122, 80, 243, 54, 207, 237, + 112, 83, 153, 44, 59, 204, 49, 128, 84, 160, 227, 216, 149, 97, 102, + ] + .to_base58(), + ) + .unwrap(), + 0, + AccountId::from_str(&[2; 32].to_base58()).unwrap(), + balance_to_move, + &signing_key, + ); + + mempool_handle + .push(tx.clone()) + .await + .expect("Mempool is closed, this is a bug"); + + sequencer_core + .produce_new_block_with_mempool_transactions() + .unwrap(); + + let max_block_size = + usize::try_from(sequencer_core.sequencer_config().max_block_size.as_u64()) + .expect("`max_block_size` is expected to fit in usize"); + let sequencer_core = Arc::new(Mutex::new(sequencer_core)); + + ( + JsonHandlerWithMockClients { + sequencer_state: sequencer_core, + mempool_handle, + max_block_size, + }, + initial_accounts, + tx, + ) + } + + async fn call_rpc_handler_with_json( + handler: JsonHandlerWithMockClients, + request_json: Value, + ) -> Value { + use actix_web::{App, test, web}; + + let app = test::init_service(App::new().app_data(web::Data::new(handler)).route( + "/", + web::post().to(rpc_handler::), + )) + .await; + + let req = test::TestRequest::post() + .uri("/") + .set_json(request_json) + .to_request(); + + let resp = test::call_service(&app, req).await; + let body = test::read_body(resp).await; + + serde_json::from_slice(&body).unwrap() + } + + #[actix_web::test] + async fn get_account_balance_for_non_existent_account() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "account_id": "11".repeat(16) }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "balance": 0 + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_account_balance_for_invalid_base58() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "account_id": "not_a_valid_base58" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "cause": { + "info": { + "error_message": "Failed parsing args: invalid base58: InvalidBase58Character('_', 3)" + }, + "name": "PARSE_ERROR" + }, + "code": -32700, + "data": "Failed parsing args: invalid base58: InvalidBase58Character('_', 3)", + "message": "Parse error", + "name": "REQUEST_VALIDATION_ERROR" + }, + }); + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_account_balance_for_invalid_length() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "account_id": "cafecafe" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "cause": { + "info": { + "error_message": "Failed parsing args: invalid length: expected 32 bytes, got 6" + }, + "name": "PARSE_ERROR" + }, + "code": -32700, + "data": "Failed parsing args: invalid length: expected 32 bytes, got 6", + "message": "Parse error", + "name": "REQUEST_VALIDATION_ERROR" + }, + }); + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_account_balance_for_existing_account() { + let (json_handler, initial_accounts, _) = components_for_tests().await; + + let acc1_id = initial_accounts[0].account_id; + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "account_id": acc1_id }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "balance": 10000 - 10 + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_accounts_nonces_for_non_existent_account() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_accounts_nonces", + "params": { "account_ids": ["11".repeat(16)] }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "nonces": [ 0 ] + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_accounts_nonces_for_existent_account() { + let (json_handler, initial_accounts, _) = components_for_tests().await; + + let acc1_id = initial_accounts[0].account_id; + let acc2_id = initial_accounts[1].account_id; + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_accounts_nonces", + "params": { "account_ids": [acc1_id, acc2_id] }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "nonces": [ 1, 0 ] + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_account_data_for_non_existent_account() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account", + "params": { "account_id": "11".repeat(16) }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "account": { + "balance": 0, + "nonce": 0, + "program_owner": [ 0, 0, 0, 0, 0, 0, 0, 0], + "data": [], + } + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_transaction_by_hash_for_non_existent_hash() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_transaction_by_hash", + "params": { "hash": "cafe".repeat(16) }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "transaction": null + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_transaction_by_hash_for_invalid_hex() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_transaction_by_hash", + "params": { "hash": "not_a_valid_hex" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "cause": { + "info": { + "error_message": "Failed parsing args: Odd number of digits" + }, + "name": "PARSE_ERROR" + }, + "code": -32700, + "data": "Failed parsing args: Odd number of digits", + "message": "Parse error", + "name": "REQUEST_VALIDATION_ERROR" + }, + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_transaction_by_hash_for_invalid_length() { + let (json_handler, _, _) = components_for_tests().await; + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_transaction_by_hash", + "params": { "hash": "cafecafe" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "cause": { + "info": { + "error_message": "Failed parsing args: Invalid string length" + }, + "name": "PARSE_ERROR" + }, + "code": -32700, + "data": "Failed parsing args: Invalid string length", + "message": "Parse error", + "name": "REQUEST_VALIDATION_ERROR" + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn get_transaction_by_hash_for_existing_transaction() { + let (json_handler, _, tx) = components_for_tests().await; + let tx_hash_hex = hex::encode(tx.hash()); + let expected_base64_encoded = general_purpose::STANDARD.encode(borsh::to_vec(&tx).unwrap()); + + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_transaction_by_hash", + "params": { "hash": tx_hash_hex}, + "id": 1 + }); + + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "transaction": expected_base64_encoded, + } + }); + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } +} diff --git a/storage/src/indexer.rs b/storage/src/indexer.rs index 8eb40ef4..07ceba8a 100644 --- a/storage/src/indexer.rs +++ b/storage/src/indexer.rs @@ -677,7 +677,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:?}" diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs new file mode 100644 index 00000000..03f31073 --- /dev/null +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -0,0 +1,33 @@ +use nssa_core::program::{ + AccountPostState, BlockId, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +type Instruction = (Option, Option); + +fn main() { + let ( + ProgramInput { + pre_states, + instruction: (from_id, until_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let post = pre.account.clone(); + + let output = ProgramOutput::new( + instruction_words, + vec![pre], + vec![AccountPostState::new(post)], + ) + .valid_from_id(from_id) + .unwrap() + .valid_until_id(until_id) + .unwrap(); + + output.write(); +} diff --git a/wallet-ffi/src/pinata.rs b/wallet-ffi/src/pinata.rs index 7ec2fc48..5807db7b 100644 --- a/wallet-ffi/src/pinata.rs +++ b/wallet-ffi/src/pinata.rs @@ -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; diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 5b1e27d2..739832ae 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -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;