mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-12 06:03:08 +00:00
Merge branch 'main' into marvin/private_transfer_simplified
This commit is contained in:
commit
de6a9f6c59
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@ -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
32
Cargo.lock
generated
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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![],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -8,8 +8,6 @@ license = { workspace = true }
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
secp256k1 = "0.31.1"
|
||||
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
common.workspace = true
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
@ -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>()
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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),
|
||||
],
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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:?}"
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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)
|
||||
};
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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),
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)?,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user