Merge branch 'main' into marvin/pq-public-keys

This commit is contained in:
jonesmarvin8 2026-03-31 19:01:15 -04:00
commit 900b7b8d53
77 changed files with 1231 additions and 555 deletions

31
Cargo.lock generated
View File

@ -1019,19 +1019,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitcoin-io"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
[[package]]
name = "bitcoin_hashes"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b"
dependencies = [
"bitcoin-io",
"hex-conservative",
]
@ -1522,6 +1515,7 @@ dependencies = [
"log",
"logos-blockchain-common-http-client",
"nssa",
"nssa_core",
"serde",
"serde_with",
"sha2",
@ -3977,7 +3971,6 @@ dependencies = [
"nssa",
"nssa_core",
"rand 0.8.5",
"secp256k1",
"serde",
"sha2",
"thiserror 2.0.18",
@ -5269,13 +5262,13 @@ dependencies = [
"env_logger",
"hex",
"hex-literal 1.1.0",
"k256",
"log",
"nssa_core",
"rand 0.8.5",
"risc0-binfmt",
"risc0-build",
"risc0-zkvm",
"secp256k1",
"serde",
"serde_with",
"sha2",
@ -7086,26 +7079,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "secp256k1"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2"
dependencies = [
"bitcoin_hashes",
"rand 0.9.2",
"secp256k1-sys",
]
[[package]]
name = "secp256k1-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38"
dependencies = [
"cc",
]
[[package]]
name = "security-framework"
version = "3.7.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies]
nssa.workspace = true
nssa_core.workspace = true
anyhow.workspace = true
thiserror.workspace = true

View File

@ -1,4 +1,5 @@
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
@ -6,8 +7,6 @@ use crate::{HashType, transaction::NSSATransaction};
pub type MantleMsgId = [u8; 32];
pub type BlockHash = HashType;
pub type BlockId = u64;
pub type TimeStamp = u64;
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct BlockMeta {
@ -35,7 +34,7 @@ pub struct BlockHeader {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,
pub hash: BlockHash,
pub timestamp: TimeStamp,
pub timestamp: Timestamp,
pub signature: nssa::Signature,
}
@ -75,7 +74,7 @@ impl<'de> Deserialize<'de> for Block {
pub struct HashableBlockData {
pub block_id: BlockId,
pub prev_block_hash: BlockHash,
pub timestamp: TimeStamp,
pub timestamp: Timestamp,
pub transactions: Vec<NSSATransaction>,
}

View File

@ -1,9 +1,10 @@
use borsh::{BorshDeserialize, BorshSerialize};
use log::warn;
use nssa::{AccountId, V03State};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize};
use crate::{HashType, block::BlockId};
use crate::HashType;
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum NSSATransaction {
@ -69,11 +70,12 @@ impl NSSATransaction {
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),
Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp),
Self::PrivacyPreserving(tx) => {
state.transition_from_privacy_preserving_transaction(tx, block_id)
state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp)
}
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
}

View File

@ -1,6 +1,4 @@
use nssa_core::program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
// Hello-world example program.
//
@ -45,13 +43,7 @@ fn main() {
// Wrap the post state account values inside a `AccountPostState` instance.
// This is used to forward the account claiming request if any
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
// This produces a claim request
AccountPostState::new_claimed(post_account)
} else {
// This doesn't produce a claim request
AccountPostState::new(post_account)
};
let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
// The output is a proposed state difference. It will only succeed if the pre states coincide
// with the previous values of the accounts, and the transition to the post states conforms

View File

@ -1,6 +1,4 @@
use nssa_core::program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
// Hello-world with authorization example program.
//
@ -52,13 +50,7 @@ fn main() {
// Wrap the post state account values inside a `AccountPostState` instance.
// This is used to forward the account claiming request if any
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
// This produces a claim request
AccountPostState::new_claimed(post_account)
} else {
// This doesn't produce a claim request
AccountPostState::new(post_account)
};
let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
// The output is a proposed state difference. It will only succeed if the pre states coincide
// with the previous values of the accounts, and the transition to the post states conforms

View File

@ -1,8 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
},
account::{AccountWithMetadata, Data},
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs},
};
// Hello-world with write + move_data example program.
@ -26,16 +24,6 @@ const MOVE_DATA_FUNCTION_ID: u8 = 1;
type Instruction = (u8, Vec<u8>);
fn build_post_state(post_account: Account) -> AccountPostState {
if post_account.program_owner == DEFAULT_PROGRAM_ID {
// This produces a claim request
AccountPostState::new_claimed(post_account)
} else {
// This doesn't produce a claim request
AccountPostState::new(post_account)
}
}
fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
// Construct the post state account values
let post_account = {
@ -48,7 +36,7 @@ fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
this
};
build_post_state(post_account)
AccountPostState::new_claimed_if_default(post_account, Claim::Authorized)
}
fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<AccountPostState> {
@ -58,7 +46,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<
let from_post = {
let mut this = from_pre.account;
this.data = Data::default();
build_post_state(this)
AccountPostState::new_claimed_if_default(this, Claim::Authorized)
};
let to_post = {
@ -68,7 +56,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<
this.data = bytes
.try_into()
.expect("Data should fit within the allowed limits");
build_post_state(this)
AccountPostState::new_claimed_if_default(this, Claim::Authorized)
};
vec![from_post, to_post]

View File

@ -177,13 +177,13 @@ pub fn TransactionPage() -> impl IntoView {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window
block_validity_window,
timestamp_validity_window,
} = message;
let WitnessSet {
signatures_and_public_keys: _,
proof,
} = witness_set;
let proof_len = proof.map_or(0, |p| p.0.len());
view! {
<div class="transaction-details">
@ -214,8 +214,12 @@ pub fn TransactionPage() -> impl IntoView {
<span class="info-value">{format!("{proof_len} bytes")}</span>
</div>
<div class="info-row">
<span class="info-label">"Validity Window:"</span>
<span class="info-value">{validity_window.to_string()}</span>
<span class="info-label">"Block Validity Window:"</span>
<span class="info-value">{block_validity_window.to_string()}</span>
</div>
<div class="info-row">
<span class="info-label">"Timestamp Validity Window:"</span>
<span class="info-value">{timestamp_validity_window.to_string()}</span>
</div>
</div>

View File

@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc};
use anyhow::Result;
use bedrock_client::HeaderId;
use common::{
block::{BedrockStatus, Block, BlockId},
block::{BedrockStatus, Block},
transaction::NSSATransaction,
};
use nssa::{Account, AccountId, V03State};
use nssa_core::BlockId;
use storage::indexer::RocksDBIO;
use tokio::sync::RwLock;
@ -125,7 +126,11 @@ impl IndexerStore {
transaction
.clone()
.transaction_stateless_check()?
.execute_check_on_state(&mut state_guard, block.header.block_id)?;
.execute_check_on_state(
&mut state_guard,
block.header.block_id,
block.header.timestamp,
)?;
}
}

View File

@ -287,7 +287,8 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
block_validity_window,
timestamp_validity_window,
} = value;
Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -302,7 +303,8 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.collect(),
validity_window: validity_window.into(),
block_validity_window: block_validity_window.into(),
timestamp_validity_window: timestamp_validity_window.into(),
}
}
}
@ -318,7 +320,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window,
block_validity_window,
timestamp_validity_window,
} = value;
Ok(Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -340,7 +343,10 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
.into_iter()
.map(|(n, d)| (n.into(), d.into()))
.collect(),
validity_window: validity_window
block_validity_window: block_validity_window
.try_into()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
timestamp_validity_window: timestamp_validity_window
.try_into()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
})
@ -692,13 +698,13 @@ impl From<HashType> for common::HashType {
// ValidityWindow conversions
// ============================================================================
impl From<nssa_core::program::ValidityWindow> for ValidityWindow {
fn from(value: nssa_core::program::ValidityWindow) -> Self {
impl From<nssa_core::program::ValidityWindow<u64>> for ValidityWindow {
fn from(value: nssa_core::program::ValidityWindow<u64>) -> Self {
Self((value.start(), value.end()))
}
}
impl TryFrom<ValidityWindow> for nssa_core::program::ValidityWindow {
impl TryFrom<ValidityWindow> for nssa_core::program::ValidityWindow<u64> {
type Error = nssa_core::program::InvalidWindow;
fn try_from(value: ValidityWindow) -> Result<Self, Self::Error> {

View File

@ -235,7 +235,8 @@ pub struct PrivacyPreservingMessage {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: ValidityWindow,
pub timestamp_validity_window: ValidityWindow,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]

View File

@ -124,7 +124,8 @@ impl MockIndexerService {
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]),
)],
validity_window: ValidityWindow((None, None)),
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
},
witness_set: WitnessSet {
signatures_and_public_keys: vec![],

View File

@ -11,10 +11,13 @@ use integration_tests::{
NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
};
use log::info;
use nssa::{AccountId, program::Program};
use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::Command;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
};
#[test]
async fn deploy_and_execute_program() -> Result<()> {
@ -40,14 +43,31 @@ async fn deploy_and_execute_program() -> Result<()> {
// logic)
let bytecode = std::fs::read(binary_filepath)?;
let data_changer = Program::new(bytecode)?;
let account_id: AccountId = "11".repeat(16).parse()?;
let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
panic!("Expected RegisterAccount return value");
};
let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?;
let private_key = ctx
.wallet()
.get_account_public_signing_key(account_id)
.unwrap();
let message = nssa::public_transaction::Message::try_new(
data_changer.id(),
vec![account_id],
vec![],
nonces,
vec![0],
)?;
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[private_key]);
let transaction = nssa::PublicTransaction::new(message, witness_set);
let _response = ctx
.sequencer_client()
@ -64,7 +84,7 @@ async fn deploy_and_execute_program() -> Result<()> {
assert_eq!(post_state_account.program_owner, data_changer.id());
assert_eq!(post_state_account.balance, 0);
assert_eq!(post_state_account.data.as_ref(), &[0]);
assert_eq!(post_state_account.nonce.0, 0);
assert_eq!(post_state_account.nonce.0, 1);
info!("Successfully deployed and executed program");

View File

@ -924,7 +924,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
let home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
let to = FfiBytes32::from_bytes([37; 32]);
let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
let amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default();
@ -967,7 +967,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
};
assert_eq!(from_balance, 9900);
assert_eq!(to_balance, 100);
assert_eq!(to_balance, 10100);
unsafe {
wallet_ffi_free_transfer_result(&raw mut transfer_result);

View File

@ -8,8 +8,6 @@ license = { workspace = true }
workspace = true
[dependencies]
secp256k1 = "0.31.1"
nssa.workspace = true
nssa_core.workspace = true
common.workspace = true

View File

@ -137,11 +137,12 @@ impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
#[cfg(test)]
mod tests {
use nssa_core::NullifierSecretKey;
use nssa_core::{NullifierPublicKey, NullifierSecretKey};
use super::*;
use crate::key_management::{self, secret_holders::ViewingSecretKey};
#[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")]
#[test]
fn master_key_generation() {
let seed: [u8; 64] = [
@ -153,7 +154,7 @@ mod tests {
let keys = ChildKeysPrivate::root(seed);
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([
246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139,
222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191,
]);
@ -168,7 +169,7 @@ mod tests {
34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163,
];
let expected_npk = nssa_core::NullifierPublicKey([
let expected_npk: NullifierPublicKey = nssa_core::NullifierPublicKey([
7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97,
247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2,
]);

View File

@ -1,4 +1,4 @@
use secp256k1::Scalar;
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyNode;
@ -14,7 +14,6 @@ pub struct ChildKeysPublic {
}
impl ChildKeysPublic {
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![];
@ -22,16 +21,17 @@ impl ChildKeysPublic {
// Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`.
let sk = secp256k1::SecretKey::from_byte_array(*self.cssk.value())
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
.expect("32 bytes, within curve order");
let pk = secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk);
hash_input.extend_from_slice(&secp256k1::PublicKey::serialize(&pk));
let pk = sk.public_key();
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else {
// Harden.
hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.cssk.value());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc)
@ -42,8 +42,14 @@ impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let cssk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
let cssk = nssa::PrivateKey::try_new(
*hash_value
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32"),
)
.expect("Expect a valid Private Key");
let csk = nssa::PrivateKey::tweak(cssk.value()).unwrap();
let ccc = *hash_value.last_chunk::<32>().unwrap();
let cpk = nssa::PublicKey::new_from_private_key(&csk);
@ -65,7 +71,7 @@ impl KeyNode for ChildKeysPublic {
.expect("hash_value is 64 bytes, must be safe to get first 32"),
)
.unwrap();
/*
let cssk = nssa::PrivateKey::try_new({
cssk.add_tweak(&Scalar::from_be_bytes(*self.cssk.value()).unwrap())
.expect("Expect a valid Scalar")
@ -79,6 +85,21 @@ impl KeyNode for ChildKeysPublic {
secp256k1::constants::CURVE_ORDER >= *csk.value(),
"Secret key cannot exceed curve order"
);
*/
let csk = nssa::PrivateKey::try_new({
let hash_value = hash_value
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32");
let value_1 =
k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar");
let value_2 = k256::Scalar::from_repr((*self.csk.value()).into())
.expect("Expect a valid k256 scalar");
let sum = value_1.add(&value_2);
sum.to_bytes().into()
})
.expect("Expect a valid private key");
let ccc = *hash_value
.last_chunk::<32>()

View File

@ -19,7 +19,7 @@ sha2.workspace = true
rand.workspace = true
borsh.workspace = true
hex.workspace = true
secp256k1 = "0.31.1"
k256.workspace = true
risc0-binfmt = "3.0.2"
log.workspace = true

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata},
encryption::Ciphertext,
program::{ProgramId, ProgramOutput, ValidityWindow},
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
};
#[derive(Serialize, Deserialize)]
@ -36,7 +36,8 @@ pub struct PrivacyPreservingCircuitOutput {
pub ciphertexts: Vec<Ciphertext>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow,
}
#[cfg(feature = "host")]
@ -102,7 +103,8 @@ mod tests {
),
[0xab; 32],
)],
validity_window: (Some(1), None).try_into().unwrap(),
block_validity_window: (1..).into(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
};
let bytes = output.to_bytes();
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();

View File

@ -21,3 +21,7 @@ pub mod program;
#[cfg(feature = "host")]
pub mod error;
pub type BlockId = u64;
/// Unix timestamp in milliseconds.
pub type Timestamp = u64;

View File

@ -5,7 +5,10 @@ use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize};
use crate::account::{Account, AccountId, AccountWithMetadata};
use crate::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
};
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
@ -22,7 +25,7 @@ pub struct ProgramInput<T> {
/// Each program can derive up to `2^256` unique account IDs by choosing different
/// seeds. PDAs allow programs to control namespaced account identifiers without
/// collisions between programs.
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub struct PdaSeed([u8; 32]);
impl PdaSeed {
@ -91,11 +94,26 @@ impl ChainedCall {
/// A post state may optionally request that the executing program
/// becomes the owner of the account (a “claim”). This is used to signal
/// that the program intends to take ownership of the account.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))]
pub struct AccountPostState {
account: Account,
claim: bool,
claim: Option<Claim>,
}
/// A claim request for an account, indicating that the executing program intends to take ownership
/// of the account.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Claim {
/// The program requests ownership of the account which was authorized by the signer.
///
/// Note that it's possible to successfully execute program outputting [`AccountPostState`] with
/// `is_authorized == false` and `claim == Some(Claim::Authorized)`.
/// This will give no error if program had authorization in pre state and may be useful
/// if program decides to give up authorization for a chained call.
Authorized,
/// The program requests ownership of the account through a PDA.
Pda(PdaSeed),
}
impl AccountPostState {
@ -105,7 +123,7 @@ impl AccountPostState {
pub const fn new(account: Account) -> Self {
Self {
account,
claim: false,
claim: None,
}
}
@ -113,25 +131,27 @@ impl AccountPostState {
/// This indicates that the executing program intends to claim the
/// account as its own and is allowed to mutate it.
#[must_use]
pub const fn new_claimed(account: Account) -> Self {
pub const fn new_claimed(account: Account, claim: Claim) -> Self {
Self {
account,
claim: true,
claim: Some(claim),
}
}
/// Creates a post state that requests ownership of the account
/// if the account's program owner is the default program ID.
#[must_use]
pub fn new_claimed_if_default(account: Account) -> Self {
let claim = account.program_owner == DEFAULT_PROGRAM_ID;
Self { account, claim }
pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self {
let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID;
Self {
account,
claim: is_default_owner.then_some(claim),
}
}
/// Returns `true` if this post state requests that the account
/// be claimed (owned) by the executing program.
/// Returns whether this post state requires a claim.
#[must_use]
pub const fn requires_claim(&self) -> bool {
pub const fn required_claim(&self) -> Option<Claim> {
self.claim
}
@ -142,6 +162,7 @@ impl AccountPostState {
}
/// Returns the underlying account.
#[must_use]
pub const fn account_mut(&mut self) -> &mut Account {
&mut self.account
}
@ -153,20 +174,21 @@ impl AccountPostState {
}
}
pub type BlockId = u64;
pub type BlockValidityWindow = ValidityWindow<BlockId>;
pub type TimestampValidityWindow = ValidityWindow<Timestamp>;
#[derive(Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(
any(feature = "host", test),
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
)]
pub struct ValidityWindow {
from: Option<BlockId>,
to: Option<BlockId>,
pub struct ValidityWindow<T> {
from: Option<T>,
to: Option<T>,
}
impl ValidityWindow {
/// Creates a window with no bounds, valid for every block ID.
impl<T> ValidityWindow<T> {
/// Creates a window with no bounds.
#[must_use]
pub const fn new_unbounded() -> Self {
Self {
@ -174,42 +196,42 @@ impl ValidityWindow {
to: None,
}
}
}
/// Returns `true` if `id` falls within the half-open range `[from, to)`.
/// A `None` bound on either side is treated as unbounded in that direction.
impl<T: Copy + PartialOrd> ValidityWindow<T> {
/// Valid for values in the range [from, to), where `from` is included and `to` is excluded.
#[must_use]
pub fn is_valid_for_block_id(&self, id: BlockId) -> bool {
self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end)
pub fn is_valid_for(&self, value: T) -> bool {
self.from.is_none_or(|start| value >= start) && self.to.is_none_or(|end| value < end)
}
/// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`.
const fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from_id), Some(until_id)) = (self.from, self.to)
&& from_id >= until_id
fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from), Some(to)) = (self.from, self.to)
&& from >= to
{
Err(InvalidWindow)
} else {
Ok(())
return Err(InvalidWindow);
}
Ok(())
}
/// Inclusive lower bound. `None` means the window starts at the beginning of the chain.
/// Inclusive lower bound. `None` means no lower bound.
#[must_use]
pub const fn start(&self) -> Option<BlockId> {
pub const fn start(&self) -> Option<T> {
self.from
}
/// Exclusive upper bound. `None` means the window has no expiry.
/// Exclusive upper bound. `None` means no upper bound.
#[must_use]
pub const fn end(&self) -> Option<BlockId> {
pub const fn end(&self) -> Option<T> {
self.to
}
}
impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
impl<T: Copy + PartialOrd> TryFrom<(Option<T>, Option<T>)> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: (Option<BlockId>, Option<BlockId>)) -> Result<Self, Self::Error> {
fn try_from(value: (Option<T>, Option<T>)) -> Result<Self, Self::Error> {
let this = Self {
from: value.0,
to: value.1,
@ -219,16 +241,16 @@ impl TryFrom<(Option<BlockId>, Option<BlockId>)> for ValidityWindow {
}
}
impl TryFrom<std::ops::Range<BlockId>> for ValidityWindow {
impl<T: Copy + PartialOrd> TryFrom<std::ops::Range<T>> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: std::ops::Range<BlockId>) -> Result<Self, Self::Error> {
fn try_from(value: std::ops::Range<T>) -> Result<Self, Self::Error> {
(Some(value.start), Some(value.end)).try_into()
}
}
impl From<std::ops::RangeFrom<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeFrom<BlockId>) -> Self {
impl<T: Copy + PartialOrd> From<std::ops::RangeFrom<T>> for ValidityWindow<T> {
fn from(value: std::ops::RangeFrom<T>) -> Self {
Self {
from: Some(value.start),
to: None,
@ -236,8 +258,8 @@ impl From<std::ops::RangeFrom<BlockId>> for ValidityWindow {
}
}
impl From<std::ops::RangeTo<BlockId>> for ValidityWindow {
fn from(value: std::ops::RangeTo<BlockId>) -> Self {
impl<T: Copy + PartialOrd> From<std::ops::RangeTo<T>> for ValidityWindow<T> {
fn from(value: std::ops::RangeTo<T>) -> Self {
Self {
from: None,
to: Some(value.end),
@ -245,7 +267,7 @@ impl From<std::ops::RangeTo<BlockId>> for ValidityWindow {
}
}
impl From<std::ops::RangeFull> for ValidityWindow {
impl<T> From<std::ops::RangeFull> for ValidityWindow<T> {
fn from(_: std::ops::RangeFull) -> Self {
Self::new_unbounded()
}
@ -267,8 +289,10 @@ pub struct ProgramOutput {
pub post_states: Vec<AccountPostState>,
/// The list of chained calls to other programs.
pub chained_calls: Vec<ChainedCall>,
/// The window where the program output is valid.
pub validity_window: ValidityWindow,
/// The block ID window where the program output is valid.
pub block_validity_window: BlockValidityWindow,
/// The timestamp window where the program output is valid.
pub timestamp_validity_window: TimestampValidityWindow,
}
impl ProgramOutput {
@ -282,7 +306,8 @@ impl ProgramOutput {
pre_states,
post_states,
chained_calls: Vec::new(),
validity_window: ValidityWindow::new_unbounded(),
block_validity_window: ValidityWindow::new_unbounded(),
timestamp_validity_window: ValidityWindow::new_unbounded(),
}
}
@ -295,19 +320,52 @@ impl ProgramOutput {
self
}
/// Sets the validity window from an infallible range conversion (`1..`, `..5`, `..`).
pub fn with_validity_window<W: Into<ValidityWindow>>(mut self, window: W) -> Self {
self.validity_window = window.into();
/// Sets the block ID validity window from an infallible range conversion (`1..`, `..5`, `..`).
pub fn with_block_validity_window<W: Into<BlockValidityWindow>>(mut self, window: W) -> Self {
self.block_validity_window = window.into();
self
}
/// Sets the validity window from a fallible range conversion (`1..5`).
/// Sets the block ID validity window from a fallible range conversion (`1..5`).
/// Returns `Err` if the range is empty.
pub fn try_with_validity_window<W: TryInto<ValidityWindow, Error = InvalidWindow>>(
pub fn try_with_block_validity_window<
W: TryInto<BlockValidityWindow, Error = InvalidWindow>,
>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
self.validity_window = window.try_into()?;
self.block_validity_window = window.try_into()?;
Ok(self)
}
/// Sets the timestamp validity window from an infallible range conversion.
pub fn with_timestamp_validity_window<W: Into<TimestampValidityWindow>>(
mut self,
window: W,
) -> Self {
self.timestamp_validity_window = window.into();
self
}
/// Sets the timestamp validity window from a fallible range conversion.
/// Returns `Err` if the range is empty.
pub fn try_with_timestamp_validity_window<
W: TryInto<TimestampValidityWindow, Error = InvalidWindow>,
>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = window.try_into()?;
Ok(self)
}
pub fn valid_from_timestamp(mut self, ts: Option<Timestamp>) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = (ts, self.timestamp_validity_window.end()).try_into()?;
Ok(self)
}
pub fn valid_until_timestamp(mut self, ts: Option<Timestamp>) -> Result<Self, InvalidWindow> {
self.timestamp_validity_window = (self.timestamp_validity_window.start(), ts).try_into()?;
Ok(self)
}
}
@ -464,128 +522,131 @@ mod tests {
use super::*;
#[test]
fn validity_window_unbounded_accepts_any_block() {
let w = ValidityWindow::new_unbounded();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(u64::MAX));
fn validity_window_unbounded_accepts_any_value() {
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
assert!(w.is_valid_for(0));
assert!(w.is_valid_for(u64::MAX));
}
#[test]
fn validity_window_bounded_range_includes_from_excludes_to() {
let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(9));
assert!(!w.is_valid_for_block_id(10));
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
assert!(!w.is_valid_for(4));
assert!(w.is_valid_for(5));
assert!(w.is_valid_for(9));
assert!(!w.is_valid_for(10));
}
#[test]
fn validity_window_only_from_bound() {
let w: ValidityWindow = (Some(5), None).try_into().unwrap();
assert!(!w.is_valid_for_block_id(4));
assert!(w.is_valid_for_block_id(5));
assert!(w.is_valid_for_block_id(u64::MAX));
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
assert!(!w.is_valid_for(4));
assert!(w.is_valid_for(5));
assert!(w.is_valid_for(u64::MAX));
}
#[test]
fn validity_window_only_to_bound() {
let w: ValidityWindow = (None, Some(5)).try_into().unwrap();
assert!(w.is_valid_for_block_id(0));
assert!(w.is_valid_for_block_id(4));
assert!(!w.is_valid_for_block_id(5));
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
assert!(w.is_valid_for(0));
assert!(w.is_valid_for(4));
assert!(!w.is_valid_for(5));
}
#[test]
fn validity_window_adjacent_bounds_are_invalid() {
// [5, 5) is an empty range — from == to
assert!(ValidityWindow::try_from((Some(5), Some(5))).is_err());
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
}
#[test]
fn validity_window_inverted_bounds_are_invalid() {
assert!(ValidityWindow::try_from((Some(10), Some(5))).is_err());
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
}
#[test]
fn validity_window_getters_match_construction() {
let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap();
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
assert_eq!(w.start(), Some(3));
assert_eq!(w.end(), Some(7));
}
#[test]
fn validity_window_getters_for_unbounded() {
let w = ValidityWindow::new_unbounded();
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range() {
let w = ValidityWindow::try_from(5_u64..10).unwrap();
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_empty_is_invalid() {
assert!(ValidityWindow::try_from(5_u64..5).is_err());
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
}
#[test]
fn validity_window_from_range_inverted_is_invalid() {
let from = 10_u64;
let to = 5_u64;
assert!(ValidityWindow::try_from(from..to).is_err());
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
}
#[test]
fn validity_window_from_range_from() {
let w: ValidityWindow = (5_u64..).into();
let w: ValidityWindow<u64> = (5_u64..).into();
assert_eq!(w.start(), Some(5));
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range_to() {
let w: ValidityWindow = (..10_u64).into();
let w: ValidityWindow<u64> = (..10_u64).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), Some(10));
}
#[test]
fn validity_window_from_range_full() {
let w: ValidityWindow = (..).into();
let w: ValidityWindow<u64> = (..).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn program_output_try_with_validity_window_range() {
fn program_output_try_with_block_validity_window_range() {
let output = ProgramOutput::new(vec![], vec![], vec![])
.try_with_validity_window(10_u64..100)
.try_with_block_validity_window(10_u64..100)
.unwrap();
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), Some(100));
assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), Some(100));
}
#[test]
fn program_output_with_validity_window_range_from() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(10_u64..);
assert_eq!(output.validity_window.start(), Some(10));
assert_eq!(output.validity_window.end(), None);
fn program_output_with_block_validity_window_range_from() {
let output =
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..);
assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), None);
}
#[test]
fn program_output_with_validity_window_range_to() {
let output = ProgramOutput::new(vec![], vec![], vec![]).with_validity_window(..100_u64);
assert_eq!(output.validity_window.start(), None);
assert_eq!(output.validity_window.end(), Some(100));
fn program_output_with_block_validity_window_range_to() {
let output =
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(..100_u64);
assert_eq!(output.block_validity_window.start(), None);
assert_eq!(output.block_validity_window.end(), Some(100));
}
#[test]
fn program_output_try_with_validity_window_empty_range_fails() {
let result = ProgramOutput::new(vec![], vec![], vec![]).try_with_validity_window(5_u64..5);
fn program_output_try_with_block_validity_window_empty_range_fails() {
let result =
ProgramOutput::new(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5);
assert!(result.is_err());
}
@ -598,10 +659,10 @@ mod tests {
nonce: 10_u128.into(),
};
let account_post_state = AccountPostState::new_claimed(account.clone());
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
assert_eq!(account, account_post_state.account);
assert!(account_post_state.requires_claim());
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
}
#[test]
@ -616,7 +677,7 @@ mod tests {
let account_post_state = AccountPostState::new(account.clone());
assert_eq!(account, account_post_state.account);
assert!(!account_post_state.requires_claim());
assert!(account_post_state.required_claim().is_none());
}
#[test]

View File

@ -29,7 +29,10 @@ pub enum NssaError {
Io(#[from] io::Error),
#[error("Invalid Public Key")]
InvalidPublicKey(#[source] secp256k1::Error),
InvalidPublicKey(#[source] k256::schnorr::Error),
#[error("Invalid hex for public key")]
InvalidHexPublicKey(hex::FromHexError),
#[error("Risc0 error: {0}")]
ProgramWriteInputFailed(String),

View File

@ -3,7 +3,7 @@ use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
program::ValidityWindow,
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -53,7 +53,8 @@ pub struct Message {
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub validity_window: ValidityWindow,
pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow,
}
impl std::fmt::Debug for Message {
@ -79,7 +80,8 @@ impl std::fmt::Debug for Message {
)
.field("new_commitments", &self.new_commitments)
.field("new_nullifiers", &nullifiers)
.field("validity_window", &self.validity_window)
.field("block_validity_window", &self.block_validity_window)
.field("timestamp_validity_window", &self.timestamp_validity_window)
.finish()
}
}
@ -112,7 +114,8 @@ impl Message {
encrypted_private_post_states,
new_commitments: output.new_commitments,
new_nullifiers: output.new_nullifiers,
validity_window: output.validity_window,
block_validity_window: output.block_validity_window,
timestamp_validity_window: output.timestamp_validity_window,
})
}
}
@ -123,6 +126,7 @@ pub mod tests {
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
account::Account,
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -165,7 +169,8 @@ pub mod tests {
encrypted_private_post_states,
new_commitments,
new_nullifiers,
validity_window: (None, None).try_into().unwrap(),
block_validity_window: BlockValidityWindow::new_unbounded(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}
}

View File

@ -5,17 +5,14 @@ use std::{
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
BlockId, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountWithMetadata},
program::{BlockId, ValidityWindow},
};
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, message::EncryptedAccountData},
AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
@ -37,6 +34,7 @@ impl PrivacyPreservingTransaction {
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = &self.message;
let witness_set = &self.witness_set;
@ -94,7 +92,9 @@ impl PrivacyPreservingTransaction {
}
// Verify validity window
if !message.validity_window.is_valid_for_block_id(block_id) {
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
@ -115,11 +115,7 @@ impl PrivacyPreservingTransaction {
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
&message.public_post_states,
&message.encrypted_private_post_states,
&message.new_commitments,
&message.new_nullifiers,
&message.validity_window,
message,
)?;
// 5. Commitment freshness
@ -177,23 +173,21 @@ impl PrivacyPreservingTransaction {
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
public_post_states: &[Account],
encrypted_private_post_states: &[EncryptedAccountData],
new_commitments: &[Commitment],
new_nullifiers: &[(Nullifier, CommitmentSetDigest)],
validity_window: &ValidityWindow,
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: public_post_states.to_vec(),
ciphertexts: encrypted_private_post_states
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: new_commitments.to_vec(),
new_nullifiers: new_nullifiers.to_vec(),
validity_window: validity_window.to_owned(),
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)

View File

@ -3,8 +3,9 @@ use std::collections::{HashMap, HashSet, VecDeque};
use borsh::{BorshDeserialize, BorshSerialize};
use log::debug;
use nssa_core::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{BlockId, ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest as _, digest::FixedOutput as _};
@ -71,6 +72,7 @@ impl PublicTransaction {
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = self.message();
let witness_set = self.witness_set();
@ -157,6 +159,10 @@ impl PublicTransaction {
&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
@ -172,10 +178,8 @@ impl PublicTransaction {
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let is_authorized = signer_account_ids.contains(&account_id)
|| authorized_pdas.contains(&account_id);
ensure!(
pre.is_authorized == is_authorized,
pre.is_authorized == is_authorized(&account_id),
NssaError::InvalidProgramBehavior
);
}
@ -193,23 +197,42 @@ impl PublicTransaction {
// Verify validity window
ensure!(
program_output
.validity_window
.is_valid_for_block_id(block_id),
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for post in program_output
.post_states
.iter_mut()
.filter(|post| post.requires_claim())
{
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.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = chained_call.program_id;
} else {
return Err(NssaError::InvalidProgramBehavior);
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
@ -368,7 +391,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);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -388,7 +411,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);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -409,7 +432,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);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -429,7 +452,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);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
@ -445,7 +468,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);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
}
}

View File

@ -49,21 +49,28 @@ impl Signature {
aux_random: [u8; 32],
) -> Self {
let value = {
let secp = secp256k1::Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap();
let keypair = secp256k1::Keypair::from_secret_key(&secp, &secret_key);
let signature = secp.sign_schnorr_with_aux_rand(message, &keypair, &aux_random);
signature.to_byte_array()
let signing_key = k256::schnorr::SigningKey::from_bytes(key.value())
.expect("Expect valid signing key");
signing_key
.sign_raw(message, &aux_random)
.expect("Expect to produce a valid signature")
.to_bytes()
};
Self { value }
}
#[must_use]
pub fn is_valid_for(&self, bytes: &[u8], public_key: &PublicKey) -> bool {
let pk = secp256k1::XOnlyPublicKey::from_byte_array(*public_key.value()).unwrap();
let secp = secp256k1::Secp256k1::new();
let sig = secp256k1::schnorr::Signature::from_byte_array(self.value);
secp.verify_schnorr(&sig, bytes, &pk).is_ok()
let Ok(pk) = k256::schnorr::VerifyingKey::from_bytes(public_key.value()) else {
return false;
};
let Ok(sig) = k256::schnorr::Signature::try_from(self.value.as_slice()) else {
return false;
};
pk.verify_raw(bytes, &sig).is_ok()
}
}

View File

@ -46,7 +46,7 @@ impl PrivateKey {
}
fn is_valid_key(value: [u8; 32]) -> bool {
secp256k1::SecretKey::from_byte_array(value).is_ok()
k256::SecretKey::from_bytes(&value.into()).is_ok()
}
pub fn try_new(value: [u8; 32]) -> Result<Self, NssaError> {

View File

@ -1,6 +1,7 @@
use std::str::FromStr;
use borsh::{BorshDeserialize, BorshSerialize};
use k256::elliptic_curve::sec1::ToEncodedPoint as _;
use nssa_core::account::AccountId;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use sha2::{Digest as _, Sha256};
@ -27,8 +28,7 @@ impl FromStr for PublicKey {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0_u8; 32];
hex::decode_to_slice(s, &mut bytes)
.map_err(|_err| NssaError::InvalidPublicKey(secp256k1::Error::InvalidPublicKey))?;
hex::decode_to_slice(s, &mut bytes).map_err(NssaError::InvalidHexPublicKey)?;
Self::try_new(bytes)
}
}
@ -46,19 +46,24 @@ impl PublicKey {
#[must_use]
pub fn new_from_private_key(key: &PrivateKey) -> Self {
let value = {
let secret_key = secp256k1::SecretKey::from_byte_array(*key.value()).unwrap();
let public_key =
secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &secret_key);
let (x_only, _) = public_key.x_only_public_key();
x_only.serialize()
let secret_key = k256::SecretKey::from_bytes(&(*key.value()).into())
.expect("Expect a valid private key");
let encoded = secret_key.public_key().to_encoded_point(false);
let x_only = encoded
.x()
.expect("Expect k256 point to have a x-coordinate");
*x_only.first_chunk().expect("x_only is exactly 32 bytes")
};
Self(value)
}
pub fn try_new(value: [u8; 32]) -> Result<Self, NssaError> {
// Check point is valid
let _ = secp256k1::XOnlyPublicKey::from_byte_array(value)
.map_err(NssaError::InvalidPublicKey)?;
// Check point is a valid x-only public key
let _ =
k256::schnorr::VerifyingKey::from_bytes(&value).map_err(NssaError::InvalidPublicKey)?;
Ok(Self(value))
}

View File

@ -2,9 +2,10 @@ use std::collections::{BTreeSet, HashMap, HashSet};
use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
Timestamp,
account::{Account, AccountId, Nonce},
program::{BlockId, ProgramId},
program::ProgramId,
};
use crate::{
@ -159,8 +160,9 @@ impl V03State {
&mut self,
tx: &PublicTransaction,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<(), NssaError> {
let state_diff = tx.validate_and_produce_public_state_diff(self, block_id)?;
let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
#[expect(
clippy::iter_over_hash_type,
@ -184,9 +186,11 @@ impl V03State {
&mut self,
tx: &PrivacyPreservingTransaction,
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)?;
let public_state_diff =
tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?;
let message = tx.message();
@ -338,10 +342,11 @@ pub mod tests {
use std::collections::HashMap;
use nssa_core::{
Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
Timestamp,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey},
program::{BlockId, PdaSeed, ProgramId, ValidityWindow},
program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow},
};
use crate::{
@ -456,16 +461,19 @@ pub mod tests {
fn transfer_transaction(
from: AccountId,
from_key: &PrivateKey,
nonce: u128,
from_nonce: u128,
to: AccountId,
to_key: &PrivateKey,
to_nonce: u128,
balance: u128,
) -> PublicTransaction {
let account_ids = vec![from, to];
let nonces = vec![Nonce(nonce)];
let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)];
let program_id = Program::authenticated_transfer_program().id();
let message =
public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]);
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]);
PublicTransaction::new(message, witness_set)
}
@ -567,17 +575,18 @@ pub mod tests {
let initial_data = [(account_id, 100)];
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
let from = account_id;
let to = AccountId::new([2; 32]);
let to_key = PrivateKey::try_new([2; 32]).unwrap();
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
assert_eq!(state.get_account_by_id(to), Account::default());
let balance_to_move = 5;
let tx = transfer_transaction(from, &key, 0, to, balance_to_move);
state.transition_from_public_transaction(&tx, 1).unwrap();
let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move);
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
assert_eq!(state.get_account_by_id(from).balance, 95);
assert_eq!(state.get_account_by_id(to).balance, 5);
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
assert_eq!(state.get_account_by_id(to).nonce, Nonce(0));
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
}
#[test]
@ -588,12 +597,13 @@ pub mod tests {
let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]);
let from = account_id;
let from_key = key;
let to = AccountId::new([2; 32]);
let to_key = PrivateKey::try_new([2; 32]).unwrap();
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
let balance_to_move = 101;
assert!(state.get_account_by_id(from).balance < balance_to_move);
let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move);
let result = state.transition_from_public_transaction(&tx, 1);
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
assert_eq!(state.get_account_by_id(from).balance, 100);
@ -613,16 +623,17 @@ pub mod tests {
let from = account_id2;
let from_key = key2;
let to = account_id1;
let to_key = key1;
assert_ne!(state.get_account_by_id(to), Account::default());
let balance_to_move = 8;
let tx = transfer_transaction(from, &from_key, 0, to, balance_to_move);
state.transition_from_public_transaction(&tx, 1).unwrap();
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
assert_eq!(state.get_account_by_id(from).balance, 192);
assert_eq!(state.get_account_by_id(to).balance, 108);
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
assert_eq!(state.get_account_by_id(to).nonce, Nonce(0));
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
}
#[test]
@ -633,21 +644,38 @@ pub mod tests {
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 account_id3 = AccountId::new([3; 32]);
let key3 = PrivateKey::try_new([3; 32]).unwrap();
let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3));
let balance_to_move = 5;
let tx = transfer_transaction(account_id1, &key1, 0, account_id2, balance_to_move);
state.transition_from_public_transaction(&tx, 1).unwrap();
let tx = transfer_transaction(
account_id1,
&key1,
0,
account_id2,
&key2,
0,
balance_to_move,
);
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let balance_to_move = 3;
let tx = transfer_transaction(account_id2, &key2, 0, account_id3, balance_to_move);
state.transition_from_public_transaction(&tx, 1).unwrap();
let tx = transfer_transaction(
account_id2,
&key2,
1,
account_id3,
&key3,
0,
balance_to_move,
);
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
assert_eq!(state.get_account_by_id(account_id1).balance, 95);
assert_eq!(state.get_account_by_id(account_id2).balance, 2);
assert_eq!(state.get_account_by_id(account_id3).balance, 3);
assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1));
assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(1));
assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(0));
assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2));
assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1));
}
#[test]
@ -662,7 +690,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -679,7 +707,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -696,7 +724,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -720,7 +748,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -744,7 +772,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -768,7 +796,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -792,7 +820,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -820,7 +848,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -845,7 +873,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -863,7 +891,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -892,7 +920,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -1085,7 +1113,7 @@ pub mod tests {
assert!(!state.private_state.0.contains(&expected_new_commitment));
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let sender_post = state.get_account_by_id(sender_keys.account_id());
@ -1155,7 +1183,7 @@ pub mod tests {
assert!(!state.private_state.1.contains(&expected_new_nullifier));
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
assert_eq!(state.public_state, previous_public_state);
@ -1219,7 +1247,7 @@ pub mod tests {
assert!(!state.private_state.1.contains(&expected_new_nullifier));
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let recipient_post = state.get_account_by_id(recipient_keys.account_id());
@ -2147,7 +2175,7 @@ pub mod tests {
);
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let sender_private_account = Account {
@ -2165,7 +2193,7 @@ pub mod tests {
&state,
);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
let NssaError::InvalidInput(error_message) = result.err().unwrap() else {
@ -2212,15 +2240,14 @@ pub mod tests {
#[test]
fn claiming_mechanism() {
let program = Program::authenticated_transfer_program();
let key = PrivateKey::try_new([1; 32]).unwrap();
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
let from_key = PrivateKey::try_new([1; 32]).unwrap();
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
let initial_balance = 100;
let initial_data = [(account_id, initial_balance)];
let initial_data = [(from, initial_balance)];
let mut state =
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from = account_id;
let from_key = key;
let to = AccountId::new([2; 32]);
let to_key = PrivateKey::try_new([2; 32]).unwrap();
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
let amount: u128 = 37;
// Check the recipient is an uninitialized account
@ -2229,26 +2256,80 @@ pub mod tests {
let expected_recipient_post = Account {
program_owner: program.id(),
balance: amount,
nonce: Nonce(1),
..Account::default()
};
let message = public_transaction::Message::try_new(
program.id(),
vec![from, to],
vec![Nonce(0)],
vec![Nonce(0), Nonce(0)],
amount,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let recipient_post = state.get_account_by_id(to);
assert_eq!(recipient_post, expected_recipient_post);
}
#[test]
fn unauthorized_public_account_claiming_fails() {
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(&[], &[]);
assert_eq!(state.get_account_by_id(account_id), Account::default());
let message =
public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::ProgramExecutionFailed(_))));
assert_eq!(state.get_account_by_id(account_id), Account::default());
}
#[test]
fn authorized_public_account_claiming_succeeds() {
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(&[], &[]);
assert_eq!(state.get_account_by_id(account_id), Account::default());
let message = public_transaction::Message::try_new(
program.id(),
vec![account_id],
vec![Nonce(0)],
0_u128,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
assert_eq!(
state.get_account_by_id(account_id),
Account {
program_owner: program.id(),
nonce: Nonce(1),
..Account::default()
}
);
}
#[test]
fn public_chained_call() {
let program = Program::chain_caller();
@ -2285,7 +2366,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2325,7 +2406,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(
result,
Err(NssaError::MaxChainedCallsDepthExceeded)
@ -2366,7 +2447,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2382,15 +2463,14 @@ pub mod tests {
// program and not the chained_caller program.
let chain_caller = Program::chain_caller();
let auth_transfer = Program::authenticated_transfer_program();
let key = PrivateKey::try_new([1; 32]).unwrap();
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
let from_key = PrivateKey::try_new([1; 32]).unwrap();
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
let initial_balance = 100;
let initial_data = [(account_id, initial_balance)];
let initial_data = [(from, initial_balance)];
let mut state =
V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs();
let from = account_id;
let from_key = key;
let to = AccountId::new([2; 32]);
let to_key = PrivateKey::try_new([2; 32]).unwrap();
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
let amount: u128 = 37;
// Check the recipient is an uninitialized account
@ -2400,6 +2480,7 @@ pub mod tests {
// The expected program owner is the authenticated transfer program
program_owner: auth_transfer.id(),
balance: amount,
nonce: Nonce(1),
..Account::default()
};
@ -2415,14 +2496,15 @@ pub mod tests {
chain_caller.id(),
vec![to, from], // The chain_caller program permutes the account order in the chain
// call
vec![Nonce(0)],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let from_post = state.get_account_by_id(from);
let to_post = state.get_account_by_id(to);
@ -2430,6 +2512,88 @@ pub mod tests {
assert_eq!(to_post, expected_to_post);
}
#[test]
fn unauthorized_public_account_claiming_fails_when_executed_privately() {
let program = Program::authenticated_transfer_program();
let account_id = AccountId::new([11; 32]);
let public_account = AccountWithMetadata::new(Account::default(), false, account_id);
let result = execute_and_prove(
vec![public_account],
Program::serialize_instruction(0_u128).unwrap(),
vec![0],
vec![],
vec![],
vec![],
&program.into(),
);
assert!(matches!(result, Err(NssaError::ProgramProveFailed(_))));
}
#[test]
fn authorized_public_account_claiming_succeeds_when_executed_privately() {
let program = Program::authenticated_transfer_program();
let program_id = program.id();
let sender_keys = test_private_account_keys_1();
let sender_private_account = Account {
program_owner: program_id,
balance: 100,
..Account::default()
};
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));
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 =
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
let recipient_pre =
AccountWithMetadata::new(Account::default(), true, recipient_account_id);
let esk = [5; 32];
let shared_secret = SharedSecretKey::new(&esk, &sender_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = execute_and_prove(
vec![sender_pre, recipient_pre],
Program::serialize_instruction(37_u128).unwrap(),
vec![1, 0],
vec![(sender_keys.npk(), shared_secret)],
vec![sender_keys.nsk],
vec![state.get_proof_for_commitment(&sender_commitment)],
&program.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![recipient_account_id],
vec![Nonce(0)],
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk);
assert!(state.private_state.1.contains(&nullifier));
assert_eq!(
state.get_account_by_id(recipient_account_id),
Account {
program_owner: program_id,
balance: 37,
nonce: Nonce(1),
..Account::default()
}
);
}
#[test_case::test_case(1; "single call")]
#[test_case::test_case(2; "two calls")]
fn private_chained_call(number_of_calls: u32) {
@ -2531,7 +2695,7 @@ pub mod tests {
let transaction = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&transaction, 1)
.transition_from_privacy_preserving_transaction(&transaction, 1, 0)
.unwrap();
// Assert
@ -2571,36 +2735,47 @@ pub mod tests {
let mut state = V03State::new_with_genesis_accounts(&[], &[]);
state.add_pinata_token_program(pinata_definition_id);
// Execution of the token program to create new token for the pinata token
// definition and supply accounts
// Set up the token accounts directly (bypassing public transactions which
// would require signers for Claim::Authorized). The focus of this test is
// the PDA mechanism in the pinata program's chained call, not token creation.
let total_supply: u128 = 10_000_000;
let instruction = token_core::Instruction::NewFungibleDefinition {
let token_definition = token_core::TokenDefinition::Fungible {
name: String::from("PINATA"),
total_supply,
metadata_id: None,
};
let message = public_transaction::Message::try_new(
token.id(),
vec![pinata_token_definition_id, pinata_token_holding_id],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
// Execution of winner's token holding account initialization
let instruction = token_core::Instruction::InitializeAccount;
let message = public_transaction::Message::try_new(
token.id(),
vec![pinata_token_definition_id, winner_token_holding_id],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
let token_holding = token_core::TokenHolding::Fungible {
definition_id: pinata_token_definition_id,
balance: total_supply,
};
let winner_holding = token_core::TokenHolding::Fungible {
definition_id: pinata_token_definition_id,
balance: 0,
};
state.force_insert_account(
pinata_token_definition_id,
Account {
program_owner: token.id(),
data: Data::from(&token_definition),
..Account::default()
},
);
state.force_insert_account(
pinata_token_holding_id,
Account {
program_owner: token.id(),
data: Data::from(&token_holding),
..Account::default()
},
);
state.force_insert_account(
winner_token_holding_id,
Account {
program_owner: token.id(),
data: Data::from(&winner_holding),
..Account::default()
},
);
// Submit a solution to the pinata program to claim the prize
let solution: u128 = 989_106;
@ -2617,7 +2792,7 @@ pub mod tests {
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id);
assert_eq!(
@ -2647,7 +2822,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
}
@ -2693,7 +2868,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
let tx = PublicTransaction::new(message, witness_set);
let res = state.transition_from_public_transaction(&tx, 1);
let res = state.transition_from_public_transaction(&tx, 1, 0);
assert!(matches!(res, Err(NssaError::InvalidProgramBehavior)));
let sender_post = state.get_account_by_id(sender_id);
@ -2762,13 +2937,60 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1);
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0);
assert!(result.is_ok());
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
assert!(state.private_state.1.contains(&nullifier));
}
#[test]
fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let private_keys = test_private_account_keys_1();
// This is intentional: claim authorization was introduced to protect public accounts,
// especially PDAs. Private PDAs are not useful in practice because there is no way to
// operate them without the corresponding private keys, so unauthorized private claiming
// remains allowed.
let unauthorized_account =
AccountWithMetadata::new(Account::default(), false, &private_keys.npk());
let program = Program::claimer();
let esk = [5; 32];
let shared_secret = SharedSecretKey::new(&esk, &private_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let (output, proof) = execute_and_prove(
vec![unauthorized_account],
Program::serialize_instruction(0_u128).unwrap(),
vec![2],
vec![(private_keys.npk(), shared_secret)],
vec![],
vec![None],
&program.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![],
vec![],
vec![(private_keys.npk(), private_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set);
state
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_initialization(&private_keys.npk());
assert!(state.private_state.1.contains(&nullifier));
}
#[test]
fn private_account_claimed_then_used_without_init_flag_should_fail() {
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
@ -2815,7 +3037,7 @@ pub mod tests {
// Claim should succeed
assert!(
state
.transition_from_privacy_preserving_transaction(&tx, 1)
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.is_ok()
);
@ -2864,7 +3086,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
// Should succeed - no changes made, no claim needed
assert!(result.is_ok());
@ -2889,7 +3111,7 @@ pub mod tests {
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
let result = state.transition_from_public_transaction(&tx, 1);
let result = state.transition_from_public_transaction(&tx, 1, 0);
// Should fail - cannot modify data without claiming the account
assert!(matches!(result, Err(NssaError::InvalidProgramBehavior)));
@ -3018,7 +3240,7 @@ pub mod tests {
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
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());
@ -3027,21 +3249,76 @@ pub mod tests {
let account_ids = vec![pre.account_id];
let nonces = vec![];
let program_id = validity_window_program.id();
let message = public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
validity_window,
)
.unwrap();
let instruction = (
block_validity_window,
TimestampValidityWindow::new_unbounded(),
);
let message =
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
PublicTransaction::new(message, witness_set)
};
let result = state.transition_from_public_transaction(&tx, block_id);
let is_inside_validity_window = match (validity_window.start(), validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
let result = state.transition_from_public_transaction(&tx, block_id, 0);
let is_inside_validity_window =
match (block_validity_window.start(), block_validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn timestamp_validity_window_works_in_public_transactions(
validity_window: (Option<Timestamp>, Option<Timestamp>),
timestamp: Timestamp,
) {
let timestamp_validity_window: TimestampValidityWindow =
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_public_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let account_ids = vec![pre.account_id];
let nonces = vec![];
let program_id = validity_window_program.id();
let instruction = (
BlockValidityWindow::new_unbounded(),
timestamp_validity_window,
);
let message =
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
PublicTransaction::new(message, witness_set)
};
let result = state.transition_from_public_transaction(&tx, 1, timestamp);
let is_inside_validity_window = match (
timestamp_validity_window.start(),
timestamp_validity_window.end(),
) {
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
(Some(s), None) => s <= timestamp,
(None, Some(e)) => timestamp < e,
(None, None) => true,
};
if is_inside_validity_window {
@ -3068,7 +3345,7 @@ pub mod tests {
validity_window: (Option<BlockId>, Option<BlockId>),
block_id: BlockId,
) {
let validity_window: ValidityWindow = validity_window.try_into().unwrap();
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
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());
@ -3078,9 +3355,13 @@ pub mod tests {
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let instruction = (
block_validity_window,
TimestampValidityWindow::new_unbounded(),
);
let (output, proof) = circuit::execute_and_prove(
vec![pre],
Program::serialize_instruction(validity_window).unwrap(),
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
@ -3100,11 +3381,83 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set)
};
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id);
let is_inside_validity_window = match (validity_window.start(), validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0);
let is_inside_validity_window =
match (block_validity_window.start(), block_validity_window.end()) {
(Some(s), Some(e)) => s <= block_id && block_id < e,
(Some(s), None) => s <= block_id,
(None, Some(e)) => block_id < e,
(None, None) => true,
};
if is_inside_validity_window {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(NssaError::OutOfValidityWindow)));
}
}
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
fn timestamp_validity_window_works_in_privacy_preserving_transactions(
validity_window: (Option<Timestamp>, Option<Timestamp>),
timestamp: Timestamp,
) {
let timestamp_validity_window: TimestampValidityWindow =
validity_window.try_into().unwrap();
let validity_window_program = Program::validity_window();
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk());
let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs();
let tx = {
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let epk = EphemeralPublicKey::from_scalar(esk);
let instruction = (
BlockValidityWindow::new_unbounded(),
timestamp_validity_window,
);
let (output, proof) = circuit::execute_and_prove(
vec![pre],
Program::serialize_instruction(instruction).unwrap(),
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&validity_window_program.into(),
)
.unwrap();
let message = Message::try_from_circuit_output(
vec![],
vec![],
vec![(account_keys.npk(), account_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set)
};
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp);
let is_inside_validity_window = match (
timestamp_validity_window.start(),
timestamp_validity_window.end(),
) {
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
(Some(s), None) => s <= timestamp,
(None, Some(e)) => timestamp < e,
(None, None) => true,
};
if is_inside_validity_window {

View File

@ -1,13 +1,13 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
AccountPostState, Claim, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs,
},
};
/// Initializes a default account under the ownership of this program.
fn initialize_account(pre_state: AccountWithMetadata) -> AccountPostState {
let account_to_claim = AccountPostState::new_claimed(pre_state.account);
let account_to_claim = AccountPostState::new_claimed(pre_state.account, Claim::Authorized);
let is_authorized = pre_state.is_authorized;
// Continue only if the account to claim has default values
@ -52,7 +52,7 @@ fn transfer(
// Claim recipient account if it has default program owner
if recipient_post_account.program_owner == DEFAULT_PROGRAM_ID {
AccountPostState::new_claimed(recipient_post_account)
AccountPostState::new_claimed(recipient_post_account, Claim::Authorized)
} else {
AccountPostState::new(recipient_post_account)
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
use risc0_zkvm::sha::{Impl, Sha256 as _};
const PRIZE: u128 = 150;
@ -82,7 +82,7 @@ fn main() {
instruction_words,
vec![pinata, winner],
vec![
AccountPostState::new_claimed_if_default(pinata_post),
AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized),
AccountPostState::new(winner_post),
],
)

View File

@ -10,8 +10,9 @@ use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Nonce},
compute_digest_for_path,
program::{
AccountPostState, ChainedCall, DEFAULT_PROGRAM_ID, MAX_NUMBER_CHAINED_CALLS, ProgramId,
ProgramOutput, ValidityWindow, validate_execution,
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow,
validate_execution,
},
};
use risc0_zkvm::{guest::env, serde::to_vec};
@ -20,29 +21,51 @@ use risc0_zkvm::{guest::env, serde::to_vec};
struct ExecutionState {
pre_states: Vec<AccountWithMetadata>,
post_states: HashMap<AccountId, Account>,
validity_window: ValidityWindow,
block_validity_window: BlockValidityWindow,
timestamp_validity_window: TimestampValidityWindow,
}
impl ExecutionState {
/// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs(program_id: ProgramId, program_outputs: Vec<ProgramOutput>) -> Self {
let valid_from_id = program_outputs
pub fn derive_from_outputs(
visibility_mask: &[u8],
program_id: ProgramId,
program_outputs: Vec<ProgramOutput>,
) -> Self {
let block_valid_from = program_outputs
.iter()
.filter_map(|output| output.validity_window.start())
.filter_map(|output| output.block_validity_window.start())
.max();
let valid_until_id = program_outputs
let block_valid_until = program_outputs
.iter()
.filter_map(|output| output.validity_window.end())
.filter_map(|output| output.block_validity_window.end())
.min();
let ts_valid_from = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.start())
.max();
let ts_valid_until = program_outputs
.iter()
.filter_map(|output| output.timestamp_validity_window.end())
.min();
let validity_window = (valid_from_id, valid_until_id).try_into().expect(
"There should be non empty intersection in the program output validity windows",
);
let block_validity_window: BlockValidityWindow = (block_valid_from, block_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output block validity windows",
);
let timestamp_validity_window: TimestampValidityWindow =
(ts_valid_from, ts_valid_until)
.try_into()
.expect(
"There should be non empty intersection in the program output timestamp validity windows",
);
let mut execution_state = Self {
pre_states: Vec::new(),
post_states: HashMap::new(),
validity_window,
block_validity_window,
timestamp_validity_window,
};
let Some(first_output) = program_outputs.first() else {
@ -102,6 +125,7 @@ impl ExecutionState {
&chained_call.pda_seeds,
);
execution_state.validate_and_sync_states(
visibility_mask,
chained_call.program_id,
&authorized_pdas,
program_output.pre_states,
@ -134,7 +158,7 @@ impl ExecutionState {
{
assert_ne!(
post.program_owner, DEFAULT_PROGRAM_ID,
"Account {account_id:?} was modified but not claimed"
"Account {account_id} was modified but not claimed"
);
}
@ -144,6 +168,7 @@ impl ExecutionState {
/// Validate program pre and post states and populate the execution state.
fn validate_and_sync_states(
&mut self,
visibility_mask: &[u8],
program_id: ProgramId,
authorized_pdas: &HashSet<AccountId>,
pre_states: Vec<AccountWithMetadata>,
@ -151,14 +176,25 @@ impl ExecutionState {
) {
for (pre, mut post) in pre_states.into_iter().zip(post_states) {
let pre_account_id = pre.account_id;
let pre_is_authorized = pre.is_authorized;
let post_states_entry = self.post_states.entry(pre.account_id);
match &post_states_entry {
Entry::Occupied(occupied) => {
#[expect(
clippy::shadow_unrelated,
reason = "Shadowing is intentional to use all fields"
)]
let AccountWithMetadata {
account: pre_account,
account_id: pre_account_id,
is_authorized: pre_is_authorized,
} = pre;
// Ensure that new pre state is the same as known post state
assert_eq!(
occupied.get(),
&pre.account,
"Inconsistent pre state for account {pre_account_id:?}",
&pre_account,
"Inconsistent pre state for account {pre_account_id}",
);
let previous_is_authorized = self
@ -167,7 +203,7 @@ impl ExecutionState {
.find(|acc| acc.account_id == pre_account_id)
.map_or_else(
|| panic!(
"Pre state must exist in execution state for account {pre_account_id:?}",
"Pre state must exist in execution state for account {pre_account_id}",
),
|acc| acc.is_authorized
);
@ -176,22 +212,57 @@ impl ExecutionState {
previous_is_authorized || authorized_pdas.contains(&pre_account_id);
assert_eq!(
pre.is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id:?}",
pre_is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id}",
);
}
Entry::Vacant(_) => {
// Pre state for the initial call
self.pre_states.push(pre);
}
}
if post.requires_claim() {
if let Some(claim) = post.required_claim() {
// The invoked program can only claim accounts with default program id.
if post.account().program_owner == DEFAULT_PROGRAM_ID {
post.account_mut().program_owner = program_id;
assert_eq!(
post.account().program_owner,
DEFAULT_PROGRAM_ID,
"Cannot claim an initialized account {pre_account_id}"
);
let pre_state_position = self
.pre_states
.iter()
.position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point");
let is_public_account = visibility_mask[pre_state_position] == 0;
if is_public_account {
match claim {
Claim::Authorized => {
// Note: no need to check authorized pdas because we have already
// checked consistency of authorization above.
assert!(
pre_is_authorized,
"Cannot claim unauthorized account {pre_account_id}"
);
}
Claim::Pda(seed) => {
let pda = AccountId::from((&program_id, &seed));
assert_eq!(
pre_account_id, pda,
"Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}"
);
}
}
} else {
panic!("Cannot claim an initialized account {pre_account_id:?}");
// We don't care about the exact claim mechanism for private accounts.
// This is because the main reason to have it is to protect against PDA griefing
// attacks in public execution, while private PDA doesn't make much sense
// anyway.
}
post.account_mut().program_owner = program_id;
}
post_states_entry.insert_entry(post.into_account());
@ -225,7 +296,8 @@ fn compute_circuit_output(
ciphertexts: Vec::new(),
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
validity_window: execution_state.validity_window,
block_validity_window: execution_state.block_validity_window,
timestamp_validity_window: execution_state.timestamp_validity_window,
};
let states_iter = execution_state.into_states_iter();
@ -408,7 +480,8 @@ fn main() {
program_id,
} = env::read();
let execution_state = ExecutionState::derive_from_outputs(program_id, program_outputs);
let execution_state =
ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs);
let output = compute_circuit_output(
execution_state,

View File

@ -2,11 +2,11 @@ use std::num::NonZeroU128;
use amm_core::{
PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
compute_pool_pda, compute_vault_pda,
compute_pool_pda, compute_pool_pda_seed, compute_vault_pda, compute_vault_pda_seed,
};
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::{AccountPostState, ChainedCall, ProgramId},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
@ -108,36 +108,52 @@ pub fn new_definition(
};
pool_post.data = Data::from(&pool_post_definition);
let pool_post = AccountPostState::new_claimed_if_default(pool_post);
let pool_pda_seed = compute_pool_pda_seed(definition_token_a_id, definition_token_b_id);
let pool_post = AccountPostState::new_claimed_if_default(pool_post, Claim::Pda(pool_pda_seed));
let token_program_id = user_holding_a.account.program_owner;
// Chain call for Token A (user_holding_a -> Vault_A)
let vault_a_seed = compute_vault_pda_seed(pool.account_id, definition_token_a_id);
let vault_a_authorized = AccountWithMetadata {
is_authorized: true,
..vault_a.clone()
};
let call_token_a = ChainedCall::new(
token_program_id,
vec![user_holding_a.clone(), vault_a.clone()],
vec![user_holding_a.clone(), vault_a_authorized],
&token_core::Instruction::Transfer {
amount_to_transfer: token_a_amount.into(),
},
);
)
.with_pda_seeds(vec![vault_a_seed]);
// Chain call for Token B (user_holding_b -> Vault_B)
let vault_b_seed = compute_vault_pda_seed(pool.account_id, definition_token_b_id);
let vault_b_authorized = AccountWithMetadata {
is_authorized: true,
..vault_b.clone()
};
let call_token_b = ChainedCall::new(
token_program_id,
vec![user_holding_b.clone(), vault_b.clone()],
vec![user_holding_b.clone(), vault_b_authorized],
&token_core::Instruction::Transfer {
amount_to_transfer: token_b_amount.into(),
},
);
let mut pool_lp_auth = pool_definition_lp.clone();
pool_lp_auth.is_authorized = true;
)
.with_pda_seeds(vec![vault_b_seed]);
let pool_lp_pda_seed = compute_liquidity_token_pda_seed(pool.account_id);
let pool_lp_authorized = AccountWithMetadata {
is_authorized: true,
..pool_definition_lp.clone()
};
let call_token_lp = ChainedCall::new(
token_program_id,
vec![pool_lp_auth, user_holding_lp.clone()],
vec![pool_lp_authorized, user_holding_lp.clone()],
&instruction,
)
.with_pda_seeds(vec![compute_liquidity_token_pda_seed(pool.account_id)]);
.with_pda_seeds(vec![pool_lp_pda_seed]);
let chained_calls = vec![call_token_lp, call_token_b, call_token_a];

View File

@ -1,4 +1,4 @@
use std::num::NonZero;
use std::{num::NonZero, vec};
use amm_core::{
PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed,
@ -1756,7 +1756,7 @@ impl AccountsForExeTests {
definition_id: IdForExeTests::token_lp_definition_id(),
balance: BalanceForExeTests::lp_supply_init(),
}),
nonce: 0_u128.into(),
nonce: 1_u128.into(),
}
}
@ -1801,7 +1801,7 @@ impl AccountsForExeTests {
definition_id: IdForExeTests::token_lp_definition_id(),
balance: 0,
}),
nonce: 0_u128.into(),
nonce: 1.into(),
}
}
}
@ -2733,7 +2733,7 @@ fn simple_amm_remove() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2799,7 +2799,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
IdForExeTests::user_token_b_id(),
IdForExeTests::user_token_lp_id(),
],
vec![0_u128.into(), 0_u128.into()],
vec![0_u128.into(), 0_u128.into(), 0_u128.into()],
instruction,
)
.unwrap();
@ -2809,11 +2809,12 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() {
&[
&PrivateKeysForTests::user_token_a_key(),
&PrivateKeysForTests::user_token_b_key(),
&PrivateKeysForTests::user_token_lp_key(),
],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2897,7 +2898,7 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -2955,7 +2956,7 @@ fn simple_amm_new_definition_uninitialized_pool() {
IdForExeTests::user_token_b_id(),
IdForExeTests::user_token_lp_id(),
],
vec![0_u128.into(), 0_u128.into()],
vec![0_u128.into(), 0_u128.into(), 0_u128.into()],
instruction,
)
.unwrap();
@ -2965,11 +2966,12 @@ fn simple_amm_new_definition_uninitialized_pool() {
&[
&PrivateKeysForTests::user_token_a_key(),
&PrivateKeysForTests::user_token_b_key(),
&PrivateKeysForTests::user_token_lp_key(),
],
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3031,7 +3033,7 @@ fn simple_amm_add() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3088,7 +3090,7 @@ fn simple_amm_swap_1() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());
@ -3138,7 +3140,7 @@ fn simple_amm_swap_2() {
);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 1).unwrap();
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
let pool_post = state.get_account_by_id(IdForExeTests::pool_definition_id());
let vault_a_post = state.get_account_by_id(IdForExeTests::vault_a_id());

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, ChainedCall, ProgramId},
program::{AccountPostState, ChainedCall, Claim, ProgramId},
};
pub fn create_associated_token_account(
@ -11,7 +11,7 @@ pub fn create_associated_token_account(
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
// No authorization check needed: create is idempotent, so anyone can call it safely.
let token_program_id = token_definition.account.program_owner;
ata_core::verify_ata_and_get_seed(
let ata_seed = ata_core::verify_ata_and_get_seed(
&ata_account,
&owner,
token_definition.account_id,
@ -22,7 +22,7 @@ pub fn create_associated_token_account(
if ata_account.account != Account::default() {
return (
vec![
AccountPostState::new_claimed_if_default(owner.account.clone()),
AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized),
AccountPostState::new(token_definition.account.clone()),
AccountPostState::new(ata_account.account.clone()),
],
@ -31,14 +31,20 @@ pub fn create_associated_token_account(
}
let post_states = vec![
AccountPostState::new_claimed_if_default(owner.account.clone()),
AccountPostState::new_claimed_if_default(owner.account.clone(), Claim::Authorized),
AccountPostState::new(token_definition.account.clone()),
AccountPostState::new(ata_account.account.clone()),
];
let ata_account_auth = AccountWithMetadata {
is_authorized: true,
..ata_account.clone()
};
let chained_call = ChainedCall::new(
token_program_id,
vec![token_definition.clone(), ata_account.clone()],
vec![token_definition.clone(), ata_account_auth],
&token_core::Instruction::InitializeAccount,
);
)
.with_pda_seeds(vec![ata_seed]);
(post_states, vec![chained_call])
}

View File

@ -10,23 +10,23 @@ pub enum Instruction {
/// Transfer tokens from sender to recipient.
///
/// Required accounts:
/// - Sender's Token Holding account (authorized),
/// - Recipient's Token Holding account.
/// - Sender's Token Holding account (initialized, authorized),
/// - Recipient's Token Holding account (initialized or authorized and uninitialized).
Transfer { amount_to_transfer: u128 },
/// Create a new fungible token definition without metadata.
///
/// Required accounts:
/// - Token Definition account (uninitialized),
/// - Token Holding account (uninitialized).
/// - Token Definition account (uninitialized, authorized),
/// - Token Holding account (uninitialized, authorized).
NewFungibleDefinition { name: String, total_supply: u128 },
/// Create a new fungible or non-fungible token definition with metadata.
///
/// Required accounts:
/// - Token Definition account (uninitialized),
/// - Token Holding account (uninitialized),
/// - Token Metadata account (uninitialized).
/// - Token Definition account (uninitialized, authorized),
/// - Token Holding account (uninitialized, authorized),
/// - Token Metadata account (uninitialized, authorized).
NewDefinitionWithMetadata {
new_definition: NewTokenDefinition,
/// Boxed to avoid large enum variant size.
@ -36,29 +36,29 @@ pub enum Instruction {
/// Initialize a token holding account for a given token definition.
///
/// Required accounts:
/// - Token Definition account (initialized),
/// - Token Holding account (uninitialized),
/// - Token Definition account (initialized, any authorization),
/// - Token Holding account (uninitialized, authorized),
InitializeAccount,
/// Burn tokens from the holder's account.
///
/// Required accounts:
/// - Token Definition account (initialized),
/// - Token Holding account (authorized).
/// - Token Definition account (initialized, any authorization),
/// - Token Holding account (initialized, authorized).
Burn { amount_to_burn: u128 },
/// Mint new tokens to the holder's account.
///
/// Required accounts:
/// - Token Definition account (authorized),
/// - Token Holding account (uninitialized or initialized).
/// - Token Definition account (initialized, authorized),
/// - Token Holding account (uninitialized or authorized and initialized).
Mint { amount_to_mint: u128 },
/// Print a new NFT from the master copy.
///
/// Required accounts:
/// - NFT Master Token Holding account (authorized),
/// - NFT Printed Copy Token Holding account (uninitialized).
/// - NFT Printed Copy Token Holding account (uninitialized, authorized).
PrintNft,
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{TokenDefinition, TokenHolding};
@ -30,6 +30,6 @@ pub fn initialize_account(
vec![
AccountPostState::new(definition_post),
AccountPostState::new_claimed(account_to_initialize),
AccountPostState::new_claimed(account_to_initialize, Claim::Authorized),
]
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{TokenDefinition, TokenHolding};
@ -67,6 +67,6 @@ pub fn mint(
vec![
AccountPostState::new(definition_post),
AccountPostState::new_claimed_if_default(holding_post),
AccountPostState::new_claimed_if_default(holding_post, Claim::Authorized),
]
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::{
NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding, TokenMetadata,
@ -42,8 +42,8 @@ pub fn new_fungible_definition(
holding_target_account_post.data = Data::from(&token_holding);
vec![
AccountPostState::new_claimed(definition_target_account_post),
AccountPostState::new_claimed(holding_target_account_post),
AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized),
]
}
@ -119,8 +119,8 @@ pub fn new_definition_with_metadata(
metadata_target_account_post.data = Data::from(&token_metadata);
vec![
AccountPostState::new_claimed(definition_target_account_post),
AccountPostState::new_claimed(holding_target_account_post),
AccountPostState::new_claimed(metadata_target_account_post),
AccountPostState::new_claimed(definition_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(holding_target_account_post, Claim::Authorized),
AccountPostState::new_claimed(metadata_target_account_post, Claim::Authorized),
]
}

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::TokenHolding;
@ -50,6 +50,6 @@ pub fn print_nft(
vec![
AccountPostState::new(master_account_post),
AccountPostState::new_claimed(printed_account_post),
AccountPostState::new_claimed(printed_account_post, Claim::Authorized),
]
}

View File

@ -5,7 +5,10 @@
reason = "We don't care about it in tests"
)]
use nssa_core::account::{Account, AccountId, AccountWithMetadata, Data};
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata, Data},
program::Claim,
};
use token_core::{
MetadataStandard, NewTokenDefinition, NewTokenMetadata, TokenDefinition, TokenHolding,
};
@ -851,7 +854,7 @@ fn mint_uninit_holding_success() {
*holding_post.account(),
AccountForTests::init_mint().account
);
assert!(holding_post.requires_claim());
assert_eq!(holding_post.required_claim(), Some(Claim::Authorized));
}
#[test]

View File

@ -1,6 +1,6 @@
use nssa_core::{
account::{Account, AccountWithMetadata, Data},
program::AccountPostState,
program::{AccountPostState, Claim},
};
use token_core::TokenHolding;
@ -106,6 +106,6 @@ pub fn transfer(
vec![
AccountPostState::new(sender_post),
AccountPostState::new_claimed_if_default(recipient_post),
AccountPostState::new_claimed_if_default(recipient_post, Claim::Authorized),
]
}

View File

@ -16,6 +16,7 @@ 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;
@ -165,14 +166,16 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
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, self.next_block_id()),
.transition_from_public_transaction(tx, block_id, timestamp),
NSSATransaction::PrivacyPreserving(tx) => self
.state
.transition_from_privacy_preserving_transaction(tx, self.next_block_id()),
.transition_from_privacy_preserving_transaction(tx, block_id, timestamp),
NSSATransaction::ProgramDeployment(tx) => self
.state
.transition_from_program_deployment_transaction(tx),
@ -218,7 +221,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
.latest_block_meta()
.context("Failed to get latest block meta from store")?;
let curr_time = u64::try_from(chrono::Utc::now().timestamp_millis())
let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis())
.expect("Timestamp must be positive");
while let Some(tx) = self.mempool.pop() {
@ -231,7 +234,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
block_id: new_block_height,
transactions: temp_valid_transactions,
prev_block_hash: latest_block_meta.hash,
timestamp: curr_time,
timestamp: new_block_timestamp,
};
let block_size = borsh::to_vec(&temp_hashable_data)
@ -249,7 +252,8 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
break;
}
match self.execute_check_transaction_on_state(tx) {
match self.execute_check_transaction_on_state(tx, new_block_height, new_block_timestamp)
{
Ok(valid_tx) => {
valid_transactions.push(valid_tx);
@ -272,7 +276,7 @@ impl<BC: BlockSettlementClientTrait, IC: IndexerClientTrait> SequencerCore<BC, I
block_id: new_block_height,
transactions: valid_transactions,
prev_block_hash: latest_block_meta.hash,
timestamp: curr_time,
timestamp: new_block_timestamp,
};
let block = hashable_data
@ -520,7 +524,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);
let result = sequencer.execute_check_transaction_on_state(tx, 0, 0);
assert!(matches!(
result,
@ -546,7 +550,7 @@ mod tests {
// Passed pre-check
assert!(result.is_ok());
let result = sequencer.execute_check_transaction_on_state(result.unwrap());
let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0);
let is_failed_at_balance_mismatch = matches!(
result.err().unwrap(),
nssa::error::NssaError::ProgramExecutionFailed(_)
@ -568,7 +572,9 @@ mod tests {
acc1, 0, acc2, 100, &sign_key1,
);
sequencer.execute_check_transaction_on_state(tx).unwrap();
sequencer
.execute_check_transaction_on_state(tx, 0, 0)
.unwrap();
let bal_from = sequencer.state.get_account_by_id(acc1).balance;
let bal_to = sequencer.state.get_account_by_id(acc2).balance;

View File

@ -1,9 +1,5 @@
//! Reexports of types used by sequencer rpc specification.
pub use common::{
HashType,
block::{Block, BlockId},
transaction::NSSATransaction,
};
pub use common::{HashType, block::Block, transaction::NSSATransaction};
pub use nssa::{Account, AccountId, ProgramId};
pub use nssa_core::{Commitment, MembershipProof, account::Nonce};
pub use nssa_core::{BlockId, Commitment, MembershipProof, account::Nonce};

View File

@ -188,7 +188,11 @@ impl RocksDBIO {
"transaction pre check failed with err {err:?}"
))
})?
.execute_check_on_state(&mut breakpoint, block.header.block_id)
.execute_check_on_state(
&mut breakpoint,
block.header.block_id,
block.header.timestamp,
)
.map_err(|err| {
DbError::db_interaction_error(format!(
"transaction execution failed with err {err:?}"

View File

@ -38,7 +38,7 @@ fn main() {
program_id: auth_transfer_id,
instruction_data: instruction_data.clone(),
pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], /* <- Account order permutation here */
pda_seeds: pda_seed.iter().cloned().collect(),
pda_seeds: pda_seed.iter().copied().collect(),
};
chained_calls.push(new_chained_call);

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = (Option<Vec<u8>>, bool);
@ -28,7 +28,7 @@ fn main() {
// Claim or not based on the boolean flag
let post_state = if should_claim {
AccountPostState::new_claimed(account_post)
AccountPostState::new_claimed(account_post, Claim::Authorized)
} else {
AccountPostState::new(account_post)
};

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = ();
@ -15,7 +15,7 @@ fn main() {
return;
};
let account_post = AccountPostState::new_claimed(pre.account.clone());
let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Authorized);
ProgramOutput::new(instruction_words, vec![pre], vec![account_post]).write();
}

View File

@ -1,4 +1,4 @@
use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs};
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
type Instruction = Vec<u8>;
@ -25,7 +25,10 @@ fn main() {
ProgramOutput::new(
instruction_words,
vec![pre],
vec![AccountPostState::new_claimed(account_post)],
vec![AccountPostState::new_claimed(
account_post,
Claim::Authorized,
)],
)
.write();
}

View File

@ -1,14 +1,15 @@
use nssa_core::program::{
AccountPostState, ProgramInput, ProgramOutput, ValidityWindow, read_nssa_inputs,
AccountPostState, BlockValidityWindow, ProgramInput, ProgramOutput, TimestampValidityWindow,
read_nssa_inputs,
};
type Instruction = ValidityWindow;
type Instruction = (BlockValidityWindow, TimestampValidityWindow);
fn main() {
let (
ProgramInput {
pre_states,
instruction: validity_window,
instruction: (block_validity_window, timestamp_validity_window),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
@ -24,6 +25,7 @@ fn main() {
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.with_block_validity_window(block_validity_window)
.with_timestamp_validity_window(timestamp_validity_window)
.write();
}

View File

@ -1,21 +1,23 @@
use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, ValidityWindow,
read_nssa_inputs,
AccountPostState, BlockValidityWindow, ChainedCall, ProgramId, ProgramInput, ProgramOutput,
TimestampValidityWindow, read_nssa_inputs,
};
use risc0_zkvm::serde::to_vec;
/// A program that sets a validity window on its output and chains to another program with a
/// potentially different validity window.
/// A program that sets a block validity window on its output and chains to another program with a
/// potentially different block validity window.
///
/// Instruction: (`window`, `chained_program_id`, `chained_window`)
/// The initial output uses `window` and chains to `chained_program_id` with `chained_window`.
type Instruction = (ValidityWindow, ProgramId, ValidityWindow);
/// The chained program (`validity_window`) expects `(BlockValidityWindow, TimestampValidityWindow)`
/// so an unbounded timestamp window is appended automatically.
type Instruction = (BlockValidityWindow, ProgramId, BlockValidityWindow);
fn main() {
let (
ProgramInput {
pre_states,
instruction: (validity_window, chained_program_id, chained_validity_window),
instruction: (block_validity_window, chained_program_id, chained_block_validity_window),
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
@ -23,7 +25,11 @@ fn main() {
let [pre] = <[_; 1]>::try_from(pre_states.clone()).expect("Expected exactly one pre state");
let post = pre.account.clone();
let chained_instruction = to_vec(&chained_validity_window).unwrap();
let chained_instruction = to_vec(&(
chained_block_validity_window,
TimestampValidityWindow::new_unbounded(),
))
.unwrap();
let chained_call = ChainedCall {
program_id: chained_program_id,
instruction_data: chained_instruction,
@ -36,7 +42,7 @@ fn main() {
vec![pre],
vec![AccountPostState::new(post)],
)
.with_validity_window(validity_window)
.with_block_validity_window(block_validity_window)
.with_chained_calls(vec![chained_call])
.write();
}

View File

@ -58,18 +58,21 @@ impl Amm<'_> {
user_holding_lp,
];
let nonces = self
let mut nonces = self
.0
.get_accounts_nonces(vec![user_holding_a, user_holding_b])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let signing_key_a = self
.0
.storage
.user_data
.get_pub_account_signing_key(user_holding_a)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(signing_key_a);
let signing_key_b = self
.0
@ -77,6 +80,26 @@ impl Amm<'_> {
.user_data
.get_pub_account_signing_key(user_holding_b)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(signing_key_b);
if let Some(signing_key_lp) = self
.0
.storage
.user_data
.get_pub_account_signing_key(user_holding_lp)
{
private_keys.push(signing_key_lp);
let lp_nonces = self
.0
.get_accounts_nonces(vec![user_holding_lp])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(lp_nonces);
} else {
println!(
"Liquidity pool tokens receiver's account ({user_holding_lp}) private key not found in wallet. Proceeding with only liquidity provider's keys."
);
}
let message = nssa::public_transaction::Message::try_new(
program.id(),
@ -86,10 +109,8 @@ impl Amm<'_> {
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(
&message,
&[signing_key_a, signing_key_b],
);
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);

View File

@ -23,24 +23,40 @@ impl NativeTokenTransfer<'_> {
.map_err(ExecutionFailureKind::SequencerError)?;
if balance >= balance_to_move {
let nonces = self
let account_ids = vec![from, to];
let program_id = Program::authenticated_transfer_program().id();
let mut nonces = self
.0
.get_accounts_nonces(vec![from])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let account_ids = vec![from, to];
let program_id = Program::authenticated_transfer_program().id();
let message =
Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap();
let signing_key = self.0.storage.user_data.get_pub_account_signing_key(from);
let Some(signing_key) = signing_key else {
let mut private_keys = Vec::new();
let from_signing_key = self.0.storage.user_data.get_pub_account_signing_key(from);
let Some(from_signing_key) = from_signing_key else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
private_keys.push(from_signing_key);
let witness_set = WitnessSet::for_message(&message, &[signing_key]);
let to_signing_key = self.0.storage.user_data.get_pub_account_signing_key(to);
if let Some(to_signing_key) = to_signing_key {
private_keys.push(to_signing_key);
let to_nonces = self
.0
.get_accounts_nonces(vec![to])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(to_nonces);
} else {
println!(
"Receiver's account ({to}) private key not found in wallet. Proceeding with only sender's key."
);
}
let message =
Message::try_new(program_id, account_ids, nonces, balance_to_move).unwrap();
let witness_set = WitnessSet::for_message(&message, &private_keys);
let tx = PublicTransaction::new(message, witness_set);

View File

@ -19,15 +19,36 @@ impl Token<'_> {
let account_ids = vec![definition_account_id, supply_account_id];
let program_id = nssa::program::Program::token().id();
let instruction = Instruction::NewFungibleDefinition { name, total_supply };
let nonces = self
.0
.get_accounts_nonces(account_ids.clone())
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let message = nssa::public_transaction::Message::try_new(
program_id,
account_ids,
vec![],
nonces,
instruction,
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
let def_private_key = self
.0
.storage
.user_data
.get_pub_account_signing_key(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let supply_private_key = self
.0
.storage
.user_data
.get_pub_account_signing_key(supply_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let witness_set = nssa::public_transaction::WitnessSet::for_message(
&message,
&[def_private_key, supply_private_key],
);
let tx = nssa::PublicTransaction::new(message, witness_set);
@ -138,11 +159,40 @@ impl Token<'_> {
let instruction = Instruction::Transfer {
amount_to_transfer: amount,
};
let nonces = self
let mut nonces = self
.0
.get_accounts_nonces(vec![sender_account_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let sender_sk = self
.0
.storage
.user_data
.get_pub_account_signing_key(sender_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(sender_sk);
if let Some(recipient_sk) = self
.0
.storage
.user_data
.get_pub_account_signing_key(recipient_account_id)
{
private_keys.push(recipient_sk);
let recipient_nonces = self
.0
.get_accounts_nonces(vec![recipient_account_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(recipient_nonces);
} else {
println!(
"Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key."
);
}
let message = nssa::public_transaction::Message::try_new(
program_id,
account_ids,
@ -150,17 +200,8 @@ impl Token<'_> {
instruction,
)
.unwrap();
let Some(signing_key) = self
.0
.storage
.user_data
.get_pub_account_signing_key(sender_account_id)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);
@ -477,11 +518,40 @@ impl Token<'_> {
amount_to_mint: amount,
};
let nonces = self
let mut nonces = self
.0
.get_accounts_nonces(vec![definition_account_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let definition_sk = self
.0
.storage
.user_data
.get_pub_account_signing_key(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(definition_sk);
if let Some(holder_sk) = self
.0
.storage
.user_data
.get_pub_account_signing_key(holder_account_id)
{
private_keys.push(holder_sk);
let recipient_nonces = self
.0
.get_accounts_nonces(vec![holder_account_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(recipient_nonces);
} else {
println!(
"Holder's account ({holder_account_id}) private key not found in wallet. Proceeding with only definition's key."
);
}
let message = nssa::public_transaction::Message::try_new(
Program::token().id(),
account_ids,
@ -489,17 +559,8 @@ impl Token<'_> {
instruction,
)
.unwrap();
let Some(signing_key) = self
.0
.storage
.user_data
.get_pub_account_signing_key(definition_account_id)
else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);