mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-03-24 03:03:09 +00:00
Merge 414abe32ba4b86b3e1c5819bca0dcc1ed1c33ebe into fb083ce91ec10487fc17137a48c47f4322f9c768
This commit is contained in:
commit
1e6c56e1e7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/validity_window.bin
Normal file
BIN
artifacts/test_program_methods/validity_window.bin
Normal file
Binary file not shown.
@ -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")?;
|
||||
|
||||
|
||||
361
common/src/sequencer_client.rs
Normal file
361
common/src/sequencer_client.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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:#?}"))?;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()?))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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![],
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -69,6 +69,9 @@ pub enum NssaError {
|
||||
|
||||
#[error("Max account nonce reached")]
|
||||
MaxAccountNonceReached,
|
||||
|
||||
#[error("Execution outside of the validity window")]
|
||||
OutOfValidityWindow,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
786
sequencer_rpc/src/process.rs
Normal file
786
sequencer_rpc/src/process.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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:?}"
|
||||
|
||||
33
test_program_methods/guest/src/bin/validity_window.rs
Normal file
33
test_program_methods/guest/src/bin/validity_window.rs
Normal 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();
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user