mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-08 12:13:24 +00:00
merge main into feat-caller-program-id-and-flash-swap
This commit is contained in:
parent
7d465dded7
commit
b22a989fbc
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -1462,6 +1462,14 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "clock_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"nssa_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
@ -1511,6 +1519,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"borsh",
|
||||
"clock_core",
|
||||
"hex",
|
||||
"log",
|
||||
"logos-blockchain-common-http-client",
|
||||
@ -5259,6 +5268,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"borsh",
|
||||
"clock_core",
|
||||
"env_logger",
|
||||
"hex",
|
||||
"hex-literal 1.1.0",
|
||||
@ -5897,6 +5907,7 @@ dependencies = [
|
||||
"amm_program",
|
||||
"ata_core",
|
||||
"ata_program",
|
||||
"clock_core",
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
@ -7151,6 +7162,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"storage",
|
||||
"tempfile",
|
||||
"test_program_methods",
|
||||
"testnet_initial_state",
|
||||
"tokio",
|
||||
"url",
|
||||
@ -7831,6 +7843,7 @@ dependencies = [
|
||||
name = "test_programs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clock_core",
|
||||
"nssa_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
|
||||
@ -15,6 +15,7 @@ members = [
|
||||
"nssa/core",
|
||||
"programs/amm/core",
|
||||
"programs/amm",
|
||||
"programs/clock/core",
|
||||
"programs/token/core",
|
||||
"programs/token",
|
||||
"programs/associated_token_account/core",
|
||||
@ -56,6 +57,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
|
||||
indexer_service_rpc = { path = "indexer/service/rpc" }
|
||||
wallet = { path = "wallet" }
|
||||
wallet-ffi = { path = "wallet-ffi", default-features = false }
|
||||
clock_core = { path = "programs/clock/core" }
|
||||
token_core = { path = "programs/token/core" }
|
||||
token_program = { path = "programs/token" }
|
||||
amm_core = { path = "programs/amm/core" }
|
||||
|
||||
Binary file not shown.
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/clock_chain_caller.bin
Normal file
BIN
artifacts/test_program_methods/clock_chain_caller.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
Binary file not shown.
@ -10,6 +10,7 @@ workspace = true
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{BlockId, Timestamp};
|
||||
use nssa_core::BlockId;
|
||||
pub use nssa_core::Timestamp;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
|
||||
|
||||
use crate::{HashType, transaction::NSSATransaction};
|
||||
|
||||
pub type MantleMsgId = [u8; 32];
|
||||
pub type BlockHash = HashType;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use log::warn;
|
||||
use nssa::{AccountId, V03State};
|
||||
use nssa::{AccountId, V03State, ValidatedStateDiff};
|
||||
use nssa_core::{BlockId, Timestamp};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -66,21 +66,53 @@ impl NSSATransaction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the transaction against the current state and returns the resulting diff
|
||||
/// without applying it. Rejects transactions that modify clock system accounts.
|
||||
pub fn validate_on_state(
|
||||
&self,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
|
||||
let diff = match self {
|
||||
Self::Public(tx) => {
|
||||
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
|
||||
}
|
||||
Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction(
|
||||
tx, state, block_id, timestamp,
|
||||
),
|
||||
Self::ProgramDeployment(tx) => {
|
||||
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
|
||||
}
|
||||
}?;
|
||||
|
||||
let public_diff = diff.public_diff();
|
||||
let touches_clock = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().any(|id| {
|
||||
public_diff
|
||||
.get(id)
|
||||
.is_some_and(|post| *post != state.get_account_by_id(*id))
|
||||
});
|
||||
if touches_clock {
|
||||
return Err(nssa::error::NssaError::InvalidInput(
|
||||
"Transaction modifies system clock accounts".into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Validates the transaction against the current state, rejects modifications to clock
|
||||
/// system accounts, and applies the resulting diff to the state.
|
||||
pub fn execute_check_on_state(
|
||||
self,
|
||||
state: &mut V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, nssa::error::NssaError> {
|
||||
match &self {
|
||||
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp),
|
||||
Self::PrivacyPreserving(tx) => {
|
||||
state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp)
|
||||
}
|
||||
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
|
||||
}
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
|
||||
let diff = self
|
||||
.validate_on_state(state, block_id, timestamp)
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
@ -121,3 +153,20 @@ pub enum TransactionMalformationError {
|
||||
#[error("Transaction size {size} exceeds maximum allowed size of {max} bytes")]
|
||||
TransactionTooLarge { size: usize, max: usize },
|
||||
}
|
||||
|
||||
/// Returns the canonical Clock Program invocation transaction for the given block timestamp.
|
||||
/// Every valid block must end with exactly one occurrence of this transaction.
|
||||
#[must_use]
|
||||
pub fn clock_invocation(timestamp: clock_core::Instruction) -> nssa::PublicTransaction {
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
nssa::program::Program::clock().id(),
|
||||
clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
||||
vec![],
|
||||
timestamp,
|
||||
)
|
||||
.expect("Clock invocation message should always be constructable");
|
||||
nssa::PublicTransaction::new(
|
||||
message,
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
use bedrock_client::HeaderId;
|
||||
use common::{
|
||||
block::{BedrockStatus, Block},
|
||||
transaction::NSSATransaction,
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use nssa::{Account, AccountId, V03State};
|
||||
use nssa_core::BlockId;
|
||||
@ -122,7 +122,18 @@ impl IndexerStore {
|
||||
{
|
||||
let mut state_guard = self.current_state.write().await;
|
||||
|
||||
for transaction in &block.body.transactions {
|
||||
let (clock_tx, user_txs) = block
|
||||
.body
|
||||
.transactions
|
||||
.split_last()
|
||||
.ok_or_else(|| anyhow::anyhow!("Block has no transactions"))?;
|
||||
|
||||
anyhow::ensure!(
|
||||
*clock_tx == NSSATransaction::Public(clock_invocation(block.header.timestamp)),
|
||||
"Last transaction in block must be the clock invocation for the block timestamp"
|
||||
);
|
||||
|
||||
for transaction in user_txs {
|
||||
transaction
|
||||
.clone()
|
||||
.transaction_stateless_check()?
|
||||
@ -132,6 +143,16 @@ impl IndexerStore {
|
||||
block.header.timestamp,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Apply the clock invocation directly (it is expected to modify clock accounts).
|
||||
let NSSATransaction::Public(clock_public_tx) = clock_tx else {
|
||||
anyhow::bail!("Clock invocation must be a public transaction");
|
||||
};
|
||||
state_guard.transition_from_public_transaction(
|
||||
clock_public_tx,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
)?;
|
||||
}
|
||||
|
||||
// ToDo: Currently we are fetching only finalized blocks
|
||||
@ -177,7 +198,7 @@ mod tests {
|
||||
let storage = IndexerStore::open_db_with_genesis(
|
||||
home.as_ref(),
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -195,7 +216,7 @@ mod tests {
|
||||
let storage = IndexerStore::open_db_with_genesis(
|
||||
home.as_ref(),
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -213,11 +234,14 @@ mod tests {
|
||||
10,
|
||||
&sign_key,
|
||||
);
|
||||
let block_id = u64::try_from(i).unwrap();
|
||||
let block_timestamp = block_id.saturating_mul(100);
|
||||
let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp));
|
||||
|
||||
let next_block = common::test_utils::produce_dummy_block(
|
||||
u64::try_from(i).unwrap(),
|
||||
block_id,
|
||||
Some(prev_hash),
|
||||
vec![tx],
|
||||
vec![tx, clock_tx],
|
||||
);
|
||||
prev_hash = next_block.header.hash;
|
||||
|
||||
|
||||
@ -92,6 +92,7 @@ impl IndexerCore {
|
||||
let mut state = V03State::new_with_genesis_accounts(
|
||||
&init_accs.unwrap_or_default(),
|
||||
&initial_commitments.unwrap_or_default(),
|
||||
genesis_block.header.timestamp,
|
||||
);
|
||||
|
||||
// ToDo: Remove after testnet
|
||||
|
||||
@ -138,7 +138,7 @@ pub struct Account {
|
||||
}
|
||||
|
||||
pub type BlockId = u64;
|
||||
pub type TimeStamp = u64;
|
||||
pub type Timestamp = u64;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Block {
|
||||
@ -153,7 +153,7 @@ pub struct BlockHeader {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: HashType,
|
||||
pub hash: HashType,
|
||||
pub timestamp: TimeStamp,
|
||||
pub timestamp: Timestamp,
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core = { workspace = true, features = ["host"] }
|
||||
clock_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -55,7 +55,7 @@ pub type NullifierSecretKey = [u8; 32];
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
)]
|
||||
pub struct Nullifier(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -16,7 +16,11 @@ pub use program_deployment_transaction::ProgramDeploymentTransaction;
|
||||
pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
|
||||
pub use public_transaction::PublicTransaction;
|
||||
pub use signature::{PrivateKey, PublicKey, Signature};
|
||||
pub use state::V03State;
|
||||
pub use state::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
|
||||
};
|
||||
pub use validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
@ -27,6 +31,7 @@ pub mod program_deployment_transaction;
|
||||
pub mod public_transaction;
|
||||
mod signature;
|
||||
mod state;
|
||||
mod validated_state_diff;
|
||||
|
||||
pub mod program_methods {
|
||||
include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs"));
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
BlockId, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use super::{message::Message, witness_set::WitnessSet};
|
||||
use crate::{
|
||||
AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PrivacyPreservingTransaction {
|
||||
@ -30,108 +21,6 @@ impl PrivacyPreservingTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_produce_public_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<HashMap<AccountId, Account>, NssaError> {
|
||||
let message = &self.message;
|
||||
let witness_set = &self.witness_set;
|
||||
|
||||
// 1. Commitments or nullifiers are non empty
|
||||
if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Empty commitments and empty nullifiers found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// 2. Check there are no duplicate account_ids in the public_account_ids list.
|
||||
if n_unique(&message.public_account_ids) != message.public_account_ids.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate account_ids found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check there are no duplicate nullifiers in the new_nullifiers list
|
||||
if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate nullifiers found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check there are no duplicate commitments in the new_commitments list
|
||||
if n_unique(&message.new_commitments) != message.new_commitments.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate commitments found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// 3. Nonce checks and Valid signatures
|
||||
// Check exactly one nonce is provided for each signature
|
||||
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check the signatures are valid
|
||||
if !witness_set.signatures_are_valid_for(message) {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Invalid signature for given message and public key".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let signer_account_ids = self.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
if current_nonce != *nonce {
|
||||
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify validity window
|
||||
if !message.block_validity_window.is_valid_for(block_id)
|
||||
|| !message.timestamp_validity_window.is_valid_for(timestamp)
|
||||
{
|
||||
return Err(NssaError::OutOfValidityWindow);
|
||||
}
|
||||
|
||||
// Build pre_states for proof verification
|
||||
let public_pre_states: Vec<_> = message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 4. Proof verification
|
||||
check_privacy_preserving_circuit_proof_is_valid(
|
||||
&witness_set.proof,
|
||||
&public_pre_states,
|
||||
message,
|
||||
)?;
|
||||
|
||||
// 5. Commitment freshness
|
||||
state.check_commitments_are_new(&message.new_commitments)?;
|
||||
|
||||
// 6. Nullifier uniqueness
|
||||
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
|
||||
|
||||
Ok(message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(message.public_post_states.clone())
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn message(&self) -> &Message {
|
||||
&self.message
|
||||
@ -170,36 +59,6 @@ impl PrivacyPreservingTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), NssaError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
ciphertexts: message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|value| value.ciphertext)
|
||||
.collect(),
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers: message.new_nullifiers.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
};
|
||||
proof
|
||||
.is_valid_for(&output)
|
||||
.then_some(())
|
||||
.ok_or(NssaError::InvalidPrivacyPreservingProof)
|
||||
}
|
||||
|
||||
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
|
||||
let set: HashSet<&T> = data.iter().collect();
|
||||
set.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
|
||||
@ -9,7 +9,8 @@ use serde::Serialize;
|
||||
use crate::{
|
||||
error::NssaError,
|
||||
program_methods::{
|
||||
AMM_ELF, ASSOCIATED_TOKEN_ACCOUNT_ELF, AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF,
|
||||
AMM_ELF, ASSOCIATED_TOKEN_ACCOUNT_ELF, AUTHENTICATED_TRANSFER_ELF, CLOCK_ELF, PINATA_ELF,
|
||||
TOKEN_ELF,
|
||||
},
|
||||
};
|
||||
|
||||
@ -126,6 +127,11 @@ impl Program {
|
||||
Self::new(AMM_ELF.to_vec()).expect("The AMM program must be a valid Risc0 program")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn clock() -> Self {
|
||||
Self::new(CLOCK_ELF.to_vec()).expect("The clock program must be a valid Risc0 program")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ata() -> Self {
|
||||
Self::new(ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec())
|
||||
@ -352,6 +358,18 @@ mod tests {
|
||||
Self::new(MALICIOUS_CALLER_PROGRAM_ID_ELF.to_vec())
|
||||
.expect("malicious_caller_program_id must be a valid Risc0 program")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn time_locked_transfer() -> Self {
|
||||
use test_program_methods::TIME_LOCKED_TRANSFER_ELF;
|
||||
Self::new(TIME_LOCKED_TRANSFER_ELF.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn pinata_cooldown() -> Self {
|
||||
use test_program_methods::PINATA_COOLDOWN_ELF;
|
||||
Self::new(PINATA_COOLDOWN_ELF.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -2,9 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use crate::{
|
||||
V03State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
|
||||
};
|
||||
use crate::program_deployment_transaction::message::Message;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ProgramDeploymentTransaction {
|
||||
@ -22,19 +20,6 @@ impl ProgramDeploymentTransaction {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_produce_public_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
) -> Result<Program, NssaError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(self.message.bytecode.clone())?;
|
||||
if state.programs().contains_key(&program.id()) {
|
||||
Err(NssaError::ProgramAlreadyExists)
|
||||
} else {
|
||||
Ok(program)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
let bytes = self.to_bytes();
|
||||
|
||||
@ -1,20 +1,10 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use log::debug;
|
||||
use nssa_core::{
|
||||
BlockId, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
|
||||
};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::NssaError,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
use crate::public_transaction::{Message, WitnessSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PublicTransaction {
|
||||
@ -67,226 +57,6 @@ impl PublicTransaction {
|
||||
hasher.update(&bytes);
|
||||
hasher.finalize_fixed().into()
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_produce_public_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<HashMap<AccountId, Account>, NssaError> {
|
||||
let message = self.message();
|
||||
let witness_set = self.witness_set();
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
|
||||
);
|
||||
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
NssaError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = self.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
NssaError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
// Build pre_states for execution
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
ensure!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
NssaError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// Check that the `program_id` corresponds to a deployed program
|
||||
let Some(program) = state.programs().get(&chained_call.program_id) else {
|
||||
return Err(NssaError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = program.execute(
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_id, program_output
|
||||
);
|
||||
|
||||
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
|
||||
caller_program_id,
|
||||
&chained_call.pda_seeds,
|
||||
);
|
||||
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
|
||||
};
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// Check that the program output pre_states coincide with the values in the public
|
||||
// state or with any modifications to those values during the chain of calls.
|
||||
let expected_pre = state_diff
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
ensure!(
|
||||
pre.is_authorized == is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
ensure!(
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
program_output.block_validity_window.is_valid_for(block_id)
|
||||
&& program_output
|
||||
.timestamp_validity_window
|
||||
.is_valid_for(timestamp),
|
||||
NssaError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter_mut().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim.
|
||||
let pda = AccountId::from((&chained_call.program_id, &seed));
|
||||
ensure!(account_id == pda, NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
}
|
||||
|
||||
// Update the state diff
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for post in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
}
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some(post)
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
|
||||
Ok(state_diff)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -298,6 +68,7 @@ pub mod tests {
|
||||
error::NssaError,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) {
|
||||
@ -311,7 +82,7 @@ pub mod tests {
|
||||
fn state_for_tests() -> V03State {
|
||||
let (_, _, addr1, addr2) = keys_for_tests();
|
||||
let initial_data = [(addr1, 10000), (addr2, 20000)];
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
}
|
||||
|
||||
fn transaction_for_tests() -> PublicTransaction {
|
||||
@ -406,7 +177,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, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -426,7 +197,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, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -447,7 +218,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, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -467,7 +238,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, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -483,7 +254,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, 1, 0);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use clock_core::ClockAccountData;
|
||||
pub use clock_core::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS,
|
||||
};
|
||||
use nssa_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
|
||||
Timestamp,
|
||||
@ -9,10 +14,13 @@ use nssa_core::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::NssaError, merkle_tree::MerkleTree,
|
||||
privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program,
|
||||
error::NssaError,
|
||||
merkle_tree::MerkleTree,
|
||||
privacy_preserving_transaction::PrivacyPreservingTransaction,
|
||||
program::Program,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
validated_state_diff::{StateDiff, ValidatedStateDiff},
|
||||
};
|
||||
|
||||
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||
@ -73,7 +81,7 @@ impl NullifierSet {
|
||||
Self(BTreeSet::new())
|
||||
}
|
||||
|
||||
fn extend(&mut self, new_nullifiers: Vec<Nullifier>) {
|
||||
fn extend(&mut self, new_nullifiers: &[Nullifier]) {
|
||||
self.0.extend(new_nullifiers);
|
||||
}
|
||||
|
||||
@ -119,6 +127,7 @@ impl V03State {
|
||||
pub fn new_with_genesis_accounts(
|
||||
initial_data: &[(AccountId, u128)],
|
||||
initial_commitments: &[nssa_core::Commitment],
|
||||
genesis_timestamp: nssa_core::Timestamp,
|
||||
) -> Self {
|
||||
let authenticated_transfer_program = Program::authenticated_transfer_program();
|
||||
let public_state = initial_data
|
||||
@ -144,6 +153,9 @@ impl V03State {
|
||||
programs: HashMap::new(),
|
||||
};
|
||||
|
||||
this.insert_program(Program::clock());
|
||||
this.insert_clock_accounts(genesis_timestamp);
|
||||
|
||||
this.insert_program(Program::authenticated_transfer_program());
|
||||
this.insert_program(Program::token());
|
||||
this.insert_program(Program::amm());
|
||||
@ -152,33 +164,67 @@ impl V03State {
|
||||
this
|
||||
}
|
||||
|
||||
fn insert_clock_accounts(&mut self, genesis_timestamp: nssa_core::Timestamp) {
|
||||
let data = ClockAccountData {
|
||||
block_id: 0,
|
||||
timestamp: genesis_timestamp,
|
||||
}
|
||||
.to_bytes();
|
||||
let clock_program_id = Program::clock().id();
|
||||
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
|
||||
self.public_state.insert(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: clock_program_id,
|
||||
data: data
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("Clock account data should fit within accounts data"),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert_program(&mut self, program: Program) {
|
||||
self.programs.insert(program.id(), program);
|
||||
}
|
||||
|
||||
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
|
||||
let StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
program,
|
||||
} = diff.into_state_diff();
|
||||
#[expect(
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "Iteration order doesn't matter here"
|
||||
)]
|
||||
for (account_id, account) in public_diff {
|
||||
*self.get_account_by_id_mut(account_id) = account;
|
||||
}
|
||||
for account_id in signer_account_ids {
|
||||
self.get_account_by_id_mut(account_id)
|
||||
.nonce
|
||||
.public_account_nonce_increment();
|
||||
}
|
||||
self.private_state.0.extend(&new_commitments);
|
||||
self.private_state.1.extend(&new_nullifiers);
|
||||
if let Some(program) = program {
|
||||
self.insert_program(program);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_public_transaction(
|
||||
&mut self,
|
||||
tx: &PublicTransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), NssaError> {
|
||||
let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
|
||||
|
||||
#[expect(
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "Iteration order doesn't matter here"
|
||||
)]
|
||||
for (account_id, post) in state_diff {
|
||||
let current_account = self.get_account_by_id_mut(account_id);
|
||||
|
||||
*current_account = post;
|
||||
}
|
||||
|
||||
for account_id in tx.signer_account_ids() {
|
||||
let current_account = self.get_account_by_id_mut(account_id);
|
||||
current_account.nonce.public_account_nonce_increment();
|
||||
}
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -188,40 +234,9 @@ impl V03State {
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), NssaError> {
|
||||
// 1. Verify the transaction satisfies acceptance criteria
|
||||
let public_state_diff =
|
||||
tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
|
||||
|
||||
let message = tx.message();
|
||||
|
||||
// 2. Add new commitments
|
||||
self.private_state.0.extend(&message.new_commitments);
|
||||
|
||||
// 3. Add new nullifiers
|
||||
let new_nullifiers = message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(nullifier, _)| nullifier)
|
||||
.collect::<Vec<Nullifier>>();
|
||||
self.private_state.1.extend(new_nullifiers);
|
||||
|
||||
// 4. Update public accounts
|
||||
#[expect(
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "Iteration order doesn't matter here"
|
||||
)]
|
||||
for (account_id, post) in public_state_diff {
|
||||
let current_account = self.get_account_by_id_mut(account_id);
|
||||
*current_account = post;
|
||||
}
|
||||
|
||||
// 5. Increment nonces for public signers
|
||||
for account_id in tx.signer_account_ids() {
|
||||
let current_account = self.get_account_by_id_mut(account_id);
|
||||
current_account.nonce.public_account_nonce_increment();
|
||||
}
|
||||
|
||||
let diff =
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -229,8 +244,8 @@ impl V03State {
|
||||
&mut self,
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
) -> Result<(), NssaError> {
|
||||
let program = tx.validate_and_produce_public_state_diff(self)?;
|
||||
self.insert_program(program);
|
||||
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -362,7 +377,10 @@ pub mod tests {
|
||||
program::Program,
|
||||
public_transaction,
|
||||
signature::PrivateKey,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
state::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS,
|
||||
},
|
||||
};
|
||||
|
||||
impl V03State {
|
||||
@ -386,6 +404,8 @@ pub mod tests {
|
||||
self.insert_program(Program::flash_swap_callback());
|
||||
self.insert_program(Program::malicious_self_program_id());
|
||||
self.insert_program(Program::malicious_caller_program_id());
|
||||
self.insert_program(Program::time_locked_transfer());
|
||||
self.insert_program(Program::pinata_cooldown());
|
||||
self
|
||||
}
|
||||
|
||||
@ -528,6 +548,7 @@ pub mod tests {
|
||||
let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [(addr1, 100_u128), (addr2, 151_u128)];
|
||||
let authenticated_transfers_program = Program::authenticated_transfer_program();
|
||||
let clock_program = Program::clock();
|
||||
let expected_public_state = {
|
||||
let mut this = HashMap::new();
|
||||
this.insert(
|
||||
@ -546,6 +567,16 @@ pub mod tests {
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
for account_id in CLOCK_PROGRAM_ACCOUNT_IDS {
|
||||
this.insert(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: clock_program.id(),
|
||||
data: [0_u8; 16].to_vec().try_into().unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
this
|
||||
};
|
||||
let expected_builtin_programs = {
|
||||
@ -554,13 +585,14 @@ pub mod tests {
|
||||
authenticated_transfers_program.id(),
|
||||
authenticated_transfers_program,
|
||||
);
|
||||
this.insert(clock_program.id(), clock_program);
|
||||
this.insert(Program::token().id(), Program::token());
|
||||
this.insert(Program::amm().id(), Program::amm());
|
||||
this.insert(Program::ata().id(), Program::ata());
|
||||
this
|
||||
};
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
|
||||
assert_eq!(state.public_state, expected_public_state);
|
||||
assert_eq!(state.programs, expected_builtin_programs);
|
||||
@ -568,7 +600,7 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn insert_program() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
let program_to_insert = Program::simple_balance_transfer();
|
||||
let program_id = program_to_insert.id();
|
||||
assert!(!state.programs.contains_key(&program_id));
|
||||
@ -583,7 +615,7 @@ pub mod tests {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(account_id, 100_u128)];
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
let expected_account = &state.public_state[&account_id];
|
||||
|
||||
let account = state.get_account_by_id(account_id);
|
||||
@ -594,7 +626,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn get_account_by_account_id_default_account() {
|
||||
let addr2 = AccountId::new([0; 32]);
|
||||
let state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
let expected_account = Account::default();
|
||||
|
||||
let account = state.get_account_by_id(addr2);
|
||||
@ -604,7 +636,7 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn builtin_programs_getter() {
|
||||
let state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
let builtin_programs = state.programs();
|
||||
|
||||
@ -616,7 +648,7 @@ pub mod tests {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(account_id, 100)];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
let from = account_id;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
@ -637,7 +669,7 @@ pub mod tests {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(account_id, 100)];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
let from = account_id;
|
||||
let from_key = key;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
@ -662,7 +694,7 @@ pub mod tests {
|
||||
let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [(account_id1, 100), (account_id2, 200)];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
let from = account_id2;
|
||||
let from_key = key2;
|
||||
let to = account_id1;
|
||||
@ -686,7 +718,7 @@ pub mod tests {
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [(account_id1, 100)];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
let key3 = PrivateKey::try_new([3; 32]).unwrap();
|
||||
let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3));
|
||||
let balance_to_move = 5;
|
||||
@ -721,11 +753,154 @@ pub mod tests {
|
||||
assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1));
|
||||
}
|
||||
|
||||
fn clock_transaction(timestamp: nssa_core::Timestamp) -> PublicTransaction {
|
||||
let message = public_transaction::Message::try_new(
|
||||
Program::clock().id(),
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
||||
vec![],
|
||||
timestamp,
|
||||
)
|
||||
.unwrap();
|
||||
PublicTransaction::new(
|
||||
message,
|
||||
public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
)
|
||||
}
|
||||
|
||||
fn clock_account_data(state: &V03State, account_id: AccountId) -> (u64, nssa_core::Timestamp) {
|
||||
let data = state.get_account_by_id(account_id).data.into_inner();
|
||||
let parsed = clock_core::ClockAccountData::from_bytes(&data);
|
||||
(parsed.block_id, parsed.timestamp)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_genesis_state_has_zero_block_id_and_genesis_timestamp() {
|
||||
let genesis_timestamp = 1_000_000_u64;
|
||||
let state = V03State::new_with_genesis_accounts(&[], &[], genesis_timestamp);
|
||||
|
||||
let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
assert_eq!(block_id, 0);
|
||||
assert_eq!(timestamp, genesis_timestamp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_invocation_increments_block_id() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
let tx = clock_transaction(1234);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
|
||||
let (block_id, _) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(block_id, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_invocation_stores_timestamp_from_instruction() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
let block_timestamp = 1_700_000_000_000_u64;
|
||||
|
||||
let tx = clock_transaction(block_timestamp);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
|
||||
let (_, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(timestamp, block_timestamp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_invocation_sequence_correctly_increments_block_id() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
for expected_block_id in 1_u64..=5 {
|
||||
let tx = clock_transaction(expected_block_id * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
|
||||
let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(block_id, expected_block_id);
|
||||
assert_eq!(timestamp, expected_block_id * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_10_account_not_updated_when_block_id_not_multiple_of_10() {
|
||||
let genesis_timestamp = 0_u64;
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], genesis_timestamp);
|
||||
|
||||
// Run 9 clock ticks (block_ids 1..=9), none of which are multiples of 10.
|
||||
for tick in 1_u64..=9 {
|
||||
let tx = clock_transaction(tick * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID);
|
||||
// The 10-block account should still reflect genesis state.
|
||||
assert_eq!(block_id_10, 0);
|
||||
assert_eq!(timestamp_10, genesis_timestamp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_10_account_updated_when_block_id_is_multiple_of_10() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
// Run 10 clock ticks so block_id reaches 10.
|
||||
for tick in 1_u64..=10 {
|
||||
let tx = clock_transaction(tick * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
let (block_id_1, timestamp_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(block_id_1, 10);
|
||||
assert_eq!(block_id_10, 10);
|
||||
assert_eq!(timestamp_10, timestamp_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clock_50_account_only_updated_at_multiples_of_50() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
// After 49 ticks the 50-block account should be unchanged.
|
||||
for tick in 1_u64..=49 {
|
||||
let tx = clock_transaction(tick * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
}
|
||||
let (block_id_50, _) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(block_id_50, 0);
|
||||
|
||||
// Tick 50 — now the 50-block account should update.
|
||||
let tx = clock_transaction(50 * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
let (block_id_50, timestamp_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID);
|
||||
assert_eq!(block_id_50, 50);
|
||||
assert_eq!(timestamp_50, 50 * 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_three_clock_accounts_updated_at_multiple_of_50() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
// Advance to block 50 (a multiple of both 10 and 50).
|
||||
for tick in 1_u64..=50 {
|
||||
let tx = clock_transaction(tick * 1000);
|
||||
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
let (block_id_1, ts_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
let (block_id_10, ts_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID);
|
||||
let (block_id_50, ts_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
assert_eq!(block_id_1, 50);
|
||||
assert_eq!(block_id_10, 50);
|
||||
assert_eq!(block_id_50, 50);
|
||||
assert_eq!(ts_1, ts_10);
|
||||
assert_eq!(ts_1, ts_50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_nonces() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let program_id = Program::nonce_changer_program().id();
|
||||
let message =
|
||||
@ -742,7 +917,7 @@ pub mod tests {
|
||||
fn program_should_fail_if_output_accounts_exceed_inputs() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let program_id = Program::extra_output_program().id();
|
||||
let message =
|
||||
@ -759,7 +934,7 @@ pub mod tests {
|
||||
fn program_should_fail_with_missing_output_accounts() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])];
|
||||
let program_id = Program::missing_output_program().id();
|
||||
let message =
|
||||
@ -776,7 +951,7 @@ pub mod tests {
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 0)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in the program owner
|
||||
@ -799,7 +974,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
@ -823,7 +998,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([254; 32]);
|
||||
@ -847,7 +1022,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([253; 32]);
|
||||
@ -872,7 +1047,7 @@ pub mod tests {
|
||||
fn program_should_fail_if_transfers_balance_from_non_owned_account() {
|
||||
let initial_data = [(AccountId::new([1; 32]), 100)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let sender_account_id = AccountId::new([1; 32]);
|
||||
let receiver_account_id = AccountId::new([2; 32]);
|
||||
let balance_to_move: u128 = 1;
|
||||
@ -899,7 +1074,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_data_of_non_owned_account() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
@ -925,7 +1100,7 @@ pub mod tests {
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_minting() {
|
||||
let initial_data = [];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = Program::minter().id();
|
||||
|
||||
@ -942,7 +1117,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_burning() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0)
|
||||
.with_test_programs()
|
||||
.with_account_owned_by_burner_program();
|
||||
let program_id = Program::burner().id();
|
||||
@ -1134,7 +1309,7 @@ pub mod tests {
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(sender_keys.account_id(), 200)], &[]);
|
||||
V03State::new_with_genesis_accounts(&[(sender_keys.account_id(), 200)], &[], 0);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
@ -1182,7 +1357,7 @@ pub mod tests {
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0)
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
@ -1252,6 +1427,7 @@ pub mod tests {
|
||||
let mut state = V03State::new_with_genesis_accounts(
|
||||
&[(recipient_keys.account_id(), recipient_initial_balance)],
|
||||
&[],
|
||||
0,
|
||||
)
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
@ -2203,7 +2379,7 @@ pub mod tests {
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[])
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0)
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
@ -2288,7 +2464,7 @@ pub mod tests {
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
@ -2326,7 +2502,7 @@ pub mod tests {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let account_key = PrivateKey::try_new([9; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
@ -2347,7 +2523,7 @@ pub mod tests {
|
||||
let program = Program::authenticated_transfer_program();
|
||||
let account_key = PrivateKey::try_new([10; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
@ -2382,7 +2558,7 @@ pub mod tests {
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
@ -2427,7 +2603,7 @@ pub mod tests {
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 0;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
@ -2465,7 +2641,7 @@ pub mod tests {
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let amount: u128 = 58;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
@ -2511,7 +2687,7 @@ pub mod tests {
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
@ -2586,7 +2762,7 @@ pub mod tests {
|
||||
};
|
||||
let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account);
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[], std::slice::from_ref(&sender_commitment));
|
||||
V03State::new_with_genesis_accounts(&[], std::slice::from_ref(&sender_commitment), 0);
|
||||
let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk());
|
||||
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let recipient_account_id =
|
||||
@ -2669,6 +2845,7 @@ pub mod tests {
|
||||
let mut state = V03State::new_with_genesis_accounts(
|
||||
&[],
|
||||
&[from_commitment.clone(), to_commitment.clone()],
|
||||
0,
|
||||
)
|
||||
.with_test_programs();
|
||||
let amount: u128 = 37;
|
||||
@ -2775,7 +2952,7 @@ pub mod tests {
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
state.add_pinata_token_program(pinata_definition_id);
|
||||
|
||||
// Set up the token accounts directly (bypassing public transactions which
|
||||
@ -2847,7 +3024,7 @@ pub mod tests {
|
||||
#[test]
|
||||
fn claiming_mechanism_cannot_claim_initialied_accounts() {
|
||||
let claimer = Program::claimer();
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
let account_id = AccountId::new([2; 32]);
|
||||
|
||||
// Insert an account with non-default program owner
|
||||
@ -2888,6 +3065,7 @@ pub mod tests {
|
||||
(recipient_id, recipient_init_balance),
|
||||
],
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
|
||||
state.insert_program(Program::modified_transfer_program());
|
||||
@ -2937,7 +3115,7 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn private_authorized_uninitialized_account() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
|
||||
// Set up keys for the authorized private account
|
||||
let private_keys = test_private_account_keys_1();
|
||||
@ -2989,7 +3167,7 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
|
||||
let private_keys = test_private_account_keys_1();
|
||||
// This is intentional: claim authorization was introduced to protect public accounts,
|
||||
@ -3036,7 +3214,7 @@ pub mod tests {
|
||||
|
||||
#[test]
|
||||
fn private_account_claimed_then_used_without_init_flag_should_fail() {
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
|
||||
let mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
|
||||
// Set up keys for the private account
|
||||
let private_keys = test_private_account_keys_1();
|
||||
@ -3117,7 +3295,7 @@ pub mod tests {
|
||||
fn public_changer_claimer_no_data_change_no_claim_succeeds() {
|
||||
let initial_data = [];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = Program::changer_claimer().id();
|
||||
// Don't change data (None) and don't claim (false)
|
||||
@ -3141,7 +3319,7 @@ pub mod tests {
|
||||
fn public_changer_claimer_data_change_no_claim_fails() {
|
||||
let initial_data = [];
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = Program::changer_claimer().id();
|
||||
// Change data but don't claim (false) - should fail
|
||||
@ -3238,6 +3416,7 @@ pub mod tests {
|
||||
let state = V03State::new_with_genesis_accounts(
|
||||
&[(sender_account.account_id, sender_account.account.balance)],
|
||||
std::slice::from_ref(&recipient_commitment),
|
||||
0,
|
||||
)
|
||||
.with_test_programs();
|
||||
|
||||
@ -3287,7 +3466,7 @@ pub mod tests {
|
||||
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 mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
@ -3339,7 +3518,7 @@ pub mod tests {
|
||||
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 mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
@ -3392,7 +3571,7 @@ pub mod tests {
|
||||
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 mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
let tx = {
|
||||
let esk = [3; 32];
|
||||
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
|
||||
@ -3461,7 +3640,7 @@ pub mod tests {
|
||||
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 mut state = V03State::new_with_genesis_accounts(&[], &[], 0).with_test_programs();
|
||||
let tx = {
|
||||
let esk = [3; 32];
|
||||
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
|
||||
@ -3510,12 +3689,233 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn time_locked_transfer_transaction(
|
||||
from: AccountId,
|
||||
from_key: &PrivateKey,
|
||||
from_nonce: u128,
|
||||
to: AccountId,
|
||||
clock_account_id: AccountId,
|
||||
amount: u128,
|
||||
deadline: u64,
|
||||
) -> PublicTransaction {
|
||||
let program_id = Program::time_locked_transfer().id();
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![from, to, clock_account_id],
|
||||
vec![Nonce(from_nonce)],
|
||||
(amount, deadline),
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_locked_transfer_succeeds_when_deadline_has_passed() {
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
let genesis_timestamp = 500_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(recipient_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
state.force_insert_account(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: Program::time_locked_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let amount = 100_u128;
|
||||
// Deadline in the past: transfer should succeed.
|
||||
let deadline = 0_u64;
|
||||
|
||||
let tx = time_locked_transfer_transaction(
|
||||
sender_id,
|
||||
&key1,
|
||||
0,
|
||||
recipient_id,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
amount,
|
||||
deadline,
|
||||
);
|
||||
|
||||
let block_id = 1;
|
||||
let timestamp = genesis_timestamp + 100;
|
||||
state
|
||||
.transition_from_public_transaction(&tx, block_id, timestamp)
|
||||
.unwrap();
|
||||
|
||||
// Balances changed.
|
||||
assert_eq!(state.get_account_by_id(sender_id).balance, 0);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_locked_transfer_fails_when_deadline_is_in_the_future() {
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
let genesis_timestamp = 500_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(recipient_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
state.force_insert_account(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: Program::time_locked_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let amount = 100_u128;
|
||||
// Far-future deadline: program should panic.
|
||||
let deadline = u64::MAX;
|
||||
|
||||
let tx = time_locked_transfer_transaction(
|
||||
sender_id,
|
||||
&key1,
|
||||
0,
|
||||
recipient_id,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
amount,
|
||||
deadline,
|
||||
);
|
||||
|
||||
let block_id = 1;
|
||||
let timestamp = genesis_timestamp + 100;
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, timestamp);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Transfer should fail when deadline is in the future"
|
||||
);
|
||||
// Balances unchanged.
|
||||
assert_eq!(state.get_account_by_id(sender_id).balance, 100);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
buf.extend_from_slice(&prize.to_le_bytes());
|
||||
buf.extend_from_slice(&cooldown_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&last_claim_timestamp.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
fn pinata_cooldown_transaction(
|
||||
pinata_id: AccountId,
|
||||
winner_id: AccountId,
|
||||
clock_account_id: AccountId,
|
||||
) -> PublicTransaction {
|
||||
let program_id = Program::pinata_cooldown().id();
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![pinata_id, winner_id, clock_account_id],
|
||||
vec![],
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinata_cooldown_claim_succeeds_after_cooldown() {
|
||||
let winner_id = AccountId::new([11; 32]);
|
||||
let pinata_id = AccountId::new([99; 32]);
|
||||
|
||||
let genesis_timestamp = 1000_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(winner_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
|
||||
let prize = 50_u128;
|
||||
let cooldown_ms = 500_u64;
|
||||
// Last claim was at genesis, so any timestamp >= genesis + cooldown should work.
|
||||
let last_claim_timestamp = genesis_timestamp;
|
||||
|
||||
state.force_insert_account(
|
||||
pinata_id,
|
||||
Account {
|
||||
program_owner: Program::pinata_cooldown().id(),
|
||||
balance: 1000,
|
||||
data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let block_id = 1;
|
||||
let block_timestamp = genesis_timestamp + cooldown_ms;
|
||||
// Advance clock so the cooldown check reads an updated timestamp.
|
||||
let clock_tx = clock_transaction(block_timestamp);
|
||||
state
|
||||
.transition_from_public_transaction(&clock_tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
state
|
||||
.transition_from_public_transaction(&tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize);
|
||||
assert_eq!(state.get_account_by_id(winner_id).balance, prize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinata_cooldown_claim_fails_during_cooldown() {
|
||||
let winner_id = AccountId::new([11; 32]);
|
||||
let pinata_id = AccountId::new([99; 32]);
|
||||
|
||||
let genesis_timestamp = 1000_u64;
|
||||
let mut state =
|
||||
V03State::new_with_genesis_accounts(&[(winner_id, 0)], &[], genesis_timestamp)
|
||||
.with_test_programs();
|
||||
|
||||
let prize = 50_u128;
|
||||
let cooldown_ms = 500_u64;
|
||||
let last_claim_timestamp = genesis_timestamp;
|
||||
|
||||
state.force_insert_account(
|
||||
pinata_id,
|
||||
Account {
|
||||
balance: 1000,
|
||||
data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let block_id = 1;
|
||||
// Timestamp is only 100ms after last claim, well within the 500ms cooldown.
|
||||
let block_timestamp = genesis_timestamp + 100;
|
||||
let clock_tx = clock_transaction(block_timestamp);
|
||||
state
|
||||
.transition_from_public_transaction(&clock_tx, block_id, block_timestamp)
|
||||
.unwrap();
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, block_timestamp);
|
||||
|
||||
assert!(result.is_err(), "Claim should fail during cooldown period");
|
||||
assert_eq!(state.get_account_by_id(pinata_id).balance, 1000);
|
||||
assert_eq!(state.get_account_by_id(winner_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_serialization_roundtrip() {
|
||||
let account_id_1 = AccountId::new([1; 32]);
|
||||
let account_id_2 = AccountId::new([2; 32]);
|
||||
let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)];
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
|
||||
let state = V03State::new_with_genesis_accounts(&initial_data, &[], 0).with_test_programs();
|
||||
let bytes = borsh::to_vec(&state).unwrap();
|
||||
let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap();
|
||||
assert_eq!(state, state_from_bytes);
|
||||
|
||||
433
nssa/src/validated_state_diff.rs
Normal file
433
nssa/src/validated_state_diff.rs
Normal file
@ -0,0 +1,433 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use log::debug;
|
||||
use nssa_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_authorized_pdas, validate_execution,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::NssaError,
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::Proof, message::Message,
|
||||
},
|
||||
program::Program,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
pub struct StateDiff {
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
pub public_diff: HashMap<AccountId, Account>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<Nullifier>,
|
||||
pub program: Option<Program>,
|
||||
}
|
||||
|
||||
/// The validated output of executing or verifying a transaction, ready to be applied to the state.
|
||||
///
|
||||
/// Can only be constructed by the transaction validation functions inside this crate, ensuring the
|
||||
/// diff has been checked before any state mutation occurs.
|
||||
pub struct ValidatedStateDiff(StateDiff);
|
||||
|
||||
impl ValidatedStateDiff {
|
||||
pub fn from_public_transaction(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, NssaError> {
|
||||
let message = tx.message();
|
||||
let witness_set = tx.witness_set();
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
|
||||
);
|
||||
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
NssaError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
NssaError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
// Build pre_states for execution
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
|
||||
ensure!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
NssaError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// Check that the `program_id` corresponds to a deployed program
|
||||
let Some(program) = state.programs().get(&chained_call.program_id) else {
|
||||
return Err(NssaError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
|
||||
);
|
||||
let mut program_output =
|
||||
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_id, program_output
|
||||
);
|
||||
|
||||
let authorized_pdas =
|
||||
compute_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
|
||||
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
|
||||
};
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// Check that the program output pre_states coincide with the values in the public
|
||||
// state or with any modifications to those values during the chain of calls.
|
||||
let expected_pre = state_diff
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Check that authorization flags are consistent with the provided ones or
|
||||
// authorized by program through the PDA mechanism
|
||||
ensure!(
|
||||
pre.is_authorized == is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
ensure!(
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
program_output.block_validity_window.is_valid_for(block_id)
|
||||
&& program_output
|
||||
.timestamp_validity_window
|
||||
.is_valid_for(timestamp),
|
||||
NssaError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter_mut().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
|
||||
let account_id = program_output.pre_states[i].account_id;
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
is_authorized(&account_id),
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim.
|
||||
let pda = AccountId::from((&chained_call.program_id, &seed));
|
||||
ensure!(account_id == pda, NssaError::InvalidProgramBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
}
|
||||
|
||||
// Update the state diff
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((new_call, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for post in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
}
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some(post)
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
NssaError::InvalidProgramBehavior
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff: state_diff,
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
program: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_privacy_preserving_transaction(
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, NssaError> {
|
||||
let message = &tx.message;
|
||||
let witness_set = &tx.witness_set;
|
||||
|
||||
// 1. Commitments or nullifiers are non empty
|
||||
if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Empty commitments and empty nullifiers found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// 2. Check there are no duplicate account_ids in the public_account_ids list.
|
||||
if n_unique(&message.public_account_ids) != message.public_account_ids.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate account_ids found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check there are no duplicate nullifiers in the new_nullifiers list
|
||||
if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate nullifiers found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check there are no duplicate commitments in the new_commitments list
|
||||
if n_unique(&message.new_commitments) != message.new_commitments.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Duplicate commitments found in message".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// 3. Nonce checks and Valid signatures
|
||||
// Check exactly one nonce is provided for each signature
|
||||
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check the signatures are valid
|
||||
if !witness_set.signatures_are_valid_for(message) {
|
||||
return Err(NssaError::InvalidInput(
|
||||
"Invalid signature for given message and public key".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
if current_nonce != *nonce {
|
||||
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
|
||||
}
|
||||
}
|
||||
|
||||
// Verify validity window
|
||||
if !message.block_validity_window.is_valid_for(block_id)
|
||||
|| !message.timestamp_validity_window.is_valid_for(timestamp)
|
||||
{
|
||||
return Err(NssaError::OutOfValidityWindow);
|
||||
}
|
||||
|
||||
// Build pre_states for proof verification
|
||||
let public_pre_states: Vec<_> = message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 4. Proof verification
|
||||
check_privacy_preserving_circuit_proof_is_valid(
|
||||
&witness_set.proof,
|
||||
&public_pre_states,
|
||||
message,
|
||||
)?;
|
||||
|
||||
// 5. Commitment freshness
|
||||
state.check_commitments_are_new(&message.new_commitments)?;
|
||||
|
||||
// 6. Nullifier uniqueness
|
||||
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
|
||||
|
||||
let public_diff = message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(message.public_post_states.clone())
|
||||
.collect();
|
||||
let new_nullifiers = message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(nullifier, _)| nullifier)
|
||||
.collect();
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers,
|
||||
program: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_program_deployment_transaction(
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Self, NssaError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(tx.message.bytecode.clone())?;
|
||||
if state.programs().contains_key(&program.id()) {
|
||||
return Err(NssaError::ProgramAlreadyExists);
|
||||
}
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids: vec![],
|
||||
public_diff: HashMap::new(),
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
program: Some(program),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the public account changes produced by this transaction.
|
||||
///
|
||||
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
|
||||
/// to enforce that system accounts are not modified by user transactions.
|
||||
#[must_use]
|
||||
pub fn public_diff(&self) -> HashMap<AccountId, Account> {
|
||||
self.0.public_diff.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn into_state_diff(self) -> StateDiff {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), NssaError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
ciphertexts: message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|value| value.ciphertext)
|
||||
.collect(),
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers: message.new_nullifiers.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
};
|
||||
proof
|
||||
.is_valid_for(&output)
|
||||
.then_some(())
|
||||
.ok_or(NssaError::InvalidPrivacyPreservingProof)
|
||||
}
|
||||
|
||||
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
|
||||
let set: HashSet<&T> = data.iter().collect();
|
||||
set.len()
|
||||
}
|
||||
@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
token_core.workspace = true
|
||||
token_program.workspace = true
|
||||
amm_core.workspace = true
|
||||
|
||||
92
program_methods/guest/src/bin/clock.rs
Normal file
92
program_methods/guest/src/bin/clock.rs
Normal file
@ -0,0 +1,92 @@
|
||||
//! Clock Program.
|
||||
//!
|
||||
//! A system program that records the current block ID and timestamp into dedicated clock accounts.
|
||||
//! Three accounts are maintained, updated at different block intervals (every 1, 10, and 50
|
||||
//! blocks), allowing programs to read recent timestamps at various granularities.
|
||||
//!
|
||||
//! This program can only be invoked exclusively by the sequencer as the last transaction in every
|
||||
//! block. Clock accounts are assigned to the clock program at genesis, so no claiming is required
|
||||
//! here.
|
||||
|
||||
use clock_core::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
ClockAccountData, Instruction,
|
||||
};
|
||||
use nssa_core::{
|
||||
account::AccountWithMetadata,
|
||||
program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs},
|
||||
};
|
||||
|
||||
fn update_if_multiple(
|
||||
pre: AccountWithMetadata,
|
||||
divisor: u64,
|
||||
current_block_id: u64,
|
||||
updated_data: &[u8],
|
||||
) -> (AccountWithMetadata, AccountPostState) {
|
||||
if current_block_id.is_multiple_of(divisor) {
|
||||
let mut post_account = pre.account.clone();
|
||||
post_account.data = updated_data
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("Clock account data should fit in account data");
|
||||
(pre, AccountPostState::new(post_account))
|
||||
} else {
|
||||
let post = AccountPostState::new(pre.account.clone());
|
||||
(pre, post)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: timestamp,
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pre_01, pre_10, pre_50]) = <[_; 3]>::try_from(pre_states) else {
|
||||
panic!("Invalid number of input accounts");
|
||||
};
|
||||
|
||||
// Verify pre-states correspond to the expected clock account IDs.
|
||||
if pre_01.account_id != CLOCK_01_PROGRAM_ACCOUNT_ID
|
||||
|| pre_10.account_id != CLOCK_10_PROGRAM_ACCOUNT_ID
|
||||
|| pre_50.account_id != CLOCK_50_PROGRAM_ACCOUNT_ID
|
||||
{
|
||||
panic!("Invalid input accounts");
|
||||
}
|
||||
|
||||
// Verify all clock accounts are owned by this program (assigned at genesis).
|
||||
if pre_01.account.program_owner != self_program_id
|
||||
|| pre_10.account.program_owner != self_program_id
|
||||
|| pre_50.account.program_owner != self_program_id
|
||||
{
|
||||
panic!("Clock accounts must be owned by the clock program");
|
||||
}
|
||||
|
||||
let prev_data = ClockAccountData::from_bytes(&pre_01.account.data.clone().into_inner());
|
||||
let current_block_id = prev_data
|
||||
.block_id
|
||||
.checked_add(1)
|
||||
.expect("Next block id should be within u64 boundaries");
|
||||
|
||||
let updated_data = ClockAccountData {
|
||||
block_id: current_block_id,
|
||||
timestamp,
|
||||
}
|
||||
.to_bytes();
|
||||
|
||||
let (pre_01, post_01) = update_if_multiple(pre_01, 1, current_block_id, &updated_data);
|
||||
let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, &updated_data);
|
||||
let (pre_50, post_50) = update_if_multiple(pre_50, 50, current_block_id, &updated_data);
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
instruction_words,
|
||||
vec![pre_01, pre_10, pre_50],
|
||||
vec![post_01, post_10, post_50],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
@ -3036,7 +3036,7 @@ fn new_definition_lp_symmetric_amounts() {
|
||||
|
||||
fn state_for_amm_tests() -> V03State {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
state.force_insert_account(
|
||||
IdForExeTests::pool_definition_id(),
|
||||
AccountsForExeTests::pool_definition_init(),
|
||||
@ -3079,7 +3079,7 @@ fn state_for_amm_tests() -> V03State {
|
||||
|
||||
fn state_for_amm_tests_with_new_def() -> V03State {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
|
||||
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[], 0);
|
||||
state.force_insert_account(
|
||||
IdForExeTests::token_a_definition_id(),
|
||||
AccountsForExeTests::token_a_definition_account(),
|
||||
|
||||
12
programs/clock/core/Cargo.toml
Normal file
12
programs/clock/core/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "clock_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
borsh.workspace = true
|
||||
42
programs/clock/core/src/lib.rs
Normal file
42
programs/clock/core/src/lib.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! Core data structures and constants for the Clock Program.
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{Timestamp, account::AccountId};
|
||||
|
||||
pub const CLOCK_01_PROGRAM_ACCOUNT_ID: AccountId =
|
||||
AccountId::new(*b"/LEZ/ClockProgramAccount/0000001");
|
||||
|
||||
pub const CLOCK_10_PROGRAM_ACCOUNT_ID: AccountId =
|
||||
AccountId::new(*b"/LEZ/ClockProgramAccount/0000010");
|
||||
|
||||
pub const CLOCK_50_PROGRAM_ACCOUNT_ID: AccountId =
|
||||
AccountId::new(*b"/LEZ/ClockProgramAccount/0000050");
|
||||
|
||||
/// All clock program account ID in the order expected by the clock program.
|
||||
pub const CLOCK_PROGRAM_ACCOUNT_IDS: [AccountId; 3] = [
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_10_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
];
|
||||
|
||||
/// The instruction type for the Clock Program. The sequencer passes the current block timestamp.
|
||||
pub type Instruction = Timestamp;
|
||||
|
||||
/// The data stored in a clock account.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ClockAccountData {
|
||||
pub block_id: u64,
|
||||
pub timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl ClockAccountData {
|
||||
#[must_use]
|
||||
pub fn to_bytes(self) -> Vec<u8> {
|
||||
borsh::to_vec(&self).expect("ClockAccountData serialization should not fail")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
||||
borsh::from_slice(bytes).expect("ClockAccountData deserialization should not fail")
|
||||
}
|
||||
}
|
||||
@ -40,3 +40,5 @@ mock = []
|
||||
|
||||
[dev-dependencies]
|
||||
futures.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
nssa = { workspace = true, features = ["test-utils"] }
|
||||
|
||||
@ -150,7 +150,7 @@ mod tests {
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
assert_eq!(None, retrieved_tx);
|
||||
// Add the block with the transaction
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||
// Try again
|
||||
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
|
||||
@ -209,7 +209,7 @@ mod tests {
|
||||
let block_hash = block.header.hash;
|
||||
let block_msg_id = [1; 32];
|
||||
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
node_store
|
||||
.update(&block, block_msg_id, &dummy_state)
|
||||
.unwrap();
|
||||
@ -244,7 +244,7 @@ mod tests {
|
||||
let block = common::test_utils::produce_dummy_block(1, None, vec![tx]);
|
||||
let block_id = block.header.block_id;
|
||||
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[]);
|
||||
let dummy_state = V03State::new_with_genesis_accounts(&[], &[], 0);
|
||||
node_store.update(&block, [1; 32], &dummy_state).unwrap();
|
||||
|
||||
// Verify initial status is Pending
|
||||
|
||||
@ -24,9 +24,10 @@ pub struct SequencerConfig {
|
||||
pub genesis_id: u64,
|
||||
/// If `True`, then adds random sequence of bytes to genesis block.
|
||||
pub is_genesis_random: bool,
|
||||
/// Maximum number of transactions in block.
|
||||
/// Maximum number of user transactions in a block (excludes the mandatory clock transaction).
|
||||
pub max_num_tx_in_block: usize,
|
||||
/// Maximum block size (includes header and transactions).
|
||||
/// Maximum block size (includes header, user transactions, and the mandatory clock
|
||||
/// transaction).
|
||||
#[serde(default = "default_max_block_size")]
|
||||
pub max_block_size: ByteSize,
|
||||
/// Mempool maximum size.
|
||||
|
||||
@ -7,7 +7,7 @@ use common::PINATA_BASE58;
|
||||
use common::{
|
||||
HashType,
|
||||
block::{BedrockStatus, Block, HashableBlockData},
|
||||
transaction::NSSATransaction,
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use config::SequencerConfig;
|
||||
use log::{error, info, warn};
|
||||
@ -16,7 +16,6 @@ use mempool::{MemPool, MemPoolHandle};
|
||||
#[cfg(feature = "mock")]
|
||||
pub use mock::SequencerCoreWithMockClients;
|
||||
use nssa::V03State;
|
||||
use nssa_core::{BlockId, Timestamp};
|
||||
pub use storage::error::DbError;
|
||||
use testnet_initial_state::initial_state;
|
||||
|
||||
@ -139,6 +138,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
V03State::new_with_genesis_accounts(
|
||||
&init_accs.unwrap_or_default(),
|
||||
&initial_commitments.unwrap_or_default(),
|
||||
genesis_block.header.timestamp,
|
||||
)
|
||||
} else {
|
||||
initial_state()
|
||||
@ -163,28 +163,6 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
(sequencer_core, mempool_handle)
|
||||
}
|
||||
|
||||
fn execute_check_transaction_on_state(
|
||||
&mut self,
|
||||
tx: NSSATransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<NSSATransaction, nssa::error::NssaError> {
|
||||
match &tx {
|
||||
NSSATransaction::Public(tx) => self
|
||||
.state
|
||||
.transition_from_public_transaction(tx, block_id, timestamp),
|
||||
NSSATransaction::PrivacyPreserving(tx) => self
|
||||
.state
|
||||
.transition_from_privacy_preserving_transaction(tx, block_id, timestamp),
|
||||
NSSATransaction::ProgramDeployment(tx) => self
|
||||
.state
|
||||
.transition_from_program_deployment_transaction(tx),
|
||||
}
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
pub async fn produce_new_block(&mut self) -> Result<u64> {
|
||||
let (tx, _msg_id) = self
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
@ -224,12 +202,20 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
|
||||
.expect("Timestamp must be positive");
|
||||
|
||||
// Pre-create the mandatory clock tx so its size is included in the block size check.
|
||||
let clock_tx = clock_invocation(new_block_timestamp);
|
||||
let clock_nssa_tx = NSSATransaction::Public(clock_tx.clone());
|
||||
|
||||
while let Some(tx) = self.mempool.pop() {
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
// Check if block size exceeds limit
|
||||
let temp_valid_transactions =
|
||||
[valid_transactions.as_slice(), std::slice::from_ref(&tx)].concat();
|
||||
// Check if block size exceeds limit (including the mandatory clock tx).
|
||||
let temp_valid_transactions = [
|
||||
valid_transactions.as_slice(),
|
||||
std::slice::from_ref(&tx),
|
||||
std::slice::from_ref(&clock_nssa_tx),
|
||||
]
|
||||
.concat();
|
||||
let temp_hashable_data = HashableBlockData {
|
||||
block_id: new_block_height,
|
||||
transactions: temp_valid_transactions,
|
||||
@ -252,26 +238,35 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
|
||||
break;
|
||||
}
|
||||
|
||||
match self.execute_check_transaction_on_state(tx, new_block_height, new_block_timestamp)
|
||||
{
|
||||
Ok(valid_tx) => {
|
||||
valid_transactions.push(valid_tx);
|
||||
|
||||
info!("Validated transaction with hash {tx_hash}, including it in block");
|
||||
|
||||
if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let validated_diff = match tx.validate_on_state(
|
||||
&self.state,
|
||||
new_block_height,
|
||||
new_block_timestamp,
|
||||
) {
|
||||
Ok(diff) => diff,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it",
|
||||
);
|
||||
// TODO: Probably need to handle unsuccessful transaction execution?
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
self.state.apply_state_diff(validated_diff);
|
||||
|
||||
valid_transactions.push(tx);
|
||||
info!("Validated transaction with hash {tx_hash}, including it in block");
|
||||
if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Append the Clock Program invocation as the mandatory last transaction.
|
||||
self.state
|
||||
.transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp)
|
||||
.context("Clock transaction failed. Aborting block production.")?;
|
||||
valid_transactions.push(clock_nssa_tx);
|
||||
|
||||
let hashable_data = HashableBlockData {
|
||||
block_id: new_block_height,
|
||||
transactions: valid_transactions,
|
||||
@ -395,7 +390,10 @@ mod tests {
|
||||
use std::{pin::pin, time::Duration};
|
||||
|
||||
use bedrock_client::BackoffConfig;
|
||||
use common::{test_utils::sequencer_sign_key_for_testing, transaction::NSSATransaction};
|
||||
use common::{
|
||||
test_utils::sequencer_sign_key_for_testing,
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use mempool::MemPoolHandle;
|
||||
use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys};
|
||||
@ -524,7 +522,7 @@ mod tests {
|
||||
let tx = tx.transaction_stateless_check().unwrap();
|
||||
|
||||
// Signature is not from sender. Execution fails
|
||||
let result = sequencer.execute_check_transaction_on_state(tx, 0, 0);
|
||||
let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
@ -550,7 +548,9 @@ mod tests {
|
||||
// Passed pre-check
|
||||
assert!(result.is_ok());
|
||||
|
||||
let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0);
|
||||
let result = result
|
||||
.unwrap()
|
||||
.execute_check_on_state(&mut sequencer.state, 0, 0);
|
||||
let is_failed_at_balance_mismatch = matches!(
|
||||
result.err().unwrap(),
|
||||
nssa::error::NssaError::ProgramExecutionFailed(_)
|
||||
@ -572,8 +572,7 @@ mod tests {
|
||||
acc1, 0, acc2, 100, &sign_key1,
|
||||
);
|
||||
|
||||
sequencer
|
||||
.execute_check_transaction_on_state(tx, 0, 0)
|
||||
tx.execute_check_on_state(&mut sequencer.state, 0, 0)
|
||||
.unwrap();
|
||||
|
||||
let bal_from = sequencer.state.get_account_by_id(acc1).balance;
|
||||
@ -652,8 +651,14 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Only one should be included in the block
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
// Only one user tx should be included; the clock tx is always appended last.
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![
|
||||
tx.clone(),
|
||||
NSSATransaction::Public(clock_invocation(block.header.timestamp))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -679,7 +684,13 @@ mod tests {
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![
|
||||
tx.clone(),
|
||||
NSSATransaction::Public(clock_invocation(block.header.timestamp))
|
||||
]
|
||||
);
|
||||
|
||||
// Add same transaction should fail
|
||||
mempool_handle.push(tx.clone()).await.unwrap();
|
||||
@ -691,7 +702,13 @@ mod tests {
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(block.body.transactions.is_empty());
|
||||
// The replay is rejected, so only the clock tx is in the block.
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![NSSATransaction::Public(clock_invocation(
|
||||
block.header.timestamp
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@ -726,7 +743,13 @@ mod tests {
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(block.body.transactions, vec![tx.clone()]);
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![
|
||||
tx.clone(),
|
||||
NSSATransaction::Public(clock_invocation(block.header.timestamp))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Instantiating a new sequencer from the same config. This should load the existing block
|
||||
@ -856,8 +879,54 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
new_block.body.transactions,
|
||||
vec![tx],
|
||||
"New block should contain the submitted transaction"
|
||||
vec![
|
||||
tx,
|
||||
NSSATransaction::Public(clock_invocation(new_block.header.timestamp))
|
||||
],
|
||||
"New block should contain the submitted transaction and the clock invocation"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transactions_touching_clock_account_are_dropped_from_block() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
|
||||
// Canonical clock invocation and a crafted variant with a different timestamp — both must
|
||||
// be dropped because their diffs touch the clock accounts.
|
||||
let crafted_clock_tx = {
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
nssa::program::Program::clock().id(),
|
||||
nssa::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
||||
vec![],
|
||||
42_u64,
|
||||
)
|
||||
.unwrap();
|
||||
NSSATransaction::Public(nssa::PublicTransaction::new(
|
||||
message,
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
};
|
||||
mempool_handle
|
||||
.push(NSSATransaction::Public(clock_invocation(0)))
|
||||
.await
|
||||
.unwrap();
|
||||
mempool_handle.push(crafted_clock_tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Both transactions were dropped. Only the system-appended clock tx remains.
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![NSSATransaction::Public(clock_invocation(
|
||||
block.header.timestamp
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
@ -909,4 +978,86 @@ mod tests {
|
||||
"Chain height should NOT match the modified config.genesis_id"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_tx_that_chain_calls_clock_is_dropped() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
|
||||
// Deploy the clock_chain_caller test program.
|
||||
let deploy_tx =
|
||||
NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new(
|
||||
nssa::program_deployment_transaction::Message::new(
|
||||
test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec(),
|
||||
),
|
||||
));
|
||||
mempool_handle.push(deploy_tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
// Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the
|
||||
// clock program with the clock accounts. The sequencer should detect that the resulting
|
||||
// state diff modifies clock accounts and drop the transaction.
|
||||
let clock_chain_caller_id =
|
||||
nssa::program::Program::new(test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec())
|
||||
.unwrap()
|
||||
.id();
|
||||
let clock_program_id = nssa::program::Program::clock().id();
|
||||
let timestamp: u64 = 0;
|
||||
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
clock_chain_caller_id,
|
||||
nssa::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
||||
vec![], // no signers
|
||||
(clock_program_id, timestamp),
|
||||
)
|
||||
.unwrap();
|
||||
let user_tx = NSSATransaction::Public(nssa::PublicTransaction::new(
|
||||
message,
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
mempool_handle.push(user_tx).await.unwrap();
|
||||
sequencer
|
||||
.produce_new_block_with_mempool_transactions()
|
||||
.unwrap();
|
||||
|
||||
let block = sequencer
|
||||
.store
|
||||
.get_block_at_id(sequencer.chain_height)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// The user tx must have been dropped; only the mandatory clock invocation remains.
|
||||
assert_eq!(
|
||||
block.body.transactions,
|
||||
vec![NSSATransaction::Public(clock_invocation(
|
||||
block.header.timestamp
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn block_production_aborts_when_clock_account_data_is_corrupted() {
|
||||
let (mut sequencer, mempool_handle) = common_setup().await;
|
||||
|
||||
// Corrupt the clock 01 account data so the clock program panics on deserialization.
|
||||
let clock_account_id = nssa::CLOCK_01_PROGRAM_ACCOUNT_ID;
|
||||
let mut corrupted = sequencer.state.get_account_by_id(clock_account_id);
|
||||
corrupted.data = vec![0xff; 3].try_into().unwrap();
|
||||
sequencer
|
||||
.state
|
||||
.force_insert_account(clock_account_id, corrupted);
|
||||
|
||||
// Push a dummy transaction so the mempool is non-empty.
|
||||
let tx = common::test_utils::produce_dummy_empty_transaction();
|
||||
mempool_handle.push(tx).await.unwrap();
|
||||
|
||||
// Block production must fail because the appended clock tx cannot execute.
|
||||
let result = sequencer.produce_new_block_with_mempool_transactions();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Block production should abort when clock account data is corrupted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,7 +257,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -294,7 +294,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -347,7 +347,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -420,7 +420,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -503,7 +503,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -599,7 +599,7 @@ mod tests {
|
||||
let dbio = RocksDBIO::open_or_create(
|
||||
temdir_path,
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[], 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@ -9,5 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
|
||||
risc0-zkvm.workspace = true
|
||||
serde = { workspace = true, default-features = false }
|
||||
|
||||
39
test_program_methods/guest/src/bin/clock_chain_caller.rs
Normal file
39
test_program_methods/guest/src/bin/clock_chain_caller.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use nssa_core::{
|
||||
Timestamp,
|
||||
program::{
|
||||
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
},
|
||||
};
|
||||
use risc0_zkvm::serde::to_vec;
|
||||
|
||||
type Instruction = (ProgramId, Timestamp); // (clock_program_id, timestamp)
|
||||
|
||||
/// A program that chain-calls the clock program with the clock accounts it received as pre-states.
|
||||
/// Used in tests to verify that user transactions cannot modify clock accounts, even indirectly
|
||||
/// via chain calls.
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: (clock_program_id, timestamp),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let post_states: Vec<_> = pre_states
|
||||
.iter()
|
||||
.map(|pre| AccountPostState::new(pre.account.clone()))
|
||||
.collect();
|
||||
|
||||
let chained_call = ChainedCall {
|
||||
program_id: clock_program_id,
|
||||
instruction_data: to_vec(×tamp).unwrap(),
|
||||
pre_states: pre_states.clone(),
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
ProgramOutput::new(self_program_id, instruction_words, pre_states, post_states)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
114
test_program_methods/guest/src/bin/pinata_cooldown.rs
Normal file
114
test_program_methods/guest/src/bin/pinata_cooldown.rs
Normal file
@ -0,0 +1,114 @@
|
||||
//! Cooldown-based pinata program.
|
||||
//!
|
||||
//! A Piñata program that uses the on-chain clock to prevent abuse.
|
||||
//! After each prize claim the program records the current timestamp; the next claim is only
|
||||
//! allowed once a configurable cooldown period has elapsed.
|
||||
//!
|
||||
//! Expected pre-states (in order):
|
||||
//! 0 - pinata account (authorized, owned by this program)
|
||||
//! 1 - winner account
|
||||
//! 2 - clock account `CLOCK_01`.
|
||||
//!
|
||||
//! Pinata account data layout (24 bytes):
|
||||
//! [prize: u64 LE | `cooldown_ms`: u64 LE | `last_claim_timestamp`: u64 LE].
|
||||
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
type Instruction = ();
|
||||
|
||||
struct PinataState {
|
||||
prize: u128,
|
||||
cooldown_ms: u64,
|
||||
last_claim_timestamp: u64,
|
||||
}
|
||||
|
||||
impl PinataState {
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
assert!(bytes.len() >= 32, "Pinata account data too short");
|
||||
let prize = u128::from_le_bytes(bytes[..16].try_into().unwrap());
|
||||
let cooldown_ms = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
|
||||
let last_claim_timestamp = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
|
||||
Self {
|
||||
prize,
|
||||
cooldown_ms,
|
||||
last_claim_timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
buf.extend_from_slice(&self.prize.to_le_bytes());
|
||||
buf.extend_from_slice(&self.cooldown_ms.to_le_bytes());
|
||||
buf.extend_from_slice(&self.last_claim_timestamp.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: (),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([pinata, winner, clock_pre]) = <[_; 3]>::try_from(pre_states) else {
|
||||
panic!("Expected exactly 3 input accounts: pinata, winner, clock");
|
||||
};
|
||||
|
||||
// Check the clock account is the system clock account
|
||||
assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner());
|
||||
let current_timestamp = clock_data.timestamp;
|
||||
|
||||
let pinata_state = PinataState::from_bytes(&pinata.account.data.clone().into_inner());
|
||||
|
||||
// Enforce cooldown: the elapsed time since the last claim must exceed the cooldown period.
|
||||
let elapsed = current_timestamp.saturating_sub(pinata_state.last_claim_timestamp);
|
||||
assert!(
|
||||
elapsed >= pinata_state.cooldown_ms,
|
||||
"Cooldown not elapsed: {elapsed}ms since last claim, need {}ms",
|
||||
pinata_state.cooldown_ms,
|
||||
);
|
||||
|
||||
let mut pinata_post = pinata.account.clone();
|
||||
let mut winner_post = winner.account.clone();
|
||||
|
||||
pinata_post.balance = pinata_post
|
||||
.balance
|
||||
.checked_sub(pinata_state.prize)
|
||||
.expect("Not enough balance in the pinata");
|
||||
winner_post.balance = winner_post
|
||||
.balance
|
||||
.checked_add(pinata_state.prize)
|
||||
.expect("Overflow when adding prize to winner");
|
||||
|
||||
// Update the last claim timestamp.
|
||||
let updated_state = PinataState {
|
||||
last_claim_timestamp: current_timestamp,
|
||||
..pinata_state
|
||||
};
|
||||
pinata_post.data = updated_state
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("Pinata state should fit in account data");
|
||||
|
||||
// Clock account is read-only.
|
||||
let clock_post = clock_pre.account.clone();
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
instruction_words,
|
||||
vec![pinata, winner, clock_pre],
|
||||
vec![
|
||||
AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized),
|
||||
AccountPostState::new(winner_post),
|
||||
AccountPostState::new(clock_post),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
70
test_program_methods/guest/src/bin/time_locked_transfer.rs
Normal file
70
test_program_methods/guest/src/bin/time_locked_transfer.rs
Normal file
@ -0,0 +1,70 @@
|
||||
//! Time-locked transfer program.
|
||||
//!
|
||||
//! Demonstrates how a program can include a clock account among its inputs and use the on-chain
|
||||
//! timestamp in its logic. The transfer only executes when the clock timestamp is at or past a
|
||||
//! caller-supplied deadline; otherwise the program panics.
|
||||
//!
|
||||
//! Expected pre-states (in order):
|
||||
//! 0 - sender account (authorized)
|
||||
//! 1 - receiver account
|
||||
//! 2 - clock account (read-only, e.g. `CLOCK_01`).
|
||||
|
||||
use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData};
|
||||
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
/// (`amount`, `deadline_timestamp`).
|
||||
type Instruction = (u128, u64);
|
||||
|
||||
fn main() {
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
pre_states,
|
||||
instruction: (amount, deadline),
|
||||
},
|
||||
instruction_words,
|
||||
) = read_nssa_inputs::<Instruction>();
|
||||
|
||||
let Ok([sender_pre, receiver_pre, clock_pre]) = <[_; 3]>::try_from(pre_states) else {
|
||||
panic!("Expected exactly 3 input accounts: sender, receiver, clock");
|
||||
};
|
||||
|
||||
// Check the clock account is the system clock account
|
||||
assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID);
|
||||
|
||||
// Read the current timestamp from the clock account.
|
||||
let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner());
|
||||
|
||||
assert!(
|
||||
clock_data.timestamp >= deadline,
|
||||
"Transfer is time-locked until timestamp {deadline}, current is {}",
|
||||
clock_data.timestamp,
|
||||
);
|
||||
|
||||
let mut sender_post = sender_pre.account.clone();
|
||||
let mut receiver_post = receiver_pre.account.clone();
|
||||
|
||||
sender_post.balance = sender_post
|
||||
.balance
|
||||
.checked_sub(amount)
|
||||
.expect("Insufficient balance");
|
||||
receiver_post.balance = receiver_post
|
||||
.balance
|
||||
.checked_add(amount)
|
||||
.expect("Balance overflow");
|
||||
|
||||
// Clock account is read-only: post state equals pre state.
|
||||
let clock_post = clock_pre.account.clone();
|
||||
|
||||
ProgramOutput::new(
|
||||
self_program_id,
|
||||
instruction_words,
|
||||
vec![sender_pre, receiver_pre, clock_pre],
|
||||
vec![
|
||||
AccountPostState::new(sender_post),
|
||||
AccountPostState::new(receiver_post),
|
||||
AccountPostState::new(clock_post),
|
||||
],
|
||||
)
|
||||
.write();
|
||||
}
|
||||
@ -214,7 +214,7 @@ pub fn initial_state() -> V03State {
|
||||
.map(|acc_data| (acc_data.account_id, acc_data.balance))
|
||||
.collect();
|
||||
|
||||
nssa::V03State::new_with_genesis_accounts(&init_accs, &initial_commitments)
|
||||
nssa::V03State::new_with_genesis_accounts(&init_accs, &initial_commitments, 0)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user