Merge 414abe32ba4b86b3e1c5819bca0dcc1ed1c33ebe into fb083ce91ec10487fc17137a48c47f4322f9c768

This commit is contained in:
Sergio Chouhy 2026-03-24 02:40:35 +05:30 committed by GitHub
commit 1e6c56e1e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1584 additions and 123 deletions

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

@ -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<BasicAuth>,
}
impl SequencerClient {
pub fn new(sequencer_addr: Url) -> Result<Self> {
Self::new_with_auth(sequencer_addr, None)
}
pub fn new_with_auth(sequencer_addr: Url, basic_auth: Option<BasicAuth>) -> Result<Self> {
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<Value, SequencerClientError> {
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::<Value>().await,
}
})
.await?;
if let Ok(response) = serde_json::from_value::<SequencerRpcResponse>(response_vall.clone())
{
Ok(response.result)
} else {
let err_resp = serde_json::from_value::<SequencerRpcError>(response_vall)?;
Err(err_resp.into())
}
}
/// Get block data at `block_id` from sequencer.
pub async fn get_block(
&self,
block_id: u64,
) -> Result<GetBlockDataResponse, SequencerClientError> {
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<u64>,
) -> Result<GetBlockRangeDataResponse, SequencerClientError> {
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<GetLastBlockResponse, SequencerClientError> {
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<GetAccountBalanceResponse, SequencerClientError> {
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<AccountId>,
) -> Result<GetAccountsNoncesResponse, SequencerClientError> {
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<GetAccountResponse, SequencerClientError> {
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<GetTransactionByHashResponse, SequencerClientError> {
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<SendTxResponse, SequencerClientError> {
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<SendTxResponse, SequencerClientError> {
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<GetGenesisIdResponse, SequencerClientError> {
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<Vec<GetInitialTestnetAccountsResponse>, 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<Option<nssa_core::MembershipProof>, 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::<GetProofForCommitmentResponse>(resp)
.unwrap()
.membership_proof;
Ok(resp_deser)
}
pub async fn send_tx_program(
&self,
transaction: nssa::ProgramDeploymentTransaction,
) -> Result<SendTxResponse, SequencerClientError> {
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<HashMap<String, ProgramId>, 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::<GetProgramIdsResponse>(resp)
.unwrap()
.program_ids;
Ok(resp_deser)
}
}

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

@ -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 {
<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_formatted}</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: ValidityWindow((validity_window.from(), validity_window.to())),
}
}
}
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,10 @@ impl TryFrom<PrivacyPreservingMessage> 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<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()?))
}
}

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,9 @@ pub struct Nullifier(
pub [u8; 32],
);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct ValidityWindow(pub (Option<BlockId>, Option<BlockId>));
#[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

@ -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],

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(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<BlockId>,
to: Option<BlockId>,
}
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<BlockId> {
self.from
}
#[must_use]
pub const fn to(&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)
}
}
#[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<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.
/// 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<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);
}
#[must_use]
pub fn with_chained_calls(mut self, chained_calls: Vec<ChainedCall>) -> Self {
self.chained_calls = chained_calls;
self
}
pub fn valid_from_id(mut self, id: Option<BlockId>) -> Result<Self, InvalidWindow> {
self.validity_window.from = id;
self.validity_window.check_window()?;
Ok(self)
}
pub fn valid_until_id(mut self, id: Option<BlockId>) -> Result<Self, InvalidWindow> {
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<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
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<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
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.

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

@ -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

@ -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]

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::{
@ -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<BlockId>, Option<BlockId>),
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<BlockId>, Option<BlockId>),
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]);

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.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();

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

@ -147,10 +147,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),
@ -184,10 +186,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![];
@ -334,6 +333,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.
@ -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(),
}
}

View File

@ -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<Output = Result<Message, HttpError>> + Send;
}
impl<
BC: BlockSettlementClientTrait + Send + Sync + 'static,
IC: IndexerClientTrait + Send + Sync + 'static,
> Process for JsonHandler<BC, IC>
{
async fn process(&self, message: Message) -> Result<Message, HttpError> {
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<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> JsonHandler<BC, IC> {
/// Example of request processing.
fn process_temp_hello(request: Request) -> Result<Value, RpcErr> {
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<Value, RpcErr> {
// 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::<NSSATransaction>(&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<Value, RpcErr> {
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<Value, RpcErr> {
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::<Result<Vec<_>, _>>()?
};
let response = GetBlockRangeDataResponse { blocks };
respond(response)
}
async fn process_get_genesis(&self, request: Request) -> Result<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
let _get_initial_testnet_accounts_request =
GetInitialTestnetAccountsRequest::parse(Some(request.params))?;
let initial_accounts: Vec<AccountInitialData> = {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<Value, RpcErr> {
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<MockBlockSettlementClient, MockIndexerClient>;
fn sequencer_config_for_tests() -> SequencerConfig {
let tempdir = tempdir().unwrap();
let home = tempdir.path().to_path_buf();
let acc1_id: Vec<u8> = 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<u8> = 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<AccountInitialData>,
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::<JsonHandlerWithMockClients>),
))
.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);
}
}

View File

@ -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:?}"

View File

@ -0,0 +1,33 @@
use nssa_core::program::{
AccountPostState, BlockId, ProgramInput, ProgramOutput, read_nssa_inputs,
};
type Instruction = (Option<BlockId>, Option<BlockId>);
fn main() {
let (
ProgramInput {
pre_states,
instruction: (from_id, until_id),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
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();
}

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;