Merge branch 'main' into marvin/private_transfer_simplified

This commit is contained in:
jonesmarvin8 2026-04-02 13:11:53 -04:00
commit de6a9f6c59
93 changed files with 2292 additions and 786 deletions

View File

@ -11,6 +11,10 @@ on:
- "**.md"
- "!.github/workflows/*.yml"
permissions:
contents: read
pull-requests: read
name: General
jobs:
@ -19,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install nightly toolchain for rustfmt
run: rustup install nightly --profile minimal --component rustfmt
@ -32,7 +36,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install taplo-cli
run: cargo install --locked taplo-cli
@ -45,7 +49,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install active toolchain
run: rustup install
@ -61,7 +65,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install cargo-deny
run: cargo install --locked cargo-deny
@ -77,7 +81,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
@ -106,7 +110,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
@ -134,7 +138,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
@ -164,7 +168,7 @@ jobs:
# steps:
# - uses: actions/checkout@v5
# with:
# ref: ${{ github.head_ref }}
# ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
# - uses: ./.github/actions/install-system-deps
@ -192,7 +196,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps
@ -218,7 +222,7 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-risc0

32
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"
@ -8684,6 +8657,7 @@ dependencies = [
"async-stream",
"ata_core",
"base58",
"bip39",
"clap",
"common",
"env_logger",

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

@ -256,11 +256,11 @@ impl TestContext {
let config_overrides = WalletConfigOverrides::default();
let wallet_password = "test_pass".to_owned();
let wallet = WalletCore::new_init_storage(
let (wallet, _mnemonic) = WalletCore::new_init_storage(
config_path,
storage_path,
Some(config_overrides),
wallet_password.clone(),
&wallet_password,
)
.context("Failed to init wallet")?;
wallet

View File

@ -223,7 +223,7 @@ async fn amm_public() -> Result<()> {
// Make swap
let subcommand = AmmProgramAgnosticSubcommand::Swap {
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: format_public_account_id(recipient_account_id_1),
user_holding_b: format_public_account_id(recipient_account_id_2),
amount_in: 2,
@ -266,7 +266,7 @@ async fn amm_public() -> Result<()> {
// Make swap
let subcommand = AmmProgramAgnosticSubcommand::Swap {
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: format_public_account_id(recipient_account_id_1),
user_holding_b: format_public_account_id(recipient_account_id_2),
amount_in: 2,

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

@ -24,7 +24,6 @@ use log::info;
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir;
use wallet::WalletCore;
use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, WalletHandle, error,
@ -211,14 +210,6 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl
})
}
fn new_wallet_rust_with_default_config(password: &str) -> Result<WalletCore> {
let tempdir = tempdir()?;
let config_path = tempdir.path().join("wallet_config.json");
let storage_path = tempdir.path().join("storage.json");
WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned())
}
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
let config_path = home.join("wallet_config.json");
let storage_path = home.join("storage.json");
@ -232,19 +223,8 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
fn wallet_ffi_create_public_accounts() -> Result<()> {
let password = "password_for_tests";
let n_accounts = 10;
// First `n_accounts` public accounts created with Rust wallet
let new_public_account_ids_rust = {
let mut account_ids = Vec::new();
let mut wallet_rust = new_wallet_rust_with_default_config(password)?;
for _ in 0..n_accounts {
let account_id = wallet_rust.create_new_account_public(None).0;
account_ids.push(*account_id.value());
}
account_ids
};
// First `n_accounts` public accounts created with wallet FFI
// Create `n_accounts` public accounts with wallet FFI
let new_public_account_ids_ffi = unsafe {
let mut account_ids = Vec::new();
@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
account_ids
};
assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust);
// All returned IDs must be unique and non-zero
assert_eq!(new_public_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_public_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate public account IDs returned"
);
assert!(
new_public_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
);
Ok(())
}
@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests";
let n_accounts = 10;
// First `n_accounts` private accounts created with Rust wallet
let new_private_account_ids_rust = {
let mut account_ids = Vec::new();
let mut wallet_rust = new_wallet_rust_with_default_config(password)?;
for _ in 0..n_accounts {
let account_id = wallet_rust.create_new_account_private(None).0;
account_ids.push(*account_id.value());
}
account_ids
};
// First `n_accounts` private accounts created with wallet FFI
// Create `n_accounts` private accounts with wallet FFI
let new_private_account_ids_ffi = unsafe {
let mut account_ids = Vec::new();
@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
account_ids
};
assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust);
// All returned IDs must be unique and non-zero
assert_eq!(new_private_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate private account IDs returned"
);
assert!(
new_private_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
);
Ok(())
}
@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
fn test_wallet_ffi_list_accounts() -> Result<()> {
let password = "password_for_tests";
// Create the wallet FFI
let wallet_ffi_handle = unsafe {
// Create the wallet FFI and track which account IDs were created as public/private
let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?;
// Create 5 public accounts and 5 private accounts
let mut public_ids: Vec<[u8; 32]> = Vec::new();
let mut private_ids: Vec<[u8; 32]> = Vec::new();
// Create 5 public accounts and 5 private accounts, recording their IDs
for _ in 0..5 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id);
public_ids.push(out_account_id.data);
wallet_ffi_create_account_private(handle, &raw mut out_account_id);
private_ids.push(out_account_id.data);
}
handle
};
// Create the wallet Rust
let wallet_rust = {
let mut wallet = new_wallet_rust_with_default_config(password)?;
// Create 5 public accounts and 5 private accounts
for _ in 0..5 {
wallet.create_new_account_public(None);
wallet.create_new_account_private(None);
}
wallet
(handle, public_ids, private_ids)
};
// Get the account list with FFI method
@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
out_list
};
let wallet_rust_account_ids = wallet_rust
.storage()
.user_data
.account_ids()
.collect::<Vec<_>>();
// Assert same number of elements between Rust and FFI result
assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count);
let wallet_ffi_account_list_slice = unsafe {
core::slice::from_raw_parts(
wallet_ffi_account_list.entries,
@ -396,37 +376,38 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
)
};
// Assert same account ids between Rust and FFI result
assert_eq!(
wallet_rust_account_ids
.iter()
.map(nssa::AccountId::value)
.collect::<HashSet<_>>(),
wallet_ffi_account_list_slice
.iter()
.map(|entry| &entry.account_id.data)
.collect::<HashSet<_>>()
);
// All created accounts must appear in the list
let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter()
.filter(|e| e.is_public)
.map(|e| e.account_id.data)
.collect();
let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter()
.filter(|e| !e.is_public)
.map(|e| e.account_id.data)
.collect();
// Assert `is_pub` flag is correct in the FFI result
for entry in wallet_ffi_account_list_slice {
let account_id = AccountId::new(entry.account_id.data);
let is_pub_default_in_rust_wallet = wallet_rust
.storage()
.user_data
.default_pub_account_signing_keys
.contains_key(&account_id);
let is_pub_key_tree_wallet_rust = wallet_rust
.storage()
.user_data
.public_key_tree
.account_id_map
.contains_key(&account_id);
let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust;
assert_eq!(entry.is_public, is_public_in_rust_wallet);
for id in &created_public_ids {
assert!(
listed_public_ids.contains(id),
"Created public account not found in list with is_public=true"
);
}
for id in &created_private_ids {
assert!(
listed_private_ids.contains(id),
"Created private account not found in list with is_public=false"
);
}
// Total listed accounts must be at least the number we created
assert!(
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(),
"Listed account count ({}) is less than the number of created accounts ({})",
wallet_ffi_account_list.count,
created_public_ids.len() + created_private_ids.len()
);
unsafe {
wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list);
@ -924,7 +905,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 +948,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;
@ -13,7 +13,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![];
@ -21,16 +20,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.csk.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.csk.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)
@ -41,7 +41,12 @@ impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
let csk = 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 ccc = *hash_value.last_chunk::<32>().unwrap();
let cpk = nssa::PublicKey::new_from_private_key(&csk);
@ -56,26 +61,20 @@ impl KeyNode for ChildKeysPublic {
fn nth_child(&self, cci: u32) -> Self {
let hash_value = self.compute_hash_value(cci);
let csk = secp256k1::SecretKey::from_byte_array(
*hash_value
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32"),
)
.unwrap();
let csk = nssa::PrivateKey::try_new({
let scalar = Scalar::from_be_bytes(*self.csk.value()).unwrap();
let hash_value = hash_value
.first_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get first 32");
csk.add_tweak(&scalar)
.expect("Expect a valid Scalar")
.secret_bytes()
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()
})
.unwrap();
assert!(
secp256k1::constants::CURVE_ORDER >= *csk.value(),
"Secret key cannot exceed curve order"
);
.expect("Expect a valid private key");
let ccc = *hash_value
.last_chunk::<32>()

View File

@ -42,10 +42,10 @@ impl KeyChain {
}
#[must_use]
pub fn new_mnemonic(passphrase: String) -> Self {
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
// Currently dropping SeedHolder at the end of initialization.
// Not entirely sure if we need it in the future.
let seed_holder = SeedHolder::new_mnemonic(passphrase);
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
@ -53,12 +53,15 @@ impl KeyChain {
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
let viewing_public_key = private_key_holder.generate_viewing_public_key();
Self {
secret_spending_key,
private_key_holder,
nullifier_public_key,
viewing_public_key,
}
(
Self {
secret_spending_key,
private_key_holder,
nullifier_public_key,
viewing_public_key,
},
mnemonic,
)
}
#[must_use]

View File

@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, digest::FixedOutput as _};
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
/// Produces `TopSecretKeyHolder` objects.
#[derive(Debug)]
@ -48,9 +46,24 @@ impl SeedHolder {
}
#[must_use]
pub fn new_mnemonic(passphrase: String) -> Self {
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
.expect("Enthropy must be a multiple of 32 bytes");
pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) {
let mut entropy_bytes: [u8; 32] = [0; 32];
OsRng.fill_bytes(&mut entropy_bytes);
let mnemonic =
Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes");
let seed_wide = mnemonic.to_seed(passphrase);
(
Self {
seed: seed_wide.to_vec(),
},
mnemonic,
)
}
#[must_use]
pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self {
let seed_wide = mnemonic.to_seed(passphrase);
Self {
@ -175,12 +188,63 @@ mod tests {
}
#[test]
fn two_seeds_generated_same_from_same_mnemonic() {
let mnemonic = "test_pass";
fn two_seeds_recovered_same_from_same_mnemonic() {
let passphrase = "test_pass";
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned());
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned());
// Generate a mnemonic with random entropy
let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
assert_eq!(seed_holder1.seed, seed_holder2.seed);
// Recover from the same mnemonic
let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase);
assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed);
}
#[test]
fn new_mnemonic_generates_different_seeds_each_time() {
let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic("");
let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic("");
// Different entropy should produce different mnemonics and seeds
assert_ne!(mnemonic1.to_string(), mnemonic2.to_string());
assert_ne!(seed_holder1.seed, seed_holder2.seed);
}
#[test]
fn new_mnemonic_generates_24_word_phrase() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
// 256 bits of entropy produces a 24-word mnemonic
let word_count = mnemonic.to_string().split_whitespace().count();
assert_eq!(word_count, 24);
}
#[test]
fn new_mnemonic_produces_valid_seed_length() {
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
assert_eq!(seed_holder.seed.len(), 64);
}
#[test]
fn different_passphrases_produce_different_seeds() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a");
let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b");
// Same mnemonic but different passphrases should produce different seeds
assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed);
}
#[test]
fn empty_passphrase_is_deterministic() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
let seed1 = SeedHolder::from_mnemonic(&mnemonic, "");
let seed2 = SeedHolder::from_mnemonic(&mnemonic, "");
// Same mnemonic and passphrase should always produce the same seed
assert_eq!(seed1.seed, seed2.seed);
}
}

View File

@ -181,11 +181,12 @@ impl NSSAUserData {
impl Default for NSSAUserData {
fn default() -> Self {
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
Self::new_with_accounts(
BTreeMap::new(),
BTreeMap::new(),
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())),
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())),
KeyTreePublic::new(&seed_holder),
KeyTreePrivate::new(&seed_holder),
)
.unwrap()
}

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")]
@ -107,7 +108,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, PrivacyPreservingCircuitOutput,
account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
program::ValidityWindow,
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -56,7 +56,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 {
@ -82,7 +83,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()
}
}
@ -115,7 +117,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,
})
}
}
@ -126,6 +129,7 @@ pub mod tests {
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey,
account::Account,
encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow},
};
use sha2::{Digest as _, Sha256};
@ -170,7 +174,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

@ -45,7 +45,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::{
@ -457,16 +462,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)
}
@ -568,17 +576,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]
@ -589,12 +598,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);
@ -614,16 +624,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]
@ -634,21 +645,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]
@ -663,7 +691,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)));
}
@ -680,7 +708,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)));
}
@ -697,7 +725,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)));
}
@ -721,7 +749,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)));
}
@ -745,7 +773,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)));
}
@ -769,7 +797,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)));
}
@ -793,7 +821,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)));
}
@ -821,7 +849,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)));
}
@ -846,7 +874,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)));
}
@ -864,7 +892,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)));
}
@ -893,7 +921,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)));
}
@ -1090,7 +1118,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());
@ -1163,7 +1191,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);
@ -1228,7 +1256,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());
@ -2180,7 +2208,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 {
@ -2198,7 +2226,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 {
@ -2246,15 +2274,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
@ -2263,26 +2290,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();
@ -2319,7 +2400,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);
@ -2359,7 +2440,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)
@ -2400,7 +2481,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);
@ -2416,15 +2497,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
@ -2434,6 +2514,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()
};
@ -2449,14 +2530,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);
@ -2464,6 +2546,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) {
@ -2567,7 +2731,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
@ -2607,36 +2771,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;
@ -2653,7 +2828,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!(
@ -2683,7 +2858,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)));
}
@ -2729,7 +2904,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);
@ -2799,13 +2974,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(&account_id);
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();
@ -2853,7 +3075,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()
);
@ -2902,7 +3124,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());
@ -2927,7 +3149,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)));
@ -3058,7 +3280,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());
@ -3067,21 +3289,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 {
@ -3108,7 +3385,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 account_id = AccountId::account_id_without_identifier(&account_keys.npk());
@ -3119,9 +3396,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![],
@ -3141,11 +3422,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

@ -112,15 +112,15 @@ fn main() {
min_amount_to_remove_token_b,
)
}
Instruction::Swap {
Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in,
} => {
let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states
.try_into()
.expect("Transfer instruction requires exactly five accounts");
amm_program::swap::swap(
.expect("SwapExactInput instruction requires exactly five accounts");
amm_program::swap::swap_exact_input(
pool,
vault_a,
vault_b,
@ -131,6 +131,25 @@ fn main() {
token_definition_id_in,
)
}
Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
} => {
let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states
.try_into()
.expect("SwapExactOutput instruction requires exactly five accounts");
amm_program::swap::swap_exact_output(
pool,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
exact_amount_out,
max_amount_in,
token_definition_id_in,
)
}
};
ProgramOutput::new(instruction_words, pre_states_clone, post_states)

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();
@ -412,7 +484,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

@ -68,11 +68,27 @@ pub enum Instruction {
/// - User Holding Account for Token A
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
/// authorized.
Swap {
SwapExactInput {
swap_amount_in: u128,
min_amount_out: u128,
token_definition_id_in: AccountId,
},
/// Swap tokens specifying the exact desired output amount,
/// while maintaining the Pool constant product.
///
/// Required accounts:
/// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
/// authorized.
SwapExactOutput {
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
},
}
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]

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

@ -4,21 +4,14 @@ use nssa_core::{
program::{AccountPostState, ChainedCall},
};
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
#[must_use]
pub fn swap(
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
swap_amount_in: u128,
min_amount_out: u128,
token_in_id: AccountId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
// Verify vaults are in fact vaults
/// Validates swap setup: checks pool is active, vaults match, and reserves are sufficient.
fn validate_swap_setup(
pool: &AccountWithMetadata,
vault_a: &AccountWithMetadata,
vault_b: &AccountWithMetadata,
) -> PoolDefinition {
let pool_def_data = PoolDefinition::try_from(&pool.account.data)
.expect("Swap: AMM Program expects a valid Pool Definition Account");
.expect("AMM Program expects a valid Pool Definition Account");
assert!(pool_def_data.active, "Pool is inactive");
assert_eq!(
@ -30,16 +23,14 @@ pub fn swap(
"Vault B was not provided"
);
// fetch pool reserves
// validates reserves is at least the vaults' balances
let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data)
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault A");
.expect("AMM Program expects a valid Token Holding Account for Vault A");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_a_balance,
} = vault_a_token_holding
else {
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A");
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A");
};
assert!(
@ -48,13 +39,13 @@ pub fn swap(
);
let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data)
.expect("Swap: AMM Program expects a valid Token Holding Account for Vault B");
.expect("AMM Program expects a valid Token Holding Account for Vault B");
let token_core::TokenHolding::Fungible {
definition_id: _,
balance: vault_b_balance,
} = vault_b_token_holding
else {
panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B");
panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B");
};
assert!(
@ -62,6 +53,59 @@ pub fn swap(
"Reserve for Token B exceeds vault balance"
);
pool_def_data
}
/// Creates post-state and returns reserves after swap.
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
#[expect(
clippy::needless_pass_by_value,
reason = "consistent with codebase style"
)]
fn create_swap_post_states(
pool: AccountWithMetadata,
pool_def_data: PoolDefinition,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
deposit_a: u128,
withdraw_a: u128,
deposit_b: u128,
withdraw_b: u128,
) -> Vec<AccountPostState> {
let mut pool_post = pool.account;
let pool_post_definition = PoolDefinition {
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
..pool_def_data
};
pool_post.data = Data::from(&pool_post_definition);
vec![
AccountPostState::new(pool_post),
AccountPostState::new(vault_a.account),
AccountPostState::new(vault_b.account),
AccountPostState::new(user_holding_a.account),
AccountPostState::new(user_holding_b.account),
]
}
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
#[must_use]
pub fn swap_exact_input(
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
swap_amount_in: u128,
min_amount_out: u128,
token_in_id: AccountId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
if token_in_id == pool_def_data.definition_token_a_id {
let (chained_calls, deposit_a, withdraw_b) = swap_logic(
@ -95,23 +139,18 @@ pub fn swap(
panic!("AccountId is not a token type for the pool");
};
// Update pool account
let mut pool_post = pool.account;
let pool_post_definition = PoolDefinition {
reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a,
reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b,
..pool_def_data
};
pool_post.data = Data::from(&pool_post_definition);
let post_states = vec![
AccountPostState::new(pool_post),
AccountPostState::new(vault_a.account),
AccountPostState::new(vault_b.account),
AccountPostState::new(user_holding_a.account),
AccountPostState::new(user_holding_b.account),
];
let post_states = create_swap_post_states(
pool,
pool_def_data,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
deposit_a,
withdraw_a,
deposit_b,
withdraw_b,
);
(post_states, chained_calls)
}
@ -131,7 +170,9 @@ fn swap_logic(
// Compute withdraw amount
// Maintains pool constant product
// k = pool_def_data.reserve_a * pool_def_data.reserve_b;
let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in)
let withdraw_amount = reserve_withdraw_vault_amount
.checked_mul(swap_amount_in)
.expect("reserve * amount_in overflows u128")
/ (reserve_deposit_vault_amount + swap_amount_in);
// Slippage check
@ -175,3 +216,135 @@ fn swap_logic(
(chained_calls, swap_amount_in, withdraw_amount)
}
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
#[must_use]
pub fn swap_exact_output(
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
exact_amount_out: u128,
max_amount_in: u128,
token_in_id: AccountId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) =
if token_in_id == pool_def_data.definition_token_a_id {
let (chained_calls, deposit_a, withdraw_b) = exact_output_swap_logic(
user_holding_a.clone(),
vault_a.clone(),
vault_b.clone(),
user_holding_b.clone(),
exact_amount_out,
max_amount_in,
pool_def_data.reserve_a,
pool_def_data.reserve_b,
pool.account_id,
);
(chained_calls, [deposit_a, 0], [0, withdraw_b])
} else if token_in_id == pool_def_data.definition_token_b_id {
let (chained_calls, deposit_b, withdraw_a) = exact_output_swap_logic(
user_holding_b.clone(),
vault_b.clone(),
vault_a.clone(),
user_holding_a.clone(),
exact_amount_out,
max_amount_in,
pool_def_data.reserve_b,
pool_def_data.reserve_a,
pool.account_id,
);
(chained_calls, [0, withdraw_a], [deposit_b, 0])
} else {
panic!("AccountId is not a token type for the pool");
};
let post_states = create_swap_post_states(
pool,
pool_def_data,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
deposit_a,
withdraw_a,
deposit_b,
withdraw_b,
);
(post_states, chained_calls)
}
#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")]
fn exact_output_swap_logic(
user_deposit: AccountWithMetadata,
vault_deposit: AccountWithMetadata,
vault_withdraw: AccountWithMetadata,
user_withdraw: AccountWithMetadata,
exact_amount_out: u128,
max_amount_in: u128,
reserve_deposit_vault_amount: u128,
reserve_withdraw_vault_amount: u128,
pool_id: AccountId,
) -> (Vec<ChainedCall>, u128, u128) {
// Guard: exact_amount_out must be nonzero
assert_ne!(exact_amount_out, 0, "Exact amount out must be nonzero");
// Guard: exact_amount_out must be less than reserve_withdraw_vault_amount
assert!(
exact_amount_out < reserve_withdraw_vault_amount,
"Exact amount out exceeds reserve"
);
// Compute deposit amount using ceiling division
// Formula: amount_in = ceil(reserve_in * exact_amount_out / (reserve_out - exact_amount_out))
let deposit_amount = reserve_deposit_vault_amount
.checked_mul(exact_amount_out)
.expect("reserve * amount_out overflows u128")
.div_ceil(reserve_withdraw_vault_amount - exact_amount_out);
// Slippage check
assert!(
deposit_amount <= max_amount_in,
"Required input exceeds maximum amount in"
);
let token_program_id = user_deposit.account.program_owner;
let mut chained_calls = Vec::new();
chained_calls.push(ChainedCall::new(
token_program_id,
vec![user_deposit, vault_deposit],
&token_core::Instruction::Transfer {
amount_to_transfer: deposit_amount,
},
));
let mut vault_withdraw = vault_withdraw;
vault_withdraw.is_authorized = true;
let pda_seed = compute_vault_pda_seed(
pool_id,
token_core::TokenHolding::try_from(&vault_withdraw.account.data)
.expect("Exact Output Swap Logic: AMM Program expects valid token data")
.definition_id(),
);
chained_calls.push(
ChainedCall::new(
token_program_id,
vec![vault_withdraw, user_withdraw],
&token_core::Instruction::Transfer {
amount_to_transfer: exact_amount_out,
},
)
.with_pda_seeds(vec![pda_seed]),
);
(chained_calls, deposit_amount, exact_amount_out)
}

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,
@ -14,7 +14,10 @@ use nssa_core::{
use token_core::{TokenDefinition, TokenHolding};
use crate::{
add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap,
add::add_liquidity,
new_definition::new_definition,
remove::remove_liquidity,
swap::{swap_exact_input, swap_exact_output},
};
const TOKEN_PROGRAM_ID: ProgramId = [15; 8];
@ -153,6 +156,10 @@ impl BalanceForTests {
200
}
fn max_amount_in() -> u128 {
166
}
fn vault_a_add_successful() -> u128 {
1_400
}
@ -243,6 +250,74 @@ impl ChainedCallForTests {
)
}
fn cc_swap_exact_output_token_a_test_1() -> ChainedCall {
let swap_amount: u128 = 498;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::vault_a_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
}
fn cc_swap_exact_output_token_b_test_1() -> ChainedCall {
let swap_amount: u128 = 166;
let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init();
vault_b_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_b_definition_id(),
)])
}
fn cc_swap_exact_output_token_a_test_2() -> ChainedCall {
let swap_amount: u128 = 285;
let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init();
vault_a_auth.is_authorized = true;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
.with_pda_seeds(vec![compute_vault_pda_seed(
IdForTests::pool_definition_id(),
IdForTests::token_a_definition_id(),
)])
}
fn cc_swap_exact_output_token_b_test_2() -> ChainedCall {
let swap_amount: u128 = 200;
ChainedCall::new(
TOKEN_PROGRAM_ID,
vec![
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::vault_b_init(),
],
&token_core::Instruction::Transfer {
amount_to_transfer: swap_amount,
},
)
}
fn cc_add_token_a() -> ChainedCall {
ChainedCall::new(
TOKEN_PROGRAM_ID,
@ -829,6 +904,54 @@ impl AccountWithMetadataForTests {
}
}
fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: 1498_u128,
reserve_b: 334_u128,
fees: 0_u128,
active: true,
}),
nonce: 0_u128.into(),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: BalanceForTests::lp_supply_init(),
reserve_a: BalanceForTests::vault_a_swap_test_2(),
reserve_b: BalanceForTests::vault_b_swap_test_2(),
fees: 0_u128,
active: true,
}),
nonce: 0_u128.into(),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
}
}
fn pool_definition_add_zero_lp() -> AccountWithMetadata {
AccountWithMetadata {
account: Account {
@ -1756,7 +1879,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 +1924,7 @@ impl AccountsForExeTests {
definition_id: IdForExeTests::token_lp_definition_id(),
balance: 0,
}),
nonce: 0_u128.into(),
nonce: 1.into(),
}
}
}
@ -2400,7 +2523,7 @@ fn call_new_definition_chained_call_successful() {
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[test]
fn call_swap_incorrect_token_type() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
@ -2415,7 +2538,7 @@ fn call_swap_incorrect_token_type() {
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn call_swap_vault_a_omitted() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
@ -2430,7 +2553,7 @@ fn call_swap_vault_a_omitted() {
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn call_swap_vault_b_omitted() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
@ -2445,7 +2568,7 @@ fn call_swap_vault_b_omitted() {
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
#[test]
fn call_swap_reserves_vault_mismatch_1() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init(),
@ -2460,7 +2583,7 @@ fn call_swap_reserves_vault_mismatch_1() {
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
#[test]
fn call_swap_reserves_vault_mismatch_2() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_low(),
@ -2475,7 +2598,7 @@ fn call_swap_reserves_vault_mismatch_2() {
#[should_panic(expected = "Pool is inactive")]
#[test]
fn call_swap_ianctive() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_inactive(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
@ -2490,7 +2613,7 @@ fn call_swap_ianctive() {
#[should_panic(expected = "Withdraw amount is less than minimal amount out")]
#[test]
fn call_swap_below_min_out() {
let _post_states = swap(
let _post_states = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
@ -2504,7 +2627,7 @@ fn call_swap_below_min_out() {
#[test]
fn call_swap_chained_call_successful_1() {
let (post_states, chained_calls) = swap(
let (post_states, chained_calls) = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
@ -2536,7 +2659,7 @@ fn call_swap_chained_call_successful_1() {
#[test]
fn call_swap_chained_call_successful_2() {
let (post_states, chained_calls) = swap(
let (post_states, chained_calls) = swap_exact_input(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
@ -2566,6 +2689,281 @@ fn call_swap_chained_call_successful_2() {
);
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[test]
fn call_swap_exact_output_incorrect_token_type() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_lp_definition_id(),
);
}
#[should_panic(expected = "Vault A was not provided")]
#[test]
fn call_swap_exact_output_vault_a_omitted() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_with_wrong_id(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Vault B was not provided")]
#[test]
fn call_swap_exact_output_vault_b_omitted() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_with_wrong_id(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token A exceeds vault balance")]
#[test]
fn call_swap_exact_output_reserves_vault_mismatch_1() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init_low(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Reserve for Token B exceeds vault balance")]
#[test]
fn call_swap_exact_output_reserves_vault_mismatch_2() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init_low(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Pool is inactive")]
#[test]
fn call_swap_exact_output_inactive() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_inactive(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Required input exceeds maximum amount in")]
#[test]
fn call_swap_exact_output_exceeds_max_in() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
166_u128,
100_u128,
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Exact amount out must be nonzero")]
#[test]
fn call_swap_exact_output_zero() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
0_u128,
500_u128,
IdForTests::token_a_definition_id(),
);
}
#[should_panic(expected = "Exact amount out exceeds reserve")]
#[test]
fn call_swap_exact_output_exceeds_reserve() {
let _post_states = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::vault_b_reserve_init(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
);
}
#[test]
fn call_swap_exact_output_chained_call_successful() {
let (post_states, chained_calls) = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
BalanceForTests::max_amount_in(),
BalanceForTests::vault_b_reserve_init(),
IdForTests::token_a_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account
== *pool_post.account()
);
let chained_call_a = chained_calls[0].clone();
let chained_call_b = chained_calls[1].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_exact_output_token_a_test_1()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_exact_output_token_b_test_1()
);
}
#[test]
fn call_swap_exact_output_chained_call_successful_2() {
let (post_states, chained_calls) = swap_exact_output(
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
285,
300,
IdForTests::token_b_definition_id(),
);
let pool_post = post_states[0].clone();
assert!(
AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account
== *pool_post.account()
);
let chained_call_a = chained_calls[1].clone();
let chained_call_b = chained_calls[0].clone();
assert_eq!(
chained_call_a,
ChainedCallForTests::cc_swap_exact_output_token_a_test_2()
);
assert_eq!(
chained_call_b,
ChainedCallForTests::cc_swap_exact_output_token_b_test_2()
);
}
// Without the fix, `reserve_a * exact_amount_out` silently wraps to 0 in release mode,
// making `deposit_amount = 0`. The slippage check `0 <= max_amount_in` always passes,
// so an attacker receives `exact_amount_out` tokens while paying nothing.
#[should_panic(expected = "reserve * amount_out overflows u128")]
#[test]
fn swap_exact_output_overflow_protection() {
// reserve_a chosen so that reserve_a * 2 overflows u128:
// (u128::MAX / 2 + 1) * 2 = u128::MAX + 1 → wraps to 0
let large_reserve: u128 = u128::MAX / 2 + 1;
let reserve_b: u128 = 1_000;
let pool = AccountWithMetadata {
account: Account {
program_owner: ProgramId::default(),
balance: 0,
data: Data::from(&PoolDefinition {
definition_token_a_id: IdForTests::token_a_definition_id(),
definition_token_b_id: IdForTests::token_b_definition_id(),
vault_a_id: IdForTests::vault_a_id(),
vault_b_id: IdForTests::vault_b_id(),
liquidity_pool_id: IdForTests::token_lp_definition_id(),
liquidity_pool_supply: 1,
reserve_a: large_reserve,
reserve_b,
fees: 0,
active: true,
}),
nonce: 0_u128.into(),
},
is_authorized: true,
account_id: IdForTests::pool_definition_id(),
};
let vault_a = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_a_definition_id(),
balance: large_reserve,
}),
nonce: 0_u128.into(),
},
is_authorized: true,
account_id: IdForTests::vault_a_id(),
};
let vault_b = AccountWithMetadata {
account: Account {
program_owner: TOKEN_PROGRAM_ID,
balance: 0,
data: Data::from(&TokenHolding::Fungible {
definition_id: IdForTests::token_b_definition_id(),
balance: reserve_b,
}),
nonce: 0_u128.into(),
},
is_authorized: true,
account_id: IdForTests::vault_b_id(),
};
let _result = swap_exact_output(
pool,
vault_a,
vault_b,
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
2, // exact_amount_out: small, valid (< reserve_b)
1, // max_amount_in: tiny — real deposit would be enormous, but
// overflow wraps it to 0, making 0 <= 1 pass silently
IdForTests::token_a_definition_id(),
);
}
#[test]
fn new_definition_lp_asymmetric_amounts() {
let (post_states, chained_calls) = new_definition(
@ -2733,7 +3131,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 +3197,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 +3207,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 +3296,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 +3354,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 +3364,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 +3431,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());
@ -3062,7 +3462,7 @@ fn simple_amm_add() {
fn simple_amm_swap_1() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::Swap {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: BalanceForExeTests::swap_amount_in(),
min_amount_out: BalanceForExeTests::swap_min_amount_out(),
token_definition_id_in: IdForExeTests::token_b_definition_id(),
@ -3088,7 +3488,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());
@ -3113,7 +3513,7 @@ fn simple_amm_swap_1() {
fn simple_amm_swap_2() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::Swap {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: BalanceForExeTests::swap_amount_in(),
min_amount_out: BalanceForExeTests::swap_min_amount_out(),
token_definition_id_in: IdForExeTests::token_a_definition_id(),
@ -3138,7 +3538,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::{AccountId, 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

@ -111,8 +111,8 @@ pub unsafe extern "C" fn wallet_ffi_create_new(
return ptr::null_mut();
};
match WalletCore::new_init_storage(config_path, storage_path, None, password) {
Ok(core) => {
match WalletCore::new_init_storage(config_path, storage_path, None, &password) {
Ok((core, _mnemonic)) => {
let wrapper = Box::new(WalletWrapper {
core: Mutex::new(core),
});

View File

@ -17,6 +17,7 @@ token_core.workspace = true
amm_core.workspace = true
testnet_initial_state.workspace = true
ata_core.workspace = true
bip39.workspace = true
anyhow.workspace = true
thiserror.workspace = true

View File

@ -1,6 +1,7 @@
use std::collections::{BTreeMap, HashMap, btree_map::Entry};
use anyhow::Result;
use bip39::Mnemonic;
use key_protocol::{
key_management::{
key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex},
@ -95,7 +96,7 @@ impl WalletChainStore {
})
}
pub fn new_storage(config: WalletConfig, password: String) -> Result<Self> {
pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> {
let mut public_init_acc_map = BTreeMap::new();
let mut private_init_acc_map = BTreeMap::new();
@ -121,13 +122,43 @@ impl WalletChainStore {
}
}
let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone()));
let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password));
// TODO: Use password for storage encryption
let _ = password;
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
let public_tree = KeyTreePublic::new(&seed_holder);
let private_tree = KeyTreePrivate::new(&seed_holder);
Ok((
Self {
user_data: NSSAUserData::new_with_accounts(
public_init_acc_map,
private_init_acc_map,
public_tree,
private_tree,
)?,
wallet_config: config,
labels: HashMap::new(),
},
mnemonic,
))
}
/// Restore storage from an existing mnemonic phrase.
pub fn restore_storage(
config: WalletConfig,
mnemonic: &Mnemonic,
password: &str,
) -> Result<Self> {
// TODO: Use password for storage encryption
let _ = password;
let seed_holder = SeedHolder::from_mnemonic(mnemonic, "");
let public_tree = KeyTreePublic::new(&seed_holder);
let private_tree = KeyTreePrivate::new(&seed_holder);
Ok(Self {
user_data: NSSAUserData::new_with_accounts(
public_init_acc_map,
private_init_acc_map,
BTreeMap::new(),
BTreeMap::new(),
public_tree,
private_tree,
)?,

View File

@ -1,6 +1,7 @@
use std::{io::Write as _, path::PathBuf};
use std::{io::Write as _, path::PathBuf, str::FromStr as _};
use anyhow::{Context as _, Result};
use bip39::Mnemonic;
use clap::{Parser, Subcommand};
use common::{HashType, transaction::NSSATransaction};
use futures::TryFutureExt as _;
@ -167,8 +168,9 @@ pub async fn execute_subcommand(
config_subcommand.handle_subcommand(wallet_core).await?
}
Command::RestoreKeys { depth } => {
let mnemonic = read_mnemonic_from_stdin()?;
let password = read_password_from_stdin()?;
wallet_core.reset_storage(password)?;
wallet_core.restore_storage(&mnemonic, &password)?;
execute_keys_restoration(wallet_core, depth).await?;
SubcommandReturnValue::Empty
@ -212,6 +214,16 @@ pub fn read_password_from_stdin() -> Result<String> {
Ok(password.trim().to_owned())
}
pub fn read_mnemonic_from_stdin() -> Result<Mnemonic> {
let mut phrase = String::new();
print!("Input recovery phrase: ");
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut phrase)?;
Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase")
}
pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> {
wallet_core
.storage

View File

@ -32,12 +32,12 @@ pub enum AmmProgramAgnosticSubcommand {
#[arg(long)]
balance_b: u128,
},
/// Swap.
/// Swap specifying exact input amount.
///
/// The account associated with swapping token must be owned.
///
/// Only public execution allowed.
Swap {
SwapExactInput {
/// `user_holding_a` - valid 32 byte base58 string with privacy prefix.
#[arg(long)]
user_holding_a: String,
@ -52,6 +52,26 @@ pub enum AmmProgramAgnosticSubcommand {
#[arg(long)]
token_definition: String,
},
/// Swap specifying exact output amount.
///
/// The account associated with swapping token must be owned.
///
/// Only public execution allowed.
SwapExactOutput {
/// `user_holding_a` - valid 32 byte base58 string with privacy prefix.
#[arg(long)]
user_holding_a: String,
/// `user_holding_b` - valid 32 byte base58 string with privacy prefix.
#[arg(long)]
user_holding_b: String,
#[arg(long)]
exact_amount_out: u128,
#[arg(long)]
max_amount_in: u128,
/// `token_definition` - valid 32 byte base58 string WITHOUT privacy prefix.
#[arg(long)]
token_definition: String,
},
/// Add liquidity.
///
/// `user_holding_a` and `user_holding_b` must be owned.
@ -150,7 +170,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
}
}
}
Self::Swap {
Self::SwapExactInput {
user_holding_a,
user_holding_b,
amount_in,
@ -168,7 +188,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
match (user_holding_a_privacy, user_holding_b_privacy) {
(AccountPrivacyKind::Public, AccountPrivacyKind::Public) => {
Amm(wallet_core)
.send_swap(
.send_swap_exact_input(
user_holding_a,
user_holding_b,
amount_in,
@ -185,6 +205,41 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand {
}
}
}
Self::SwapExactOutput {
user_holding_a,
user_holding_b,
exact_amount_out,
max_amount_in,
token_definition,
} => {
let (user_holding_a, user_holding_a_privacy) =
parse_addr_with_privacy_prefix(&user_holding_a)?;
let (user_holding_b, user_holding_b_privacy) =
parse_addr_with_privacy_prefix(&user_holding_b)?;
let user_holding_a: AccountId = user_holding_a.parse()?;
let user_holding_b: AccountId = user_holding_b.parse()?;
match (user_holding_a_privacy, user_holding_b_privacy) {
(AccountPrivacyKind::Public, AccountPrivacyKind::Public) => {
Amm(wallet_core)
.send_swap_exact_output(
user_holding_a,
user_holding_b,
exact_amount_out,
max_amount_in,
token_definition.parse()?,
)
.await?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
// ToDo: Implement after private multi-chain calls is available
anyhow::bail!("Only public execution allowed for Amm calls");
}
}
}
Self::AddLiquidity {
user_holding_a,
user_holding_b,

View File

@ -11,6 +11,7 @@
use std::path::PathBuf;
use anyhow::{Context as _, Result};
use bip39::Mnemonic;
use chain_storage::WalletChainStore;
use common::{HashType, transaction::NSSATransaction};
use config::WalletConfig;
@ -117,15 +118,24 @@ impl WalletCore {
config_path: PathBuf,
storage_path: PathBuf,
config_overrides: Option<WalletConfigOverrides>,
password: String,
) -> Result<Self> {
Self::new(
password: &str,
) -> Result<(Self, Mnemonic)> {
let mut mnemonic_out = None;
let wallet = Self::new(
config_path,
storage_path,
config_overrides,
|config| WalletChainStore::new_storage(config, password),
|config| {
let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?;
mnemonic_out = Some(mnemonic);
Ok(storage)
},
0,
)
)?;
Ok((
wallet,
mnemonic_out.expect("mnemonic should be set after new_storage"),
))
}
fn new(
@ -191,9 +201,13 @@ impl WalletCore {
&self.storage
}
/// Reset storage.
pub fn reset_storage(&mut self, password: String) -> Result<()> {
self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?;
/// Restore storage from an existing mnemonic phrase.
pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> {
self.storage = WalletChainStore::restore_storage(
self.storage.wallet_config.clone(),
mnemonic,
password,
)?;
Ok(())
}

View File

@ -46,13 +46,21 @@ async fn main() -> Result<()> {
println!("Persistent storage not found, need to execute setup");
let password = read_password_from_stdin()?;
let wallet = WalletCore::new_init_storage(
let (wallet, mnemonic) = WalletCore::new_init_storage(
config_path,
storage_path,
Some(config_overrides),
password,
&password,
)?;
println!();
println!("IMPORTANT: Write down your recovery phrase and store it securely.");
println!("This is the only way to recover your wallet if you lose access.");
println!();
println!("Recovery phrase:");
println!(" {mnemonic}");
println!();
wallet.store_persistent_data().await?;
wallet
};

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);
@ -100,7 +121,7 @@ impl Amm<'_> {
.await?)
}
pub async fn send_swap(
pub async fn send_swap_exact_input(
&self,
user_holding_a: AccountId,
user_holding_b: AccountId,
@ -108,7 +129,7 @@ impl Amm<'_> {
min_amount_out: u128,
token_definition_id_in: AccountId,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::Swap {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in,
@ -147,34 +168,105 @@ impl Amm<'_> {
user_holding_b,
];
let account_id_auth;
let account_id_auth = if definition_token_a_id == token_definition_id_in {
user_holding_a
} else if definition_token_b_id == token_definition_id_in {
user_holding_b
} else {
return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in,
));
};
// Checking, which account are associated with TokenDefinition
let token_holder_acc_a = self
let nonces = self
.0
.get_accounts_nonces(vec![account_id_auth])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let signing_key = self
.0
.storage
.user_data
.get_pub_account_signing_key(account_id_auth)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_swap_exact_output(
&self,
user_holding_a: AccountId,
user_holding_b: AccountId,
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
};
let program = Program::amm();
let amm_program_id = Program::amm().id();
let user_a_acc = self
.0
.get_account_public(user_holding_a)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let token_holder_acc_b = self
let user_b_acc = self
.0
.get_account_public(user_holding_b)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let token_holder_a = TokenHolding::try_from(&token_holder_acc_a.data)
.map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))?;
let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data)
.map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))?;
let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data)
.map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))?
.definition_id();
let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data)
.map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))?
.definition_id();
if token_holder_a.definition_id() == token_definition_id_in {
account_id_auth = user_holding_a;
} else if token_holder_b.definition_id() == token_definition_id_in {
account_id_auth = user_holding_b;
let amm_pool =
compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id);
let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id);
let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id);
let account_ids = vec![
amm_pool,
vault_holding_a,
vault_holding_b,
user_holding_a,
user_holding_b,
];
let account_id_auth = if definition_token_a_id == token_definition_id_in {
user_holding_a
} else if definition_token_b_id == token_definition_id_in {
user_holding_b
} else {
return Err(ExecutionFailureKind::AccountDataError(
token_definition_id_in,
));
}
};
let nonces = self
.0

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