mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-08 20:23:47 +00:00
Merge pull request #403 from logos-blockchain/schouhy/add-block-context-system-accounts
Add block context system accounts
This commit is contained in:
commit
98e98c6214
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",
|
||||
]
|
||||
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
Binary file not shown.
Binary file not shown.
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.
Binary file not shown.
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,
|
||||
},
|
||||
};
|
||||
|
||||
@ -115,6 +116,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())
|
||||
@ -313,6 +319,18 @@ mod tests {
|
||||
use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF;
|
||||
Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap()
|
||||
}
|
||||
|
||||
#[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,217 +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(&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 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)]
|
||||
@ -289,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) {
|
||||
@ -302,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 {
|
||||
@ -397,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(_))));
|
||||
}
|
||||
|
||||
@ -417,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(_))));
|
||||
}
|
||||
|
||||
@ -438,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(_))));
|
||||
}
|
||||
|
||||
@ -458,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(_))));
|
||||
}
|
||||
|
||||
@ -474,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 {
|
||||
@ -382,6 +400,8 @@ pub mod tests {
|
||||
self.insert_program(Program::claimer());
|
||||
self.insert_program(Program::changer_claimer());
|
||||
self.insert_program(Program::validity_window());
|
||||
self.insert_program(Program::time_locked_transfer());
|
||||
self.insert_program(Program::pinata_cooldown());
|
||||
self
|
||||
}
|
||||
|
||||
@ -485,6 +505,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(
|
||||
@ -503,6 +524,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 = {
|
||||
@ -511,13 +542,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);
|
||||
@ -525,7 +557,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));
|
||||
@ -540,7 +572,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);
|
||||
@ -551,7 +583,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);
|
||||
@ -561,7 +593,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();
|
||||
|
||||
@ -573,7 +605,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));
|
||||
@ -594,7 +626,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();
|
||||
@ -619,7 +651,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;
|
||||
@ -643,7 +675,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;
|
||||
@ -678,11 +710,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 =
|
||||
@ -699,7 +874,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 =
|
||||
@ -716,7 +891,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 =
|
||||
@ -733,7 +908,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
|
||||
@ -756,7 +931,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]);
|
||||
@ -780,7 +955,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]);
|
||||
@ -804,7 +979,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]);
|
||||
@ -829,7 +1004,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;
|
||||
@ -856,7 +1031,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]);
|
||||
@ -882,7 +1057,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();
|
||||
|
||||
@ -899,7 +1074,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();
|
||||
@ -1091,7 +1266,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;
|
||||
|
||||
@ -1139,7 +1314,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;
|
||||
@ -1209,6 +1384,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);
|
||||
|
||||
@ -2160,7 +2336,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;
|
||||
@ -2245,7 +2421,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;
|
||||
@ -2283,7 +2459,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());
|
||||
|
||||
@ -2304,7 +2480,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());
|
||||
|
||||
@ -2339,7 +2515,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>) = (
|
||||
@ -2384,7 +2560,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>) = (
|
||||
@ -2422,7 +2598,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,
|
||||
@ -2468,7 +2644,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;
|
||||
@ -2543,7 +2719,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 =
|
||||
@ -2626,6 +2802,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;
|
||||
@ -2732,7 +2909,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
|
||||
@ -2804,7 +2981,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
|
||||
@ -2845,6 +3022,7 @@ pub mod tests {
|
||||
(recipient_id, recipient_init_balance),
|
||||
],
|
||||
&[],
|
||||
0,
|
||||
);
|
||||
|
||||
state.insert_program(Program::modified_transfer_program());
|
||||
@ -2894,7 +3072,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();
|
||||
@ -2946,7 +3124,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,
|
||||
@ -2993,7 +3171,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();
|
||||
@ -3074,7 +3252,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)
|
||||
@ -3098,7 +3276,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
|
||||
@ -3195,6 +3373,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();
|
||||
|
||||
@ -3244,7 +3423,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![];
|
||||
@ -3296,7 +3475,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![];
|
||||
@ -3349,7 +3528,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());
|
||||
@ -3418,7 +3597,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());
|
||||
@ -3467,12 +3646,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,6 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
|
||||
risc0-zkvm.workspace = true
|
||||
|
||||
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