Merge remote-tracking branch 'upstream/main' into feat/sequencer-healthcheck

This commit is contained in:
ygd58 2026-04-24 22:38:01 +02:00
commit 48e5b10bf7
No known key found for this signature in database
GPG Key ID: 82B49AE8D5B28600
210 changed files with 13972 additions and 4075 deletions

View File

@ -13,6 +13,7 @@ ignore = [
{ id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" }, { id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" },
{ id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." }, { id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." },
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" }, { id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" },
{ id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
] ]
yanked = "deny" yanked = "deny"
unused-ignored-advisory = "deny" unused-ignored-advisory = "deny"

View File

@ -11,6 +11,10 @@ on:
- "**.md" - "**.md"
- "!.github/workflows/*.yml" - "!.github/workflows/*.yml"
permissions:
contents: read
pull-requests: read
name: General name: General
jobs: jobs:
@ -19,7 +23,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install nightly toolchain for rustfmt - name: Install nightly toolchain for rustfmt
run: rustup install nightly --profile minimal --component rustfmt run: rustup install nightly --profile minimal --component rustfmt
@ -32,7 +36,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install taplo-cli - name: Install taplo-cli
run: cargo install --locked taplo-cli run: cargo install --locked taplo-cli
@ -45,7 +49,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install active toolchain - name: Install active toolchain
run: rustup install run: rustup install
@ -61,7 +65,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install cargo-deny - name: Install cargo-deny
run: cargo install --locked cargo-deny run: cargo install --locked cargo-deny
@ -77,7 +81,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -106,7 +110,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -126,7 +130,7 @@ jobs:
env: env:
RISC0_DEV_MODE: "1" RISC0_DEV_MODE: "1"
RUST_LOG: "info" RUST_LOG: "info"
run: cargo nextest run --workspace --exclude integration_tests run: cargo nextest run --workspace --exclude integration_tests --all-features
integration-tests: integration-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -134,7 +138,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -162,7 +166,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -182,7 +186,7 @@ jobs:
env: env:
RISC0_DEV_MODE: "1" RISC0_DEV_MODE: "1"
RUST_LOG: "info" RUST_LOG: "info"
run: cargo nextest run -p integration_tests indexer -- --skip tps_test run: cargo nextest run -p integration_tests indexer -- --skip tps_test
valid-proof-test: valid-proof-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -190,7 +194,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -216,7 +220,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-risc0 - uses: ./.github/actions/install-risc0

View File

@ -50,7 +50,7 @@ jobs:
type=ref,event=pr type=ref,event=pr
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}- type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image - name: Build and push Docker image

595
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,11 @@ members = [
"nssa/core", "nssa/core",
"programs/amm/core", "programs/amm/core",
"programs/amm", "programs/amm",
"programs/clock/core",
"programs/token/core", "programs/token/core",
"programs/token", "programs/token",
"programs/associated_token_account/core",
"programs/associated_token_account",
"sequencer/core", "sequencer/core",
"sequencer/service", "sequencer/service",
"sequencer/service/protocol", "sequencer/service/protocol",
@ -34,6 +37,7 @@ members = [
"examples/program_deployment/methods", "examples/program_deployment/methods",
"examples/program_deployment/methods/guest", "examples/program_deployment/methods/guest",
"bedrock_client", "bedrock_client",
"testnet_initial_state",
] ]
[workspace.dependencies] [workspace.dependencies]
@ -53,12 +57,16 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
indexer_service_rpc = { path = "indexer/service/rpc" } indexer_service_rpc = { path = "indexer/service/rpc" }
wallet = { path = "wallet" } wallet = { path = "wallet" }
wallet-ffi = { path = "wallet-ffi", default-features = false } wallet-ffi = { path = "wallet-ffi", default-features = false }
clock_core = { path = "programs/clock/core" }
token_core = { path = "programs/token/core" } token_core = { path = "programs/token/core" }
token_program = { path = "programs/token" } token_program = { path = "programs/token" }
amm_core = { path = "programs/amm/core" } amm_core = { path = "programs/amm/core" }
amm_program = { path = "programs/amm" } amm_program = { path = "programs/amm" }
ata_core = { path = "programs/associated_token_account/core" }
ata_program = { path = "programs/associated_token_account" }
test_program_methods = { path = "test_program_methods" } test_program_methods = { path = "test_program_methods" }
bedrock_client = { path = "bedrock_client" } bedrock_client = { path = "bedrock_client" }
testnet_initial_state = { path = "testnet_initial_state" }
tokio = { version = "1.50", features = [ tokio = { version = "1.50", features = [
"net", "net",
@ -112,11 +120,11 @@ tokio-retry = "0.3.0"
schemars = "1.2" schemars = "1.2"
async-stream = "0.3.6" async-stream = "0.3.6"
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
rocksdb = { version = "0.24.0", default-features = false, features = [ rocksdb = { version = "0.24.0", default-features = false, features = [
"snappy", "snappy",
@ -144,6 +152,14 @@ opt-level = 'z'
lto = true lto = true
codegen-units = 1 codegen-units = 1
# Keep backtraces but drop full DWARF type info to avoid LLD OOM/SIGBUS when
# linking large integration-test binaries on resource-constrained CI runners.
[profile.dev]
debug = "line-tables-only"
[profile.test]
debug = "line-tables-only"
[workspace.lints.rust] [workspace.lints.rust]
warnings = "deny" warnings = "deny"

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.

View File

@ -1,12 +0,0 @@
port: 4400
n_hosts: 4
timeout: 10
# Tracing
tracing_settings:
logger: Stdout
tracing: None
filter: None
metrics: None
console: None
level: DEBUG

View File

@ -0,0 +1,82 @@
blend:
common:
num_blend_layers: 3
minimum_network_size: 30
protocol_name: /blend/integration-tests
data_replication_factor: 0
core:
scheduler:
cover:
message_frequency_per_round: 1.0
intervals_for_safety_buffer: 100
delayer:
maximum_release_delay_in_rounds: 3
minimum_messages_coefficient: 1
normalization_constant: 1.03
activity_threshold_sensitivity: 1
network:
kademlia_protocol_name: /integration/logos-blockchain/kad/1.0.0
identify_protocol_name: /integration/logos-blockchain/identify/1.0.0
chain_sync_protocol_name: /integration/logos-blockchain/chainsync/1.0.0
cryptarchia:
epoch_config:
epoch_stake_distribution_stabilization: 3
epoch_period_nonce_buffer: 3
epoch_period_nonce_stabilization: 4
security_param: 10
slot_activation_coeff:
numerator: 1
denominator: 2
learning_rate: 0.1
sdp_config:
service_params:
BN:
lock_period: 10
inactivity_period: 1
retention_period: 1
timestamp: 0
min_stake:
threshold: 1
timestamp: 0
gossipsub_protocol: /integration/logos-blockchain/cryptarchia/proto/1.0.0
genesis_state:
mantle_tx:
ops:
- opcode: 0
payload:
inputs: [ ]
outputs:
- value: 1
pk: d204000000000000000000000000000000000000000000000000000000000000
- value: 100
pk: 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26
- opcode: 17
payload:
channel_id: "0000000000000000000000000000000000000000000000000000000000000000"
inscription: [ 103, 101, 110, 101, 115, 105, 115 ] # "genesis" in bytes
parent: "0000000000000000000000000000000000000000000000000000000000000000"
signer: "0000000000000000000000000000000000000000000000000000000000000000"
execution_gas_price: 0
storage_gas_price: 0
ops_proofs:
- !ZkSig
pi_a: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
pi_b: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
pi_c: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
- NoProof
time:
slot_duration: '1.0'
chain_start_time: PLACEHOLDER_CHAIN_START_TIME
mempool:
pubsub_topic: mantle_e2e_tests

View File

@ -1,46 +1,12 @@
services: services:
cfgsync:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
volumes:
- ./scripts:/etc/logos-blockchain/scripts
- ./cfgsync.yaml:/etc/logos-blockchain/cfgsync.yaml:z
entrypoint: /etc/logos-blockchain/scripts/run_cfgsync.sh
logos-blockchain-node-0: logos-blockchain-node-0:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 image: ghcr.io/logos-blockchain/logos-blockchain@sha256:c5243681b353278cabb562a176f0a5cfbefc2056f18cebc47fe0e3720c29fb12
ports: ports:
- "${PORT:-8080}:18080/tcp" - "${PORT:-8080}:18080/tcp"
volumes: volumes:
- ./scripts:/etc/logos-blockchain/scripts - ./scripts:/etc/logos-blockchain/scripts
- ./kzgrs_test_params:/kzgrs_test_params:z - ./kzgrs_test_params:/kzgrs_test_params:z
depends_on: - ./node-config.yaml:/etc/logos-blockchain/node-config.yaml:z
- cfgsync - ./deployment-settings.yaml:/etc/logos-blockchain/deployment-settings.yaml:z
entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh
logos-blockchain-node-1:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
volumes:
- ./scripts:/etc/logos-blockchain/scripts
- ./kzgrs_test_params:/kzgrs_test_params:z
depends_on:
- cfgsync
entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh
logos-blockchain-node-2:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
volumes:
- ./scripts:/etc/logos-blockchain/scripts
- ./kzgrs_test_params:/kzgrs_test_params:z
depends_on:
- cfgsync
entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh
logos-blockchain-node-3:
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
volumes:
- ./scripts:/etc/logos-blockchain/scripts
- ./kzgrs_test_params:/kzgrs_test_params:z
depends_on:
- cfgsync
entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh

54
bedrock/node-config.yaml Normal file
View File

@ -0,0 +1,54 @@
blend:
non_ephemeral_signing_key_id: 86c8519f00178e9eb1fe5f4247e4bed77d4c9f6da2fb10e8a1fdd7ba6bc79fa0
core:
zk:
secret_key_kms_id: 64249c75c2cb813578b75d05b215fc95f67cea5862fff047228183f22e63460e
cryptarchia:
service:
bootstrap:
prolonged_bootstrap_period: '1.000000000'
network:
network:
max_connected_peers_to_try_download: 16
max_discovered_peers_to_try_download: 16
leader:
wallet:
funding_pk: "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"
sdp:
wallet:
funding_pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717
kms:
backend:
keys:
2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26: !Zk 6c645cd4636d9c4c36a37a9aeabcaa3300000000000000000000000000000000
64249c75c2cb813578b75d05b215fc95f67cea5862fff047228183f22e63460e: !Zk 83c851cf4436e8d2fdac33d56d2b235f66431be97e2a20bf241d431713dc720f
ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717: !Zk 7364705cd4636d9c4c36a37a9aeabcaa00000000000000000000000000000000
86c8519f00178e9eb1fe5f4247e4bed77d4c9f6da2fb10e8a1fdd7ba6bc79fa0: !Ed25519 5cd4636d9c4c36a37a9aeabcaa332c3ec796226af0af48a0b2e70167205af749
wallet:
known_keys:
2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26: "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"
ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717: "ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717"
voucher_master_key_id: 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26
api:
backend:
listen_address: 0.0.0.0:18080
cors_origins: []
timeout: 30
max_body_size: 10485760
max_concurrent_requests: 500
tracing:
logger:
stdout: true
stderr: false
file:
directory: "./state/logs"
otlp: null
loki: null
gelf: null
tracing: None
filter: None
metrics: None
console: None
level: "INFO"

View File

@ -2,12 +2,19 @@
set -e set -e
export CFG_FILE_PATH="/config.yaml" \ export POL_PROOF_DEV_MODE=true
CFG_SERVER_ADDR="http://cfgsync:4400" \
CFG_HOST_IP=$(hostname -i) \
CFG_HOST_IDENTIFIER="validator-$(hostname -i)" \
LOG_LEVEL="INFO" \
POL_PROOF_DEV_MODE=true
/usr/bin/logos-blockchain-cfgsync-client && \ # Use static configs mounted from host. Both node-config.yaml and
exec /usr/bin/logos-blockchain-node /config.yaml # deployment-settings.yaml have matching validator keys so the node
# can produce blocks as a single-validator network.
# Copy deployment-settings to a writable path because sed -i can't
# rename on a bind-mounted file.
cp /etc/logos-blockchain/deployment-settings.yaml /deployment-settings.yaml
# Set chain_start_time to "now" so the chain starts immediately.
sed -i "s/PLACEHOLDER_CHAIN_START_TIME/$(date -u '+%Y-%m-%d %H:%M:%S.000000 +00:00:00')/" \
/deployment-settings.yaml
exec /usr/bin/logos-blockchain-node \
/etc/logos-blockchain/node-config.yaml \
--deployment /deployment-settings.yaml

View File

@ -46,7 +46,7 @@ impl BedrockClient {
info!("Creating Bedrock client with node URL {node_url}"); info!("Creating Bedrock client with node URL {node_url}");
let client = Client::builder() let client = Client::builder()
//Add more fields if needed //Add more fields if needed
.timeout(std::time::Duration::from_secs(60)) .timeout(std::time::Duration::from_mins(1))
.build() .build()
.context("Failed to build HTTP client")?; .context("Failed to build HTTP client")?;

View File

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

View File

@ -1,14 +1,12 @@
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use nssa::AccountId; use nssa_core::BlockId;
pub use nssa_core::Timestamp;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
use crate::{HashType, transaction::NSSATransaction}; use crate::{HashType, transaction::NSSATransaction};
pub type MantleMsgId = [u8; 32]; pub type MantleMsgId = [u8; 32];
pub type BlockHash = HashType; pub type BlockHash = HashType;
pub type BlockId = u64;
pub type TimeStamp = u64;
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct BlockMeta { pub struct BlockMeta {
@ -36,7 +34,7 @@ pub struct BlockHeader {
pub block_id: BlockId, pub block_id: BlockId,
pub prev_block_hash: BlockHash, pub prev_block_hash: BlockHash,
pub hash: BlockHash, pub hash: BlockHash,
pub timestamp: TimeStamp, pub timestamp: Timestamp,
pub signature: nssa::Signature, pub signature: nssa::Signature,
} }
@ -76,7 +74,7 @@ impl<'de> Deserialize<'de> for Block {
pub struct HashableBlockData { pub struct HashableBlockData {
pub block_id: BlockId, pub block_id: BlockId,
pub prev_block_hash: BlockHash, pub prev_block_hash: BlockHash,
pub timestamp: TimeStamp, pub timestamp: Timestamp,
pub transactions: Vec<NSSATransaction>, pub transactions: Vec<NSSATransaction>,
} }
@ -123,20 +121,6 @@ impl From<Block> for HashableBlockData {
} }
} }
/// Helper struct for account (de-)serialization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountInitialData {
pub account_id: AccountId,
pub balance: u128,
}
/// Helper struct to (de-)serialize initial commitments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitmentsInitialData {
pub npk: nssa_core::NullifierPublicKey,
pub account: nssa_core::account::Account,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{HashType, block::HashableBlockData, test_utils}; use crate::{HashType, block::HashableBlockData, test_utils};

View File

@ -1,6 +1,7 @@
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use log::warn; use log::warn;
use nssa::{AccountId, V03State}; use nssa::{AccountId, V03State, ValidatedStateDiff};
use nssa_core::{BlockId, Timestamp};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::HashType; use crate::HashType;
@ -65,17 +66,53 @@ impl NSSATransaction {
} }
} }
/// Validates the transaction against the current state and returns the resulting diff
/// without applying it. Rejects transactions that modify clock system accounts.
pub fn validate_on_state(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<ValidatedStateDiff, nssa::error::NssaError> {
let diff = match self {
Self::Public(tx) => {
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
}
Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction(
tx, state, block_id, timestamp,
),
Self::ProgramDeployment(tx) => {
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
}
}?;
let public_diff = diff.public_diff();
let touches_clock = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().any(|id| {
public_diff
.get(id)
.is_some_and(|post| *post != state.get_account_by_id(*id))
});
if touches_clock {
return Err(nssa::error::NssaError::InvalidInput(
"Transaction modifies system clock accounts".into(),
));
}
Ok(diff)
}
/// Validates the transaction against the current state, rejects modifications to clock
/// system accounts, and applies the resulting diff to the state.
pub fn execute_check_on_state( pub fn execute_check_on_state(
self, self,
state: &mut V03State, state: &mut V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, nssa::error::NssaError> { ) -> Result<Self, nssa::error::NssaError> {
match &self { let diff = self
Self::Public(tx) => state.transition_from_public_transaction(tx), .validate_on_state(state, block_id, timestamp)
Self::PrivacyPreserving(tx) => state.transition_from_privacy_preserving_transaction(tx), .inspect_err(|err| warn!("Error at transition {err:#?}"))?;
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), state.apply_state_diff(diff);
}
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
Ok(self) Ok(self)
} }
} }
@ -116,3 +153,20 @@ pub enum TransactionMalformationError {
#[error("Transaction size {size} exceeds maximum allowed size of {max} bytes")] #[error("Transaction size {size} exceeds maximum allowed size of {max} bytes")]
TransactionTooLarge { size: usize, max: usize }, TransactionTooLarge { size: usize, max: usize },
} }
/// Returns the canonical Clock Program invocation transaction for the given block timestamp.
/// Every valid block must end with exactly one occurrence of this transaction.
#[must_use]
pub fn clock_invocation(timestamp: clock_core::Instruction) -> nssa::PublicTransaction {
let message = nssa::public_transaction::Message::try_new(
nssa::program::Program::clock().id(),
clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
vec![],
timestamp,
)
.expect("Clock invocation message should always be constructable");
nssa::PublicTransaction::new(
message,
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
)
}

View File

@ -22,6 +22,20 @@ _wallet_complete_account_id() {
fi fi
} }
# Helper function to complete account labels
_wallet_complete_account_label() {
local cur="$1"
local labels
if command -v wallet &>/dev/null; then
labels=$(wallet account list 2>/dev/null | grep -o '\[.*\]' | sed 's/^\[//;s/\]$//')
fi
if [[ -n "$labels" ]]; then
COMPREPLY=($(compgen -W "$labels" -- "$cur"))
fi
}
_wallet() { _wallet() {
local cur prev words cword local cur prev words cword
_init_completion 2>/dev/null || { _init_completion 2>/dev/null || {
@ -91,20 +105,32 @@ _wallet() {
--account-id) --account-id)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--account-label)
_wallet_complete_account_label "$cur"
;;
*) *)
COMPREPLY=($(compgen -W "--account-id" -- "$cur")) COMPREPLY=($(compgen -W "--account-id --account-label" -- "$cur"))
;; ;;
esac esac
;; ;;
send) send)
case "$prev" in case "$prev" in
--from | --to) --from)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--from-label)
_wallet_complete_account_label "$cur"
;;
--to)
_wallet_complete_account_id "$cur"
;;
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount) --to-npk | --to-vpk | --amount)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
;; ;;
esac esac
;; ;;
@ -147,8 +173,11 @@ _wallet() {
-a | --account-id) -a | --account-id)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--account-label)
_wallet_complete_account_label "$cur"
;;
*) *)
COMPREPLY=($(compgen -W "-r --raw -k --keys -a --account-id" -- "$cur")) COMPREPLY=($(compgen -W "-r --raw -k --keys -a --account-id --account-label" -- "$cur"))
;; ;;
esac esac
;; ;;
@ -186,10 +215,13 @@ _wallet() {
-a | --account-id) -a | --account-id)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--account-label)
_wallet_complete_account_label "$cur"
;;
-l | --label) -l | --label)
;; # no specific completion for label value ;; # no specific completion for label value
*) *)
COMPREPLY=($(compgen -W "-a --account-id -l --label" -- "$cur")) COMPREPLY=($(compgen -W "-a --account-id --account-label -l --label" -- "$cur"))
;; ;;
esac esac
;; ;;
@ -206,8 +238,11 @@ _wallet() {
--to) --to)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--to-label)
_wallet_complete_account_label "$cur"
;;
*) *)
COMPREPLY=($(compgen -W "--to" -- "$cur")) COMPREPLY=($(compgen -W "--to --to-label" -- "$cur"))
;; ;;
esac esac
;; ;;
@ -221,49 +256,85 @@ _wallet() {
;; ;;
new) new)
case "$prev" in case "$prev" in
--definition-account-id | --supply-account-id) --definition-account-id)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--definition-account-label)
_wallet_complete_account_label "$cur"
;;
--supply-account-id)
_wallet_complete_account_id "$cur"
;;
--supply-account-label)
_wallet_complete_account_label "$cur"
;;
-n | --name | -t | --total-supply) -n | --name | -t | --total-supply)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--definition-account-id --supply-account-id -n --name -t --total-supply" -- "$cur")) COMPREPLY=($(compgen -W "--definition-account-id --definition-account-label --supply-account-id --supply-account-label -n --name -t --total-supply" -- "$cur"))
;; ;;
esac esac
;; ;;
send) send)
case "$prev" in case "$prev" in
--from | --to) --from)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--from-label)
_wallet_complete_account_label "$cur"
;;
--to)
_wallet_complete_account_id "$cur"
;;
--to-label)
_wallet_complete_account_label "$cur"
;;
--to-npk | --to-vpk | --amount) --to-npk | --to-vpk | --amount)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur"))
;; ;;
esac esac
;; ;;
burn) burn)
case "$prev" in case "$prev" in
--definition | --holder) --definition)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--definition-label)
_wallet_complete_account_label "$cur"
;;
--holder)
_wallet_complete_account_id "$cur"
;;
--holder-label)
_wallet_complete_account_label "$cur"
;;
--amount) --amount)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--definition --holder --amount" -- "$cur")) COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --amount" -- "$cur"))
;; ;;
esac esac
;; ;;
mint) mint)
case "$prev" in case "$prev" in
--definition | --holder) --definition)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--definition-label)
_wallet_complete_account_label "$cur"
;;
--holder)
_wallet_complete_account_id "$cur"
;;
--holder-label)
_wallet_complete_account_label "$cur"
;;
--holder-npk | --holder-vpk | --amount) --holder-npk | --holder-vpk | --amount)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--definition --holder --holder-npk --holder-vpk --amount" -- "$cur")) COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --amount" -- "$cur"))
;; ;;
esac esac
;; ;;
@ -277,49 +348,103 @@ _wallet() {
;; ;;
new) new)
case "$prev" in case "$prev" in
--user-holding-a | --user-holding-b | --user-holding-lp) --user-holding-a)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--user-holding-a-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-b)
_wallet_complete_account_id "$cur"
;;
--user-holding-b-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-lp)
_wallet_complete_account_id "$cur"
;;
--user-holding-lp-label)
_wallet_complete_account_label "$cur"
;;
--balance-a | --balance-b) --balance-a | --balance-b)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-a --balance-b" -- "$cur")) COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --balance-a --balance-b" -- "$cur"))
;; ;;
esac esac
;; ;;
swap) swap)
case "$prev" in case "$prev" in
--user-holding-a | --user-holding-b) --user-holding-a)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--user-holding-a-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-b)
_wallet_complete_account_id "$cur"
;;
--user-holding-b-label)
_wallet_complete_account_label "$cur"
;;
--amount-in | --min-amount-out | --token-definition) --amount-in | --min-amount-out | --token-definition)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --amount-in --min-amount-out --token-definition" -- "$cur")) COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --amount-in --min-amount-out --token-definition" -- "$cur"))
;; ;;
esac esac
;; ;;
add-liquidity) add-liquidity)
case "$prev" in case "$prev" in
--user-holding-a | --user-holding-b | --user-holding-lp) --user-holding-a)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--user-holding-a-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-b)
_wallet_complete_account_id "$cur"
;;
--user-holding-b-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-lp)
_wallet_complete_account_id "$cur"
;;
--user-holding-lp-label)
_wallet_complete_account_label "$cur"
;;
--max-amount-a | --max-amount-b | --min-amount-lp) --max-amount-a | --max-amount-b | --min-amount-lp)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --max-amount-a --max-amount-b --min-amount-lp" -- "$cur")) COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --max-amount-a --max-amount-b --min-amount-lp" -- "$cur"))
;; ;;
esac esac
;; ;;
remove-liquidity) remove-liquidity)
case "$prev" in case "$prev" in
--user-holding-a | --user-holding-b | --user-holding-lp) --user-holding-a)
_wallet_complete_account_id "$cur" _wallet_complete_account_id "$cur"
;; ;;
--user-holding-a-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-b)
_wallet_complete_account_id "$cur"
;;
--user-holding-b-label)
_wallet_complete_account_label "$cur"
;;
--user-holding-lp)
_wallet_complete_account_id "$cur"
;;
--user-holding-lp-label)
_wallet_complete_account_label "$cur"
;;
--balance-lp | --min-amount-a | --min-amount-b) --balance-lp | --min-amount-a | --min-amount-b)
;; # no specific completion ;; # no specific completion
*) *)
COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-lp --min-amount-a --min-amount-b" -- "$cur")) COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --balance-lp --min-amount-a --min-amount-b" -- "$cur"))
;; ;;
esac esac
;; ;;

View File

@ -90,12 +90,15 @@ _wallet_auth_transfer() {
case $line[1] in case $line[1] in
init) init)
_arguments \ _arguments \
'--account-id[Account ID to initialize]:account_id:_wallet_account_ids' '--account-id[Account ID to initialize]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
;; ;;
send) send)
_arguments \ _arguments \
'--from[Source account ID]:from_account:_wallet_account_ids' \ '--from[Source account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \ '--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \ '--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \ '--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--amount[Amount of native tokens to send]:amount:' '--amount[Amount of native tokens to send]:amount:'
@ -165,7 +168,8 @@ _wallet_account() {
_arguments \ _arguments \
'(-r --raw)'{-r,--raw}'[Get raw account data]' \ '(-r --raw)'{-r,--raw}'[Get raw account data]' \
'(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/vpk for private accounts)]' \ '(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/vpk for private accounts)]' \
'(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' '(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels'
;; ;;
list|ls) list|ls)
_arguments \ _arguments \
@ -189,6 +193,7 @@ _wallet_account() {
label) label)
_arguments \ _arguments \
'(-a --account-id)'{-a,--account-id}'[Account ID to label]:account_id:_wallet_account_ids' \ '(-a --account-id)'{-a,--account-id}'[Account ID to label]:account_id:_wallet_account_ids' \
'--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' \
'(-l --label)'{-l,--label}'[The label to assign to the account]:label:' '(-l --label)'{-l,--label}'[The label to assign to the account]:label:'
;; ;;
esac esac
@ -216,7 +221,8 @@ _wallet_pinata() {
case $line[1] in case $line[1] in
claim) claim)
_arguments \ _arguments \
'--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' '--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels'
;; ;;
esac esac
;; ;;
@ -249,12 +255,16 @@ _wallet_token() {
'--name[Token name]:name:' \ '--name[Token name]:name:' \
'--total-supply[Total supply of tokens to mint]:total_supply:' \ '--total-supply[Total supply of tokens to mint]:total_supply:' \
'--definition-account-id[Account ID for token definition]:definition_account:_wallet_account_ids' \ '--definition-account-id[Account ID for token definition]:definition_account:_wallet_account_ids' \
'--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' '--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:_wallet_account_labels' \
'--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' \
'--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:_wallet_account_labels'
;; ;;
send) send)
_arguments \ _arguments \
'--from[Source holding account ID]:from_account:_wallet_account_ids' \ '--from[Source holding account ID]:from_account:_wallet_account_ids' \
'--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \
'--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \ '--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \
'--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \ '--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \ '--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
'--amount[Amount of tokens to send]:amount:' '--amount[Amount of tokens to send]:amount:'
@ -262,13 +272,17 @@ _wallet_token() {
burn) burn)
_arguments \ _arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \ '--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--holder[Holder account ID]:holder_account:_wallet_account_ids' \ '--holder[Holder account ID]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--amount[Amount of tokens to burn]:amount:' '--amount[Amount of tokens to burn]:amount:'
;; ;;
mint) mint)
_arguments \ _arguments \
'--definition[Definition account ID]:definition_account:_wallet_account_ids' \ '--definition[Definition account ID]:definition_account:_wallet_account_ids' \
'--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \
'--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \ '--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \
'--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
'--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \ '--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \
'--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \ '--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \
'--amount[Amount of tokens to mint]:amount:' '--amount[Amount of tokens to mint]:amount:'
@ -302,15 +316,20 @@ _wallet_amm() {
new) new)
_arguments \ _arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--balance-a[Amount of token A to deposit]:balance_a:' \ '--balance-a[Amount of token A to deposit]:balance_a:' \
'--balance-b[Amount of token B to deposit]:balance_b:' '--balance-b[Amount of token B to deposit]:balance_b:'
;; ;;
swap) swap)
_arguments \ _arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--amount-in[Amount of tokens to swap]:amount_in:' \ '--amount-in[Amount of tokens to swap]:amount_in:' \
'--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \ '--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \
'--token-definition[Definition ID of the token being provided]:token_def:' '--token-definition[Definition ID of the token being provided]:token_def:'
@ -318,8 +337,11 @@ _wallet_amm() {
add-liquidity) add-liquidity)
_arguments \ _arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--max-amount-a[Maximum amount of token A to deposit]:max_amount_a:' \ '--max-amount-a[Maximum amount of token A to deposit]:max_amount_a:' \
'--max-amount-b[Maximum amount of token B to deposit]:max_amount_b:' \ '--max-amount-b[Maximum amount of token B to deposit]:max_amount_b:' \
'--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:' '--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:'
@ -327,8 +349,11 @@ _wallet_amm() {
remove-liquidity) remove-liquidity)
_arguments \ _arguments \
'--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \
'--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \
'--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \
'--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
'--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \
'--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \
'--balance-lp[Amount of LP tokens to burn]:balance_lp:' \ '--balance-lp[Amount of LP tokens to burn]:balance_lp:' \
'--min-amount-a[Minimum token A to receive]:min_amount_a:' \ '--min-amount-a[Minimum token A to receive]:min_amount_a:' \
'--min-amount-b[Minimum token B to receive]:min_amount_b:' '--min-amount-b[Minimum token B to receive]:min_amount_b:'
@ -424,7 +449,7 @@ _wallet_help() {
_wallet_account_ids() { _wallet_account_ids() {
local -a accounts local -a accounts
local line local line
# Try to get accounts from wallet account list command # Try to get accounts from wallet account list command
# Filter to lines starting with /N (numbered accounts) and extract the account ID # Filter to lines starting with /N (numbered accounts) and extract the account ID
if command -v wallet &>/dev/null; then if command -v wallet &>/dev/null; then
@ -433,14 +458,35 @@ _wallet_account_ids() {
[[ -n "$line" ]] && accounts+=("${line%,}") [[ -n "$line" ]] && accounts+=("${line%,}")
done < <(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}') done < <(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}')
fi fi
# Provide type prefixes as fallback if command fails or returns nothing # Provide type prefixes as fallback if command fails or returns nothing
if (( ${#accounts} == 0 )); then if (( ${#accounts} == 0 )); then
compadd -S '' -- 'Public/' 'Private/' compadd -S '' -- 'Public/' 'Private/'
return return
fi fi
_multi_parts / accounts _multi_parts / accounts
} }
# Helper function to complete account labels
# Uses `wallet account list` to get available labels
_wallet_account_labels() {
local -a labels
local line
if command -v wallet &>/dev/null; then
while IFS= read -r line; do
local label
# Extract label from [...] at end of line
label="${line##*\[}"
label="${label%\]}"
[[ -n "$label" && "$label" != "$line" ]] && labels+=("$label")
done < <(wallet account list 2>/dev/null)
fi
if (( ${#labels} > 0 )); then
compadd -a labels
fi
}
_wallet "$@" _wallet "$@"

View File

@ -0,0 +1,369 @@
# Associated Token Accounts (ATAs)
This tutorial covers Associated Token Accounts (ATAs). An ATA lets you derive a unique token holding address from an owner account and a token definition — no need to create and track holding accounts manually. Given the same inputs, anyone can compute the same ATA address without a network call. By the end, you will have practiced:
1. Deriving ATA addresses locally.
2. Creating an ATA.
3. Sending tokens via ATAs.
4. Burning tokens from an ATA.
5. Listing ATAs across multiple token definitions.
6. Creating an ATA with a private owner.
7. Sending tokens from a private owner's ATA.
8. Burning tokens from a private owner's ATA.
> [!Important]
> This tutorial assumes you have completed the [wallet-setup](wallet-setup.md) and [custom-tokens](custom-tokens.md) tutorials. You need a running wallet with accounts and at least one token definition.
## Prerequisites
### Deploy the ATA program
Unlike the Token program (which is built-in), the ATA program must be deployed before you can use it. The pre-built binary is included in the repository:
```bash
wallet deploy-program artifacts/program_methods/associated_token_account.bin
```
> [!Note]
> Program deployment is idempotent — if the ATA program has already been deployed (e.g. by another user on the same network), the command is a no-op.
You can verify the deployment succeeded by running any `wallet ata` command. If the program is not deployed, commands that submit transactions will fail.
The CLI provides commands to work with the ATA program. Run `wallet ata` to see the options:
```bash
Commands:
address Derive and print the Associated Token Account address (local only, no network)
create Create (or idempotently no-op) the Associated Token Account
send Send tokens from owner's ATA to a recipient
burn Burn tokens from holder's ATA
list List all ATAs for a given owner across multiple token definitions
help Print this message or the help of the given subcommand(s)
```
## 1. How ATA addresses work
An ATA address is deterministically derived from two inputs:
1. The **owner** account ID.
2. The **token definition** account ID.
The derivation works as follows:
```
seed = SHA256(owner_id || definition_id)
ata_address = AccountId::for_public_pda(ata_program_id, seed)
```
Because the computation is pure, anyone who knows the owner and definition can reproduce the exact same ATA address — no network call required.
> [!Note]
> All ATA commands that submit transactions accept a privacy prefix on the owner/holder argument — `Public/` for public accounts and `Private/` for private accounts. Using `Private/` generates a zero-knowledge proof locally and submits only the proof to the sequencer, keeping the owner's identity off-chain.
## 2. Deriving an ATA address (`wallet ata address`)
The `address` subcommand computes the ATA address locally without submitting a transaction.
### a. Set up an owner and token definition
If you already have a public account and a token definition from the custom-tokens tutorial, you can reuse them. Otherwise, create them now:
```bash
wallet account new public
# Output:
Generated new account with account_id Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB
```
```bash
wallet account new public
# Output:
Generated new account with account_id Public/3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4
```
```bash
wallet token new \
--name MYTOKEN \
--total-supply 10000 \
--definition-account-id Public/3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
--supply-account-id Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB
```
### b. Derive the ATA address
```bash
wallet ata address \
--owner 5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4
# Output:
7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R
```
> [!Note]
> This is a pure computation — no transaction is submitted and no network connection is needed. The same inputs will always produce the same output.
## 3. Creating an ATA (`wallet ata create`)
Before an ATA can hold tokens it must be created on-chain. The `create` subcommand submits a transaction that initializes the ATA. If it already exists, the operation is a no-op.
### a. Create the ATA
```bash
wallet ata create \
--owner Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4
```
### b. Inspect the ATA
Use the ATA address derived in the previous section:
```bash
wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":0}
```
> [!Tip]
> Creation is idempotent — running the same command again is a no-op.
## 4. Sending tokens via ATA (`wallet ata send`)
The `send` subcommand transfers tokens from the owner's ATA to a recipient account.
### a. Fund the ATA
First, move tokens into the ATA from the supply account created earlier:
```bash
wallet token send \
--from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--to Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R \
--amount 5000
```
### b. Create a recipient account
```bash
wallet account new public
# Output:
Generated new account with account_id Public/9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi
```
### c. Send tokens from the ATA to the recipient
```bash
wallet ata send \
--from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
--to 9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi \
--amount 2000
```
### d. Verify balances
```bash
wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":3000}
```
```bash
wallet account get --account-id Public/9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi
# Output:
Holding account owned by token program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":2000}
```
## 5. Burning tokens from an ATA (`wallet ata burn`)
The `burn` subcommand destroys tokens held in the owner's ATA, reducing the token's total supply.
### a. Burn tokens
```bash
wallet ata burn \
--holder Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
--amount 500
```
### b. Verify the reduced balance
```bash
wallet account get --account-id Public/7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":2500}
```
## 6. Listing ATAs (`wallet ata list`)
The `list` subcommand queries ATAs for a given owner across one or more token definitions.
### a. Create a second token and ATA
Create a second token definition so there are multiple ATAs to list:
```bash
wallet account new public
# Output:
Generated new account with account_id Public/BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi
```
```bash
wallet account new public
# Output:
Generated new account with account_id Public/Ck8mVp4YhWn2rXjD6dFsQtA7bEoLf3gUZx1wDnR9eTs
```
```bash
wallet token new \
--name OTHERTOKEN \
--total-supply 5000 \
--definition-account-id Public/BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi \
--supply-account-id Public/Ck8mVp4YhWn2rXjD6dFsQtA7bEoLf3gUZx1wDnR9eTs
```
Create an ATA for the second token:
```bash
wallet ata create \
--owner Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi
```
### b. List ATAs for both token definitions
```bash
wallet ata list \
--owner 5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--token-definition \
3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi
# Output:
ATA 7a2Bf9cKLm3XpRtH1wDqZs8vYjN4eU6gAoFxW5kMnE2R (definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4): balance 2500
ATA 4nPxKd8YmW7rVsH2jDfQcA9bEoLf6gUZx3wTnR1eMs5 (definition BxR3Lm7YkWp9vNs2hD4qJcTfA8eUoZ6gKn1wXjM5rFi): balance 0
```
> [!Note]
> The `list` command derives each ATA address locally and fetches its on-chain state. If an ATA has not been created for a given definition, it prints "No ATA for definition ..." instead.
## 7. Private owner operations
All three ATA operations — `create`, `send`, and `burn` — support private owner accounts. Passing a `Private/` prefix on the owner argument switches the wallet into privacy-preserving mode:
1. The wallet builds the transaction locally.
2. The ATA program is executed inside the RISC0 ZK VM to generate a proof.
3. The proof, the updated ATA state (in plaintext), and an encrypted update for the owner's private account are submitted to the sequencer.
4. The sequencer verifies the proof, writes the ATA state change to the public chain, and records the owner's new commitment in the nullifier set.
The result is that the ATA account and its token balance are **fully public** — anyone can see them. What stays private is the link between the ATA and its owner: the proof demonstrates that someone with the correct private key authorized the operation, but reveals nothing about which account that was.
> [!Note]
> The ATA address is derived from `SHA256(owner_id || definition_id)`. Because SHA256 is one-way, the ATA address does not reveal the owner's identity. However, if the owner's account ID becomes known for any other reason, all of their ATAs across every token definition can be enumerated by anyone.
### a. Create a private account
```bash
wallet account new private
# Output:
Generated new account with account_id Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi
```
### b. Create the ATA for the private owner
Pass `Private/` on `--owner`. The token definition account has no privacy prefix — it is always a public account.
```bash
wallet ata create \
--owner Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4
```
> [!Note]
> Proof generation runs locally in the RISC0 ZK VM and can take up to a minute on first run.
### c. Verify the ATA was created
Derive the ATA address using the raw account ID (no privacy prefix):
```bash
wallet ata address \
--owner HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4
# Output:
2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1
```
```bash
wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":0}
```
### d. Fund the ATA
The ATA is a public account. Fund it with a direct token transfer from any public holding account:
```bash
wallet token send \
--from Public/5FkBei8HYoSUNqh9rWCrJDnSZE5FJfGiWmTvhgBx3qTB \
--to Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1 \
--amount 500
```
### e. Send tokens from the private owner's ATA
```bash
wallet ata send \
--from Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
--to 9Ht4Kv8pYmW2rXjN6dFcQsA7bEoLf3gUZx1wDnR5eTi \
--amount 200
```
Verify the ATA balance decreased:
```bash
wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":300}
```
### f. Burn tokens from the private owner's ATA
```bash
wallet ata burn \
--holder Private/HkR7Lm2YnWp4vNs8hD3qJcTfA6eUoZ9gKn5wXjM1rFi \
--token-definition 3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4 \
--amount 100
```
Verify the balance and token supply:
```bash
wallet account get --account-id Public/2pQxNf7YkWm3rVsH8jDcQaA4bEoLf9gUZx6wTnR2eMs1
# Output:
Holding account owned by ata program
{"account_type":"Token holding","definition_id":"3YpK8RvVzWm6Q4h2nDAbxJfLmuRqkEkFP9C7UwTdGvE4","balance":200}
```

View File

@ -1,6 +1,4 @@
use nssa_core::program::{ use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
};
// Hello-world example program. // Hello-world example program.
// //
@ -21,6 +19,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: greeting, instruction: greeting,
}, },
@ -45,16 +45,19 @@ fn main() {
// Wrap the post state account values inside a `AccountPostState` instance. // Wrap the post state account values inside a `AccountPostState` instance.
// This is used to forward the account claiming request if any // This is used to forward the account claiming request if any
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
// This produces a claim request
AccountPostState::new_claimed(post_account)
} else {
// This doesn't produce a claim request
AccountPostState::new(post_account)
};
// The output is a proposed state difference. It will only succeed if the pre states coincide // 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 // with the previous values of the accounts, and the transition to the post states conforms
// with the NSSA program rules. // with the NSSA program rules.
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.write();
} }

View File

@ -1,6 +1,4 @@
use nssa_core::program::{ use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
};
// Hello-world with authorization example program. // Hello-world with authorization example program.
// //
@ -21,6 +19,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: greeting, instruction: greeting,
}, },
@ -52,16 +52,19 @@ fn main() {
// Wrap the post state account values inside a `AccountPostState` instance. // Wrap the post state account values inside a `AccountPostState` instance.
// This is used to forward the account claiming request if any // This is used to forward the account claiming request if any
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID { let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
// This produces a claim request
AccountPostState::new_claimed(post_account)
} else {
// This doesn't produce a claim request
AccountPostState::new(post_account)
};
// The output is a proposed state difference. It will only succeed if the pre states coincide // 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 // with the previous values of the accounts, and the transition to the post states conforms
// with the NSSA program rules. // with the NSSA program rules.
write_nssa_outputs(instruction_data, vec![pre_state], vec![post_state]); // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.write();
} }

View File

@ -1,8 +1,6 @@
use nssa_core::{ use nssa_core::{
account::{Account, AccountWithMetadata, Data}, account::{AccountWithMetadata, Data},
program::{ program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs},
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
},
}; };
// Hello-world with write + move_data example program. // Hello-world with write + move_data example program.
@ -26,16 +24,6 @@ const MOVE_DATA_FUNCTION_ID: u8 = 1;
type Instruction = (u8, Vec<u8>); 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 { fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
// Construct the post state account values // Construct the post state account values
let post_account = { let post_account = {
@ -48,7 +36,7 @@ fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
this 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> { 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 from_post = {
let mut this = from_pre.account; let mut this = from_pre.account;
this.data = Data::default(); this.data = Data::default();
build_post_state(this) AccountPostState::new_claimed_if_default(this, Claim::Authorized)
}; };
let to_post = { let to_post = {
@ -68,7 +56,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<
this.data = bytes this.data = bytes
.try_into() .try_into()
.expect("Data should fit within the allowed limits"); .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] vec![from_post, to_post]
@ -78,6 +66,8 @@ fn main() {
// Read input accounts. // Read input accounts.
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (function_id, data), instruction: (function_id, data),
}, },
@ -95,5 +85,14 @@ fn main() {
_ => panic!("invalid params"), _ => panic!("invalid params"),
}; };
write_nssa_outputs(instruction_words, pre_states, post_states); // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states,
post_states,
)
.write();
} }

View File

@ -1,6 +1,5 @@
use nssa_core::program::{ use nssa_core::program::{
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs, AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
write_nssa_outputs_with_chained_call,
}; };
// Tail Call example program. // Tail Call example program.
@ -28,6 +27,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (), instruction: (),
}, },
@ -53,11 +54,16 @@ fn main() {
pda_seeds: vec![], pda_seeds: vec![],
}; };
// Write the outputs // Write the outputs.
write_nssa_outputs_with_chained_call( // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data, instruction_data,
vec![pre_state], vec![pre_state],
vec![post_state], vec![post_state],
vec![chained_call], )
); .with_chained_calls(vec![chained_call])
.write();
} }

View File

@ -1,6 +1,6 @@
use nssa_core::program::{ use nssa_core::program::{
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs, AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
write_nssa_outputs_with_chained_call, read_nssa_inputs,
}; };
// Tail Call with PDA example program. // Tail Call with PDA example program.
@ -33,6 +33,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (), instruction: (),
}, },
@ -65,11 +67,16 @@ fn main() {
pda_seeds: vec![PDA_SEED], pda_seeds: vec![PDA_SEED],
}; };
// Write the outputs // Write the outputs.
write_nssa_outputs_with_chained_call( // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data, instruction_data,
vec![pre_state], vec![pre_state],
vec![post_state], vec![post_state],
vec![chained_call], )
); .with_chained_calls(vec![chained_call])
.write();
} }

View File

@ -46,7 +46,7 @@ async fn main() {
let program = Program::new(bytecode).unwrap(); let program = Program::new(bytecode).unwrap();
// Compute the PDA to pass it as input account to the public execution // Compute the PDA to pass it as input account to the public execution
let pda = AccountId::from((&program.id(), &PDA_SEED)); let pda = AccountId::for_public_pda(&program.id(), &PDA_SEED);
let account_ids = vec![pda]; let account_ids = vec![pda];
let instruction_data = (); let instruction_data = ();
let nonces = vec![]; let nonces = vec![];

View File

@ -177,12 +177,13 @@ pub fn TransactionPage() -> impl IntoView {
encrypted_private_post_states, encrypted_private_post_states,
new_commitments, new_commitments,
new_nullifiers, new_nullifiers,
block_validity_window,
timestamp_validity_window,
} = message; } = message;
let WitnessSet { let WitnessSet {
signatures_and_public_keys: _, signatures_and_public_keys: _,
proof, proof,
} = witness_set; } = witness_set;
let proof_len = proof.map_or(0, |p| p.0.len()); let proof_len = proof.map_or(0, |p| p.0.len());
view! { view! {
<div class="transaction-details"> <div class="transaction-details">
@ -212,6 +213,14 @@ pub fn TransactionPage() -> impl IntoView {
<span class="info-label">"Proof Size:"</span> <span class="info-label">"Proof Size:"</span>
<span class="info-value">{format!("{proof_len} bytes")}</span> <span class="info-value">{format!("{proof_len} bytes")}</span>
</div> </div>
<div class="info-row">
<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> </div>
<h3>"Public Accounts"</h3> <h3>"Public Accounts"</h3>

View File

@ -13,6 +13,7 @@ bedrock_client.workspace = true
nssa.workspace = true nssa.workspace = true
nssa_core.workspace = true nssa_core.workspace = true
storage.workspace = true storage.workspace = true
testnet_initial_state.workspace = true
anyhow.workspace = true anyhow.workspace = true
log.workspace = true log.workspace = true

View File

@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc};
use anyhow::Result; use anyhow::Result;
use bedrock_client::HeaderId; use bedrock_client::HeaderId;
use common::{ use common::{
block::{BedrockStatus, Block, BlockId}, block::{BedrockStatus, Block},
transaction::NSSATransaction, transaction::{NSSATransaction, clock_invocation},
}; };
use nssa::{Account, AccountId, V03State}; use nssa::{Account, AccountId, V03State};
use nssa_core::BlockId;
use storage::indexer::RocksDBIO; use storage::indexer::RocksDBIO;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -121,12 +122,37 @@ impl IndexerStore {
{ {
let mut state_guard = self.current_state.write().await; let mut state_guard = self.current_state.write().await;
for transaction in &block.body.transactions { let (clock_tx, user_txs) = block
.body
.transactions
.split_last()
.ok_or_else(|| anyhow::anyhow!("Block has no transactions"))?;
anyhow::ensure!(
*clock_tx == NSSATransaction::Public(clock_invocation(block.header.timestamp)),
"Last transaction in block must be the clock invocation for the block timestamp"
);
for transaction in user_txs {
transaction transaction
.clone() .clone()
.transaction_stateless_check()? .transaction_stateless_check()?
.execute_check_on_state(&mut state_guard)?; .execute_check_on_state(
&mut state_guard,
block.header.block_id,
block.header.timestamp,
)?;
} }
// Apply the clock invocation directly (it is expected to modify clock accounts).
let NSSATransaction::Public(clock_public_tx) = clock_tx else {
anyhow::bail!("Clock invocation must be a public transaction");
};
state_guard.transition_from_public_transaction(
clock_public_tx,
block.header.block_id,
block.header.timestamp,
)?;
} }
// ToDo: Currently we are fetching only finalized blocks // ToDo: Currently we are fetching only finalized blocks
@ -172,7 +198,11 @@ mod tests {
let storage = IndexerStore::open_db_with_genesis( let storage = IndexerStore::open_db_with_genesis(
home.as_ref(), home.as_ref(),
&genesis_block(), &genesis_block(),
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), &nssa::V03State::new_with_genesis_accounts(
&[(acc1(), 10000), (acc2(), 20000)],
vec![],
0,
),
) )
.unwrap(); .unwrap();
@ -190,7 +220,11 @@ mod tests {
let storage = IndexerStore::open_db_with_genesis( let storage = IndexerStore::open_db_with_genesis(
home.as_ref(), home.as_ref(),
&genesis_block(), &genesis_block(),
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), &nssa::V03State::new_with_genesis_accounts(
&[(acc1(), 10000), (acc2(), 20000)],
vec![],
0,
),
) )
.unwrap(); .unwrap();
@ -208,11 +242,14 @@ mod tests {
10, 10,
&sign_key, &sign_key,
); );
let block_id = u64::try_from(i).unwrap();
let block_timestamp = block_id.saturating_mul(100);
let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp));
let next_block = common::test_utils::produce_dummy_block( let next_block = common::test_utils::produce_dummy_block(
u64::try_from(i).unwrap(), block_id,
Some(prev_hash), Some(prev_hash),
vec![tx], vec![tx, clock_tx],
); );
prev_hash = next_block.header.hash; prev_hash = next_block.header.hash;

View File

@ -7,13 +7,11 @@ use std::{
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
pub use bedrock_client::BackoffConfig; pub use bedrock_client::BackoffConfig;
use common::{ use common::config::BasicAuth;
block::{AccountInitialData, CommitmentsInitialData},
config::BasicAuth,
};
use humantime_serde; use humantime_serde;
pub use logos_blockchain_core::mantle::ops::channel::ChannelId; pub use logos_blockchain_core::mantle::ops::channel::ChannelId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData};
use url::Url; use url::Url;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -29,16 +27,16 @@ pub struct ClientConfig {
pub struct IndexerConfig { pub struct IndexerConfig {
/// Home dir of sequencer storage. /// Home dir of sequencer storage.
pub home: PathBuf, pub home: PathBuf,
/// List of initial accounts data.
pub initial_accounts: Vec<AccountInitialData>,
/// List of initial commitments.
pub initial_commitments: Vec<CommitmentsInitialData>,
/// Sequencers signing key. /// Sequencers signing key.
pub signing_key: [u8; 32], pub signing_key: [u8; 32],
#[serde(with = "humantime_serde")] #[serde(with = "humantime_serde")]
pub consensus_info_polling_interval: Duration, pub consensus_info_polling_interval: Duration,
pub bedrock_client_config: ClientConfig, pub bedrock_client_config: ClientConfig,
pub channel_id: ChannelId, pub channel_id: ChannelId,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_public_accounts: Option<Vec<PublicAccountPublicInitialData>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub initial_private_accounts: Option<Vec<PrivateAccountPublicInitialData>>,
} }
impl IndexerConfig { impl IndexerConfig {

View File

@ -2,14 +2,17 @@ use std::collections::VecDeque;
use anyhow::Result; use anyhow::Result;
use bedrock_client::{BedrockClient, HeaderId}; use bedrock_client::{BedrockClient, HeaderId};
use common::block::{Block, HashableBlockData}; use common::{
// ToDo: Remove after testnet HashType, PINATA_BASE58,
use common::{HashType, PINATA_BASE58}; block::{Block, HashableBlockData},
};
use log::{debug, error, info}; use log::{debug, error, info};
use logos_blockchain_core::mantle::{ use logos_blockchain_core::mantle::{
Op, SignedMantleTx, Op, SignedMantleTx,
ops::channel::{ChannelId, inscribe::InscriptionOp}, ops::channel::{ChannelId, inscribe::InscriptionOp},
}; };
use nssa::V03State;
use testnet_initial_state::initial_state_testnet;
use crate::{block_store::IndexerStore, config::IndexerConfig}; use crate::{block_store::IndexerStore, config::IndexerConfig};
@ -54,36 +57,52 @@ impl IndexerCore {
let channel_genesis_msg_id = [0; 32]; let channel_genesis_msg_id = [0; 32];
let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id); let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id);
// This is a troubling moment, because changes in key protocol can let initial_private_accounts: Option<Vec<(nssa_core::Commitment, nssa_core::Nullifier)>> =
// affect this. And indexer can not reliably ask this data from sequencer config.initial_private_accounts.as_ref().map(|accounts| {
// because indexer must be independent from it. accounts
// ToDo: move initial state generation into common and use the same method .iter()
// for indexer and sequencer. This way both services buit at same version .map(|init_comm_data| {
// could be in sync. let npk = &init_comm_data.npk;
let initial_commitments: Vec<nssa_core::Commitment> = config
.initial_commitments
.iter()
.map(|init_comm_data| {
let npk = &init_comm_data.npk;
let mut acc = init_comm_data.account.clone(); let mut acc = init_comm_data.account.clone();
acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); acc.program_owner =
nssa::program::Program::authenticated_transfer_program().id();
nssa_core::Commitment::new(npk, &acc) (
}) nssa_core::Commitment::new(npk, &acc),
.collect(); nssa_core::Nullifier::for_account_initialization(npk),
)
})
.collect()
});
let init_accs: Vec<(nssa::AccountId, u128)> = config let init_accs: Option<Vec<(nssa::AccountId, u128)>> = config
.initial_accounts .initial_public_accounts
.iter() .as_ref()
.map(|acc_data| (acc_data.account_id, acc_data.balance)) .map(|initial_accounts| {
.collect(); initial_accounts
.iter()
.map(|acc_data| (acc_data.account_id, acc_data.balance))
.collect()
});
let mut state = nssa::V03State::new_with_genesis_accounts(&init_accs, &initial_commitments); // If initial commitments or accounts are present in config, need to construct state from
// them
let state = if initial_private_accounts.is_some() || init_accs.is_some() {
let mut state = V03State::new_with_genesis_accounts(
&init_accs.unwrap_or_default(),
initial_private_accounts.unwrap_or_default(),
genesis_block.header.timestamp,
);
// ToDo: Remove after testnet // ToDo: Remove after testnet
state.add_pinata_program(PINATA_BASE58.parse().unwrap()); state.add_pinata_program(PINATA_BASE58.parse().unwrap());
state
} else {
initial_state_testnet()
};
let home = config.home.join("rocksdb"); let home = config.home.join("rocksdb");
@ -125,7 +144,22 @@ impl IndexerCore {
info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids);
for l2_block in l2_block_vec { for l2_block in l2_block_vec {
self.store.put_block(l2_block.clone(), l1_header).await?; // TODO: proper fix is to make the sequencer's genesis include a
// trailing `clock_invocation(0)` (and have the indexer's
// `open_db_with_genesis` not pre-apply state transitions) so the
// inscribed genesis can flow through `put_block` like any other
// block. For now we skip re-applying it.
//
// The channel-start (block_id == 1) is the sequencer's genesis
// inscription that we re-discover during initial search. The
// indexer already has its own locally-constructed genesis in
// the store from `open_db_with_genesis`, so re-applying the
// inscribed copy is both redundant and would fail the strict
// block validation in `put_block` (the inscribed genesis lacks
// the trailing clock invocation).
if l2_block.header.block_id != 1 {
self.store.put_block(l2_block.clone(), l1_header).await?;
}
yield Ok(l2_block); yield Ok(l2_block);
} }

View File

@ -7,7 +7,7 @@ use crate::{
CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId, CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId,
Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction,
Signature, Transaction, WitnessSet, Signature, Transaction, ValidityWindow, WitnessSet,
}; };
// ============================================================================ // ============================================================================
@ -287,6 +287,8 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
encrypted_private_post_states, encrypted_private_post_states,
new_commitments, new_commitments,
new_nullifiers, new_nullifiers,
block_validity_window,
timestamp_validity_window,
} = value; } = value;
Self { Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -301,12 +303,14 @@ impl From<nssa::privacy_preserving_transaction::message::Message> for PrivacyPre
.into_iter() .into_iter()
.map(|(n, d)| (n.into(), d.into())) .map(|(n, d)| (n.into(), d.into()))
.collect(), .collect(),
block_validity_window: block_validity_window.into(),
timestamp_validity_window: timestamp_validity_window.into(),
} }
} }
} }
impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction::message::Message { impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction::message::Message {
type Error = nssa_core::account::data::DataTooBigError; type Error = nssa::error::NssaError;
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> { fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
let PrivacyPreservingMessage { let PrivacyPreservingMessage {
@ -316,6 +320,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
encrypted_private_post_states, encrypted_private_post_states,
new_commitments, new_commitments,
new_nullifiers, new_nullifiers,
block_validity_window,
timestamp_validity_window,
} = value; } = value;
Ok(Self { Ok(Self {
public_account_ids: public_account_ids.into_iter().map(Into::into).collect(), public_account_ids: public_account_ids.into_iter().map(Into::into).collect(),
@ -326,7 +332,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
public_post_states: public_post_states public_post_states: public_post_states
.into_iter() .into_iter()
.map(TryInto::try_into) .map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()
.map_err(|e| nssa::error::NssaError::InvalidInput(format!("{e}")))?,
encrypted_private_post_states: encrypted_private_post_states encrypted_private_post_states: encrypted_private_post_states
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
@ -336,6 +343,12 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
.into_iter() .into_iter()
.map(|(n, d)| (n.into(), d.into())) .map(|(n, d)| (n.into(), d.into()))
.collect(), .collect(),
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}")))?,
}) })
} }
} }
@ -479,14 +492,7 @@ impl TryFrom<PrivacyPreservingTransaction> for nssa::PrivacyPreservingTransactio
witness_set, witness_set,
} = value; } = value;
Ok(Self::new( Ok(Self::new(message.try_into()?, witness_set.try_into()?))
message
.try_into()
.map_err(|err: nssa_core::account::data::DataTooBigError| {
nssa::error::NssaError::InvalidInput(err.to_string())
})?,
witness_set.try_into()?,
))
} }
} }
@ -687,3 +693,21 @@ impl From<HashType> for common::HashType {
Self(value.0) Self(value.0)
} }
} }
// ============================================================================
// ValidityWindow conversions
// ============================================================================
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<u64> {
type Error = nssa_core::program::InvalidWindow;
fn try_from(value: ValidityWindow) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

View File

@ -138,7 +138,7 @@ pub struct Account {
} }
pub type BlockId = u64; pub type BlockId = u64;
pub type TimeStamp = u64; pub type Timestamp = u64;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct Block { pub struct Block {
@ -153,7 +153,7 @@ pub struct BlockHeader {
pub block_id: BlockId, pub block_id: BlockId,
pub prev_block_hash: HashType, pub prev_block_hash: HashType,
pub hash: HashType, pub hash: HashType,
pub timestamp: TimeStamp, pub timestamp: Timestamp,
pub signature: Signature, pub signature: Signature,
} }
@ -235,6 +235,8 @@ pub struct PrivacyPreservingMessage {
pub encrypted_private_post_states: Vec<EncryptedAccountData>, pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>, pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub block_validity_window: ValidityWindow,
pub timestamp_validity_window: ValidityWindow,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
@ -300,6 +302,20 @@ pub struct Nullifier(
pub [u8; 32], pub [u8; 32],
); );
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct ValidityWindow(pub (Option<BlockId>, Option<BlockId>));
impl Display for ValidityWindow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
(Some(start), Some(end)) => write!(f, "[{start}, {end})"),
(Some(start), None) => write!(f, "[{start}, \u{221e})"),
(None, Some(end)) => write!(f, "(-\u{221e}, {end})"),
(None, None) => write!(f, "(-\u{221e}, \u{221e})"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct CommitmentSetDigest( pub struct CommitmentSetDigest(
#[serde(with = "base64::arr")] #[serde(with = "base64::arr")]

View File

@ -13,7 +13,7 @@ use indexer_service_protocol::{
CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId, CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId,
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature, ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
Transaction, WitnessSet, Transaction, ValidityWindow, WitnessSet,
}; };
use jsonrpsee::{ use jsonrpsee::{
core::{SubscriptionResult, async_trait}, core::{SubscriptionResult, async_trait},
@ -124,6 +124,8 @@ impl MockIndexerService {
indexer_service_protocol::Nullifier([tx_idx as u8; 32]), indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
CommitmentSetDigest([0xff; 32]), CommitmentSetDigest([0xff; 32]),
)], )],
block_validity_window: ValidityWindow((None, None)),
timestamp_validity_window: ValidityWindow((None, None)),
}, },
witness_set: WitnessSet { witness_set: WitnessSet {
signatures_and_public_keys: vec![], signatures_and_public_keys: vec![],

View File

@ -18,9 +18,11 @@ key_protocol.workspace = true
indexer_service.workspace = true indexer_service.workspace = true
serde_json.workspace = true serde_json.workspace = true
token_core.workspace = true token_core.workspace = true
ata_core.workspace = true
indexer_service_rpc.workspace = true indexer_service_rpc.workspace = true
sequencer_service_rpc = { workspace = true, features = ["client"] } sequencer_service_rpc = { workspace = true, features = ["client"] }
wallet-ffi.workspace = true wallet-ffi.workspace = true
testnet_initial_state.workspace = true
url.workspace = true url.workspace = true

View File

@ -2,16 +2,17 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use bytesize::ByteSize; use bytesize::ByteSize;
use common::block::{AccountInitialData, CommitmentsInitialData};
use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig}; use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig};
use key_protocol::key_management::KeyChain; use key_protocol::key_management::KeyChain;
use nssa::{Account, AccountId, PrivateKey, PublicKey}; use nssa::{Account, AccountId, PrivateKey, PublicKey};
use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID}; use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID};
use sequencer_core::config::{BedrockConfig, SequencerConfig}; use sequencer_core::config::{BedrockConfig, SequencerConfig};
use url::Url; use testnet_initial_state::{
use wallet::config::{ PrivateAccountPrivateInitialData, PrivateAccountPublicInitialData,
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, WalletConfig, PublicAccountPrivateInitialData, PublicAccountPublicInitialData,
}; };
use url::Url;
use wallet::config::{InitialAccountData, WalletConfig};
/// Sequencer config options available for custom changes in integration tests. /// Sequencer config options available for custom changes in integration tests.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -102,13 +103,13 @@ impl InitialData {
} }
} }
fn sequencer_initial_accounts(&self) -> Vec<AccountInitialData> { fn sequencer_initial_public_accounts(&self) -> Vec<PublicAccountPublicInitialData> {
self.public_accounts self.public_accounts
.iter() .iter()
.map(|(priv_key, balance)| { .map(|(priv_key, balance)| {
let pub_key = PublicKey::new_from_private_key(priv_key); let pub_key = PublicKey::new_from_private_key(priv_key);
let account_id = AccountId::from(&pub_key); let account_id = AccountId::from(&pub_key);
AccountInitialData { PublicAccountPublicInitialData {
account_id, account_id,
balance: *balance, balance: *balance,
} }
@ -116,11 +117,11 @@ impl InitialData {
.collect() .collect()
} }
fn sequencer_initial_commitments(&self) -> Vec<CommitmentsInitialData> { fn sequencer_initial_private_accounts(&self) -> Vec<PrivateAccountPublicInitialData> {
self.private_accounts self.private_accounts
.iter() .iter()
.map(|(key_chain, account)| CommitmentsInitialData { .map(|(key_chain, account)| PrivateAccountPublicInitialData {
npk: key_chain.nullifier_public_key.clone(), npk: key_chain.nullifier_public_key,
account: account.clone(), account: account.clone(),
}) })
.collect() .collect()
@ -132,14 +133,14 @@ impl InitialData {
.map(|(priv_key, _)| { .map(|(priv_key, _)| {
let pub_key = PublicKey::new_from_private_key(priv_key); let pub_key = PublicKey::new_from_private_key(priv_key);
let account_id = AccountId::from(&pub_key); let account_id = AccountId::from(&pub_key);
InitialAccountData::Public(InitialAccountDataPublic { InitialAccountData::Public(PublicAccountPrivateInitialData {
account_id, account_id,
pub_sign_key: priv_key.clone(), pub_sign_key: priv_key.clone(),
}) })
}) })
.chain(self.private_accounts.iter().map(|(key_chain, account)| { .chain(self.private_accounts.iter().map(|(key_chain, account)| {
let account_id = AccountId::from(&key_chain.nullifier_public_key); let account_id = AccountId::from(&key_chain.nullifier_public_key);
InitialAccountData::Private(Box::new(InitialAccountDataPrivate { InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
account_id, account_id,
account: account.clone(), account: account.clone(),
key_chain: key_chain.clone(), key_chain: key_chain.clone(),
@ -181,8 +182,8 @@ pub fn indexer_config(
max_retries: 10, max_retries: 10,
}, },
}, },
initial_accounts: initial_data.sequencer_initial_accounts(), initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_commitments: initial_data.sequencer_initial_commitments(), initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
signing_key: [37; 32], signing_key: [37; 32],
channel_id: bedrock_channel_id(), channel_id: bedrock_channel_id(),
}) })
@ -210,9 +211,9 @@ pub fn sequencer_config(
max_block_size, max_block_size,
mempool_max_size, mempool_max_size,
block_create_timeout, block_create_timeout,
retry_pending_blocks_timeout: Duration::from_secs(120), retry_pending_blocks_timeout: Duration::from_secs(5),
initial_accounts: initial_data.sequencer_initial_accounts(), initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_commitments: initial_data.sequencer_initial_commitments(), initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
signing_key: [37; 32], signing_key: [37; 32],
bedrock_config: BedrockConfig { bedrock_config: BedrockConfig {
backoff: BackoffConfig { backoff: BackoffConfig {
@ -240,7 +241,7 @@ pub fn wallet_config(
seq_tx_poll_max_blocks: 15, seq_tx_poll_max_blocks: 15,
seq_poll_max_retries: 10, seq_poll_max_retries: 10,
seq_block_poll_max_amount: 100, seq_block_poll_max_amount: 100,
initial_accounts: initial_data.wallet_initial_accounts(), initial_accounts: Some(initial_data.wallet_initial_accounts()),
basic_auth: None, basic_auth: None,
}) })
} }

View File

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

View File

@ -113,9 +113,12 @@ async fn amm_public() -> Result<()> {
// Create new token // Create new token
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id_1), definition_account_id: Some(format_public_account_id(definition_account_id_1)),
supply_account_id: format_public_account_id(supply_account_id_1), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_1)),
supply_account_label: None,
name: "A NAM1".to_owned(), name: "A NAM1".to_owned(),
total_supply: 37, total_supply: 37,
}; };
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
@ -124,8 +127,10 @@ async fn amm_public() -> Result<()> {
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_public_account_id(supply_account_id_1), from: Some(format_public_account_id(supply_account_id_1)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id_1)), to: Some(format_public_account_id(recipient_account_id_1)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 7, amount: 7,
@ -137,9 +142,12 @@ async fn amm_public() -> Result<()> {
// Create new token // Create new token
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id_2), definition_account_id: Some(format_public_account_id(definition_account_id_2)),
supply_account_id: format_public_account_id(supply_account_id_2), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_2)),
supply_account_label: None,
name: "A NAM2".to_owned(), name: "A NAM2".to_owned(),
total_supply: 37, total_supply: 37,
}; };
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
@ -148,8 +156,10 @@ async fn amm_public() -> Result<()> {
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2` // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_public_account_id(supply_account_id_2), from: Some(format_public_account_id(supply_account_id_2)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id_2)), to: Some(format_public_account_id(recipient_account_id_2)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 7, amount: 7,
@ -181,9 +191,12 @@ async fn amm_public() -> Result<()> {
// Send creation tx // Send creation tx
let subcommand = AmmProgramAgnosticSubcommand::New { let subcommand = AmmProgramAgnosticSubcommand::New {
user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_b: format_public_account_id(recipient_account_id_2), user_holding_a_label: None,
user_holding_lp: format_public_account_id(user_holding_lp), user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
balance_a: 3, balance_a: 3,
balance_b: 3, balance_b: 3,
}; };
@ -223,9 +236,11 @@ async fn amm_public() -> Result<()> {
// Make swap // Make swap
let subcommand = AmmProgramAgnosticSubcommand::Swap { let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_b: format_public_account_id(recipient_account_id_2), user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
amount_in: 2, amount_in: 2,
min_amount_out: 1, min_amount_out: 1,
token_definition: definition_account_id_1.to_string(), token_definition: definition_account_id_1.to_string(),
@ -266,9 +281,11 @@ async fn amm_public() -> Result<()> {
// Make swap // Make swap
let subcommand = AmmProgramAgnosticSubcommand::Swap { let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_b: format_public_account_id(recipient_account_id_2), user_holding_a_label: None,
user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
amount_in: 2, amount_in: 2,
min_amount_out: 1, min_amount_out: 1,
token_definition: definition_account_id_2.to_string(), token_definition: definition_account_id_2.to_string(),
@ -310,9 +327,12 @@ async fn amm_public() -> Result<()> {
// Add liquidity // Add liquidity
let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity { let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity {
user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_b: format_public_account_id(recipient_account_id_2), user_holding_a_label: None,
user_holding_lp: format_public_account_id(user_holding_lp), user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
min_amount_lp: 1, min_amount_lp: 1,
max_amount_a: 2, max_amount_a: 2,
max_amount_b: 2, max_amount_b: 2,
@ -354,9 +374,12 @@ async fn amm_public() -> Result<()> {
// Remove liquidity // Remove liquidity
let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity { let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity {
user_holding_a: format_public_account_id(recipient_account_id_1), user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
user_holding_b: format_public_account_id(recipient_account_id_2), user_holding_a_label: None,
user_holding_lp: format_public_account_id(user_holding_lp), user_holding_b: Some(format_public_account_id(recipient_account_id_2)),
user_holding_b_label: None,
user_holding_lp: Some(format_public_account_id(user_holding_lp)),
user_holding_lp_label: None,
balance_lp: 2, balance_lp: 2,
min_amount_a: 1, min_amount_a: 1,
min_amount_b: 1, min_amount_b: 1,
@ -397,3 +420,188 @@ async fn amm_public() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn amm_new_pool_using_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create token 1 accounts
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id_1,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id_1,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create holding_a with a label
let holding_a_label = "amm-holding-a-label".to_owned();
let SubcommandReturnValue::RegisterAccount {
account_id: holding_a_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_a_label.clone()),
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create token 2 accounts
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id_2,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id_2,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create holding_b with a label
let holding_b_label = "amm-holding-b-label".to_owned();
let SubcommandReturnValue::RegisterAccount {
account_id: holding_b_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_b_label.clone()),
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create holding_lp with a label
let holding_lp_label = "amm-holding-lp-label".to_owned();
let SubcommandReturnValue::RegisterAccount {
account_id: holding_lp_id,
} = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(holding_lp_label.clone()),
})),
)
.await?
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create token 1 and distribute to holding_a
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_1)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_1)),
supply_account_label: None,
name: "TOKEN1".to_owned(),
total_supply: 10,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_1)),
from_label: None,
to: Some(format_public_account_id(holding_a_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create token 2 and distribute to holding_b
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id_2)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id_2)),
supply_account_label: None,
name: "TOKEN2".to_owned(),
total_supply: 10,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id_2)),
from_label: None,
to: Some(format_public_account_id(holding_b_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 5,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create AMM pool using account labels instead of IDs
let subcommand = AmmProgramAgnosticSubcommand::New {
user_holding_a: None,
user_holding_a_label: Some(holding_a_label),
user_holding_b: None,
user_holding_b_label: Some(holding_b_label),
user_holding_lp: None,
user_holding_lp_label: Some(holding_lp_label),
balance_a: 3,
balance_b: 3,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let holding_lp_acc = ctx.sequencer_client().get_account(holding_lp_id).await?;
// LP balance should be 3 (geometric mean of 3, 3)
assert_eq!(
u128::from_le_bytes(holding_lp_acc.data[33..].try_into().unwrap()),
3
);
info!("Successfully created AMM pool using account labels");
Ok(())
}

View File

@ -0,0 +1,674 @@
#![expect(
clippy::shadow_unrelated,
clippy::tests_outside_test_module,
reason = "We don't care about these in tests"
)]
use std::time::Duration;
use anyhow::{Context as _, Result};
use ata_core::{compute_ata_seed, get_associated_token_account_id};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
};
use log::info;
use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _;
use token_core::{TokenDefinition, TokenHolding};
use tokio::test;
use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
programs::{ata::AtaSubcommand, token::TokenProgramAgnosticSubcommand},
};
/// Create a public account and return its ID.
async fn new_public_account(ctx: &mut TestContext) -> Result<nssa::AccountId> {
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
anyhow::bail!("Expected RegisterAccount return value");
};
Ok(account_id)
}
/// Create a private account and return its ID.
async fn new_private_account(ctx: &mut TestContext) -> Result<nssa::AccountId> {
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
anyhow::bail!("Expected RegisterAccount return value");
};
Ok(account_id)
}
#[test]
async fn create_ata_initializes_holding_account() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let owner_account_id = new_public_account(&mut ctx).await?;
// Create a fungible token
let total_supply = 100_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create the ATA for owner + definition
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Derive expected ATA address and check on-chain state
let ata_program_id = Program::ata().id();
let ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(owner_account_id, definition_account_id),
);
let ata_acc = ctx
.sequencer_client()
.get_account(ata_id)
.await
.context("ATA account not found")?;
assert_eq!(ata_acc.program_owner, Program::token().id());
let holding = TokenHolding::try_from(&ata_acc.data)?;
assert_eq!(
holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: 0,
}
);
Ok(())
}
#[test]
async fn create_ata_is_idempotent() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let owner_account_id = new_public_account(&mut ctx).await?;
// Create a fungible token
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply: 100,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create the ATA once
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create the ATA a second time — must succeed (idempotent)
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// State must be unchanged
let ata_program_id = Program::ata().id();
let ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(owner_account_id, definition_account_id),
);
let ata_acc = ctx
.sequencer_client()
.get_account(ata_id)
.await
.context("ATA account not found")?;
assert_eq!(ata_acc.program_owner, Program::token().id());
let holding = TokenHolding::try_from(&ata_acc.data)?;
assert_eq!(
holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: 0,
}
);
Ok(())
}
#[test]
async fn transfer_and_burn_via_ata() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let sender_account_id = new_public_account(&mut ctx).await?;
let recipient_account_id = new_public_account(&mut ctx).await?;
let total_supply = 1000_u128;
// Create a fungible token, supply goes to supply_account_id
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Derive ATA addresses
let ata_program_id = Program::ata().id();
let sender_ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(sender_account_id, definition_account_id),
);
let recipient_ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(recipient_account_id, definition_account_id),
);
// Create ATAs for sender and recipient
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(recipient_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Fund sender's ATA from the supply account (direct token transfer)
let fund_amount = 200_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: fund_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Transfer from sender's ATA to recipient's ATA via the ATA program
let transfer_amount = 50_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Send {
from: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
to: recipient_ata_id.to_string(),
amount: transfer_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Verify sender ATA balance decreased
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
assert_eq!(
sender_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: fund_amount - transfer_amount,
}
);
// Verify recipient ATA balance increased
let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?;
let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?;
assert_eq!(
recipient_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: transfer_amount,
}
);
// Burn from sender's ATA
let burn_amount = 30_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Burn {
holder: format_public_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
amount: burn_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Verify sender ATA balance after burn
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
assert_eq!(
sender_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: fund_amount - transfer_amount - burn_amount,
}
);
// Verify the token definition total_supply decreased by burn_amount
let definition_acc = ctx
.sequencer_client()
.get_account(definition_account_id)
.await?;
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
assert_eq!(
token_definition,
TokenDefinition::Fungible {
name: "TEST".to_owned(),
total_supply: total_supply - burn_amount,
metadata_id: None,
}
);
Ok(())
}
#[test]
async fn create_ata_with_private_owner() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let owner_account_id = new_private_account(&mut ctx).await?;
// Create a fungible token
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply: 100,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Create the ATA for the private owner + definition
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(owner_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Derive expected ATA address and check on-chain state
let ata_program_id = Program::ata().id();
let ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(owner_account_id, definition_account_id),
);
let ata_acc = ctx
.sequencer_client()
.get_account(ata_id)
.await
.context("ATA account not found")?;
assert_eq!(ata_acc.program_owner, Program::token().id());
let holding = TokenHolding::try_from(&ata_acc.data)?;
assert_eq!(
holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: 0,
}
);
// Verify the private owner's commitment is in state
let commitment = ctx
.wallet()
.get_private_account_commitment(owner_account_id)
.context("Private owner commitment not found")?;
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
Ok(())
}
#[test]
async fn transfer_via_ata_private_owner() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let sender_account_id = new_private_account(&mut ctx).await?;
let recipient_account_id = new_public_account(&mut ctx).await?;
let total_supply = 1000_u128;
// Create a fungible token
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Derive ATA addresses
let ata_program_id = Program::ata().id();
let sender_ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(sender_account_id, definition_account_id),
);
let recipient_ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(recipient_account_id, definition_account_id),
);
// Create ATAs for sender (private owner) and recipient (public owner)
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_public_account_id(recipient_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Fund sender's ATA from the supply account (direct token transfer)
let fund_amount = 200_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: fund_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Transfer from sender's ATA (private owner) to recipient's ATA
let transfer_amount = 50_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Send {
from: format_private_account_id(sender_account_id),
token_definition: definition_account_id.to_string(),
to: recipient_ata_id.to_string(),
amount: transfer_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Verify sender ATA balance decreased
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
assert_eq!(
sender_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: fund_amount - transfer_amount,
}
);
// Verify recipient ATA balance increased
let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?;
let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?;
assert_eq!(
recipient_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: transfer_amount,
}
);
// Verify the private sender's commitment is in state
let commitment = ctx
.wallet()
.get_private_account_commitment(sender_account_id)
.context("Private sender commitment not found")?;
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
Ok(())
}
#[test]
async fn burn_via_ata_private_owner() -> Result<()> {
let mut ctx = TestContext::new().await?;
let definition_account_id = new_public_account(&mut ctx).await?;
let supply_account_id = new_public_account(&mut ctx).await?;
let holder_account_id = new_private_account(&mut ctx).await?;
let total_supply = 500_u128;
// Create a fungible token
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(),
total_supply,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Derive holder's ATA address
let ata_program_id = Program::ata().id();
let holder_ata_id = get_associated_token_account_id(
&ata_program_id,
&compute_ata_seed(holder_account_id, definition_account_id),
);
// Create ATA for the private holder
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Create {
owner: format_private_account_id(holder_account_id),
token_definition: definition_account_id.to_string(),
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Fund holder's ATA from the supply account
let fund_amount = 300_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send {
from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(holder_ata_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: fund_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Burn from holder's ATA (private owner)
let burn_amount = 100_u128;
wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Ata(AtaSubcommand::Burn {
holder: format_private_account_id(holder_account_id),
token_definition: definition_account_id.to_string(),
amount: burn_amount,
}),
)
.await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Verify holder ATA balance after burn
let holder_ata_acc = ctx.sequencer_client().get_account(holder_ata_id).await?;
let holder_holding = TokenHolding::try_from(&holder_ata_acc.data)?;
assert_eq!(
holder_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: fund_amount - burn_amount,
}
);
// Verify the token definition total_supply decreased by burn_amount
let definition_acc = ctx
.sequencer_client()
.get_account(definition_account_id)
.await?;
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
assert_eq!(
token_definition,
TokenDefinition::Fungible {
name: "TEST".to_owned(),
total_supply: total_supply - burn_amount,
metadata_id: None,
}
);
// Verify the private holder's commitment is in state
let commitment = ctx
.wallet()
.get_private_account_commitment(holder_account_id)
.context("Private holder commitment not found")?;
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
Ok(())
}

View File

@ -24,8 +24,10 @@ async fn private_transfer_to_owned_account() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1]; let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)), to: Some(format_private_account_id(to)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -63,8 +65,10 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
let to_vpk = Secp256k1Point::from_scalar(to_npk.0); let to_vpk = Secp256k1Point::from_scalar(to_npk.0);
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: None, to: None,
to_label: None,
to_npk: Some(to_npk_string), to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)), to_vpk: Some(hex::encode(to_vpk.0)),
amount: 100, amount: 100,
@ -111,8 +115,10 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
assert_eq!(from_acc.balance, 10000); assert_eq!(from_acc.balance, 10000);
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to)), to: Some(format_public_account_id(to)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -174,8 +180,10 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
// Send to this account using claiming path (using npk and vpk instead of account ID) // Send to this account using claiming path (using npk and vpk instead of account ID)
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: None, to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
amount: 100, amount: 100,
@ -222,8 +230,10 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
let to: AccountId = ctx.existing_private_accounts()[1]; let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(from), from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)), to: Some(format_private_account_id(to)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -264,8 +274,10 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
let from: AccountId = ctx.existing_public_accounts()[0]; let from: AccountId = ctx.existing_public_accounts()[0];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(from), from: Some(format_public_account_id(from)),
from_label: None,
to: None, to: None,
to_label: None,
to_npk: Some(to_npk_string), to_npk: Some(to_npk_string),
to_vpk: Some(hex::encode(to_vpk.0)), to_vpk: Some(hex::encode(to_vpk.0)),
amount: 100, amount: 100,
@ -334,8 +346,10 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
// Send transfer using nullifier and viewing public keys // Send transfer using nullifier and viewing public keys
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: None, to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
amount: 100, amount: 100,
@ -383,7 +397,8 @@ async fn initialize_private_account() -> Result<()> {
}; };
let command = Command::AuthTransfer(AuthTransferSubcommand::Init { let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: format_private_account_id(account_id), account_id: Some(format_private_account_id(account_id)),
account_label: None,
}); });
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -415,3 +430,100 @@ async fn initialize_private_account() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn private_transfer_using_from_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
let from: AccountId = ctx.existing_private_accounts()[0];
let to: AccountId = ctx.existing_private_accounts()[1];
// Assign a label to the sender account
let label = "private-sender-label".to_owned();
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_private_account_id(from)),
account_label: None,
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(label),
to: Some(format_private_account_id(to)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let new_commitment1 = ctx
.wallet()
.get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?;
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
let new_commitment2 = ctx
.wallet()
.get_private_account_commitment(to)
.context("Failed to get private account commitment for receiver")?;
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
info!("Successfully transferred privately using from_label");
Ok(())
}
#[test]
async fn initialize_private_account_using_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create a new private account with a label
let label = "init-private-label".to_owned();
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
cci: None,
label: Some(label.clone()),
}));
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Initialize using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: None,
account_label: Some(label),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let command = Command::Account(AccountSubcommand::SyncPrivate {});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let new_commitment = ctx
.wallet()
.get_private_account_commitment(account_id)
.context("Failed to get private account commitment")?;
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
let account = ctx
.wallet()
.get_account_private(account_id)
.context("Failed to get private account")?;
assert_eq!(
account.program_owner,
Program::authenticated_transfer_program().id()
);
info!("Successfully initialized private account using label");
Ok(())
}

View File

@ -17,8 +17,10 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
let mut ctx = TestContext::new().await?; let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -73,8 +75,10 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
.expect("Failed to find newly created account in the wallet storage"); .expect("Failed to find newly created account in the wallet storage");
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(new_persistent_account_id)), to: Some(format_public_account_id(new_persistent_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -109,8 +113,10 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
let mut ctx = TestContext::new().await?; let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 1_000_000, amount: 1_000_000,
@ -147,8 +153,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
// First transfer // First transfer
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -179,8 +187,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
// Second transfer // Second transfer
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -226,7 +236,8 @@ async fn initialize_public_account() -> Result<()> {
}; };
let command = Command::AuthTransfer(AuthTransferSubcommand::Init { let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: format_public_account_id(account_id), account_id: Some(format_public_account_id(account_id)),
account_label: None,
}); });
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -245,3 +256,97 @@ async fn initialize_public_account() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn successful_transfer_using_from_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign a label to the sender account
let label = "sender-label".to_owned();
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label instead of account ID
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(label),
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
let acc_1_balance = ctx
.sequencer_client()
.get_account_balance(ctx.existing_public_accounts()[0])
.await?;
let acc_2_balance = ctx
.sequencer_client()
.get_account_balance(ctx.existing_public_accounts()[1])
.await?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Successfully transferred using from_label");
Ok(())
}
#[test]
async fn successful_transfer_using_to_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign a label to the receiver account
let label = "receiver-label".to_owned();
let command = Command::Account(AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
label: label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
// Send using the label for the recipient
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: None,
to_label: Some(label),
to_npk: None,
to_vpk: None,
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
let acc_1_balance = ctx
.sequencer_client()
.get_account_balance(ctx.existing_public_accounts()[0])
.await?;
let acc_2_balance = ctx
.sequencer_client()
.get_account_balance(ctx.existing_public_accounts()[1])
.await?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Successfully transferred using to_label");
Ok(())
}

View File

@ -1,38 +1,68 @@
#![expect( #![expect(
clippy::shadow_unrelated,
clippy::tests_outside_test_module, clippy::tests_outside_test_module,
reason = "We don't care about these in tests" reason = "We don't care about these in tests"
)] )]
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::{Context as _, Result};
use indexer_service_rpc::RpcClient as _; use indexer_service_rpc::RpcClient as _;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id,
format_public_account_id, verify_commitment_is_in_state,
};
use log::info; use log::info;
use nssa::AccountId;
use tokio::test; use tokio::test;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
/// Timeout in milliseconds to reliably await for block finalization. /// Maximum time to wait for the indexer to catch up to the sequencer.
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 600_000; const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000;
/// Poll the indexer until its last finalized block id reaches the sequencer's
/// current last block id (and at least the genesis block has been advanced past),
/// or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses. Returns the last indexer block
/// id observed.
async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 {
let timeout = Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS);
let mut last_ind: u64 = 1;
let inner = async {
loop {
let seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client())
.await
.unwrap_or(0);
let ind = ctx
.indexer_client()
.get_last_finalized_block_id()
.await
.unwrap_or(1);
last_ind = ind;
if ind >= seq && ind > 1 {
info!("Indexer caught up: seq={seq}, ind={ind}");
return ind;
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
};
tokio::time::timeout(timeout, inner)
.await
.unwrap_or_else(|_| {
info!("Indexer catch-up timed out: ind={last_ind}");
last_ind
})
}
#[test] #[test]
async fn indexer_test_run() -> Result<()> { async fn indexer_test_run() -> Result<()> {
let ctx = TestContext::new().await?; let ctx = TestContext::new().await?;
// RUN OBSERVATION let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await;
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await;
let last_block_seq = let last_block_seq =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?; sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?;
info!("Last block on seq now is {last_block_seq}"); info!("Last block on seq now is {last_block_seq}");
let last_block_indexer = ctx
.indexer_client()
.get_last_finalized_block_id()
.await
.unwrap();
info!("Last block on ind now is {last_block_indexer}"); info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 1); assert!(last_block_indexer > 1);
@ -44,15 +74,8 @@ async fn indexer_test_run() -> Result<()> {
async fn indexer_block_batching() -> Result<()> { async fn indexer_block_batching() -> Result<()> {
let ctx = TestContext::new().await?; let ctx = TestContext::new().await?;
// WAIT
info!("Waiting for indexer to parse blocks"); info!("Waiting for indexer to parse blocks");
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await;
let last_block_indexer = ctx
.indexer_client()
.get_last_finalized_block_id()
.await
.unwrap();
info!("Last block on ind now is {last_block_indexer}"); info!("Last block on ind now is {last_block_indexer}");
@ -83,8 +106,10 @@ async fn indexer_state_consistency() -> Result<()> {
let mut ctx = TestContext::new().await?; let mut ctx = TestContext::new().await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(ctx.existing_public_accounts()[0]), from: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
from_label: None,
to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), to: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -113,9 +138,40 @@ async fn indexer_state_consistency() -> Result<()> {
assert_eq!(acc_1_balance, 9900); assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100); assert_eq!(acc_2_balance, 20100);
// WAIT let from: AccountId = ctx.existing_private_accounts()[0];
let to: AccountId = ctx.existing_private_accounts()[1];
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let new_commitment1 = ctx
.wallet()
.get_private_account_commitment(from)
.context("Failed to get private account commitment for sender")?;
assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await);
let new_commitment2 = ctx
.wallet()
.get_private_account_commitment(to)
.context("Failed to get private account commitment for receiver")?;
assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await);
info!("Successfully transferred privately to owned account");
info!("Waiting for indexer to parse blocks"); info!("Waiting for indexer to parse blocks");
tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; wait_for_indexer_to_catch_up(&ctx).await;
let acc1_ind_state = ctx let acc1_ind_state = ctx
.indexer_client() .indexer_client()
@ -147,3 +203,76 @@ async fn indexer_state_consistency() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn indexer_state_consistency_with_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Assign labels to both accounts
let from_label = "idx-sender-label".to_owned();
let to_label_str = "idx-receiver-label".to_owned();
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
account_label: None,
label: from_label.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?;
let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label {
account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])),
account_label: None,
label: to_label_str.clone(),
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?;
// Send using labels instead of account IDs
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: None,
from_label: Some(from_label),
to: None,
to_label: Some(to_label_str),
to_npk: None,
to_vpk: None,
amount: 100,
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let acc_1_balance = sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
)
.await?;
let acc_2_balance = sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
ctx.existing_public_accounts()[1],
)
.await?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await;
let acc1_ind_state = ctx
.indexer_client()
.get_account(ctx.existing_public_accounts()[0].into())
.await
.unwrap();
let acc1_seq_state = sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
ctx.existing_public_accounts()[0],
)
.await?;
assert_eq!(acc1_ind_state, acc1_seq_state.into());
info!("Indexer state is consistent after label-based transfer");
Ok(())
}

View File

@ -69,8 +69,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
// Send to this account using claiming path (using npk and vpk instead of account ID) // Send to this account using claiming path (using npk and vpk instead of account ID)
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: None, to: None,
to_label: None,
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
amount: 100, amount: 100,
@ -143,8 +145,10 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to first private account // Send to first private account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to_account_id1)), to: Some(format_private_account_id(to_account_id1)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 100, amount: 100,
@ -153,8 +157,10 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to second private account // Send to second private account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(from), from: Some(format_private_account_id(from)),
from_label: None,
to: Some(format_private_account_id(to_account_id2)), to: Some(format_private_account_id(to_account_id2)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 101, amount: 101,
@ -191,8 +197,10 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to first public account // Send to first public account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(from), from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to_account_id3)), to: Some(format_public_account_id(to_account_id3)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 102, amount: 102,
@ -201,8 +209,10 @@ async fn restore_keys_from_seed() -> Result<()> {
// Send to second public account // Send to second public account
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(from), from: Some(format_public_account_id(from)),
from_label: None,
to: Some(format_public_account_id(to_account_id4)), to: Some(format_public_account_id(to_account_id4)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 103, amount: 103,
@ -264,8 +274,10 @@ async fn restore_keys_from_seed() -> Result<()> {
// Test that restored accounts can send transactions // Test that restored accounts can send transactions
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_private_account_id(to_account_id1), from: Some(format_private_account_id(to_account_id1)),
from_label: None,
to: Some(format_private_account_id(to_account_id2)), to: Some(format_private_account_id(to_account_id2)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 10, amount: 10,
@ -273,8 +285,10 @@ async fn restore_keys_from_seed() -> Result<()> {
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
let command = Command::AuthTransfer(AuthTransferSubcommand::Send { let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
from: format_public_account_id(to_account_id3), from: Some(format_public_account_id(to_account_id3)),
from_label: None,
to: Some(format_public_account_id(to_account_id4)), to: Some(format_public_account_id(to_account_id4)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: 11, amount: 11,

View File

@ -52,7 +52,8 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
let claim_result = wallet::cli::execute_subcommand( let claim_result = wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Pinata(PinataProgramAgnosticSubcommand::Claim { Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: winner_account_id_formatted, to: Some(winner_account_id_formatted),
to_label: None,
}), }),
) )
.await; .await;
@ -106,7 +107,8 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()
let claim_result = wallet::cli::execute_subcommand( let claim_result = wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Pinata(PinataProgramAgnosticSubcommand::Claim { Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: winner_account_id_formatted, to: Some(winner_account_id_formatted),
to_label: None,
}), }),
) )
.await; .await;
@ -137,7 +139,8 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
let pinata_prize = 150; let pinata_prize = 150;
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: format_public_account_id(ctx.existing_public_accounts()[0]), to: Some(format_public_account_id(ctx.existing_public_accounts()[0])),
to_label: None,
}); });
let pinata_balance_pre = ctx let pinata_balance_pre = ctx
@ -175,7 +178,10 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
let pinata_prize = 150; let pinata_prize = 150;
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: format_private_account_id(ctx.existing_private_accounts()[0]), to: Some(format_private_account_id(
ctx.existing_private_accounts()[0],
)),
to_label: None,
}); });
let pinata_balance_pre = ctx let pinata_balance_pre = ctx
@ -239,7 +245,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
// Initialize account under auth transfer program // Initialize account under auth transfer program
let command = Command::AuthTransfer(AuthTransferSubcommand::Init { let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
account_id: winner_account_id_formatted.clone(), account_id: Some(winner_account_id_formatted.clone()),
account_label: None,
}); });
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
@ -254,7 +261,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
// Claim pinata to the new private account // Claim pinata to the new private account
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
to: winner_account_id_formatted, to: Some(winner_account_id_formatted),
to_label: None,
}); });
let pinata_balance_pre = ctx let pinata_balance_pre = ctx

View File

@ -11,10 +11,13 @@ use integration_tests::{
NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
}; };
use log::info; use log::info;
use nssa::{AccountId, program::Program}; use nssa::program::Program;
use sequencer_service_rpc::RpcClient as _; use sequencer_service_rpc::RpcClient as _;
use tokio::test; use tokio::test;
use wallet::cli::Command; use wallet::cli::{
Command, SubcommandReturnValue,
account::{AccountSubcommand, NewSubcommand},
};
#[test] #[test]
async fn deploy_and_execute_program() -> Result<()> { async fn deploy_and_execute_program() -> Result<()> {
@ -40,14 +43,31 @@ async fn deploy_and_execute_program() -> Result<()> {
// logic) // logic)
let bytecode = std::fs::read(binary_filepath)?; let bytecode = std::fs::read(binary_filepath)?;
let data_changer = Program::new(bytecode)?; 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( let message = nssa::public_transaction::Message::try_new(
data_changer.id(), data_changer.id(),
vec![account_id], vec![account_id],
vec![], nonces,
vec![0], 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 transaction = nssa::PublicTransaction::new(message, witness_set);
let _response = ctx let _response = ctx
.sequencer_client() .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.program_owner, data_changer.id());
assert_eq!(post_state_account.balance, 0); assert_eq!(post_state_account.balance, 0);
assert_eq!(post_state_account.data.as_ref(), &[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"); info!("Successfully deployed and executed program");

View File

@ -79,8 +79,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: name.clone(), name: name.clone(),
total_supply, total_supply,
}; };
@ -126,8 +128,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Transfer 7 tokens from supply_acc to recipient_account_id // Transfer 7 tokens from supply_acc to recipient_account_id
let transfer_amount = 7; let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_public_account_id(supply_account_id), from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id)), to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: transfer_amount, amount: transfer_amount,
@ -171,8 +175,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Burn 3 tokens from recipient_acc // Burn 3 tokens from recipient_acc
let burn_amount = 3; let burn_amount = 3;
let subcommand = TokenProgramAgnosticSubcommand::Burn { let subcommand = TokenProgramAgnosticSubcommand::Burn {
definition: format_public_account_id(definition_account_id), definition: Some(format_public_account_id(definition_account_id)),
holder: format_public_account_id(recipient_account_id), definition_label: None,
holder: Some(format_public_account_id(recipient_account_id)),
holder_label: None,
amount: burn_amount, amount: burn_amount,
}; };
@ -215,8 +221,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
// Mint 10 tokens at recipient_acc // Mint 10 tokens at recipient_acc
let mint_amount = 10; let mint_amount = 10;
let subcommand = TokenProgramAgnosticSubcommand::Mint { let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: format_public_account_id(definition_account_id), definition: Some(format_public_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_public_account_id(recipient_account_id)), holder: Some(format_public_account_id(recipient_account_id)),
holder_label: None,
holder_npk: None, holder_npk: None,
holder_vpk: None, holder_vpk: None,
amount: mint_amount, amount: mint_amount,
@ -319,8 +327,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_private_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
name: name.clone(), name: name.clone(),
total_supply, total_supply,
}; };
@ -356,8 +366,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Transfer 7 tokens from supply_acc to recipient_account_id // Transfer 7 tokens from supply_acc to recipient_account_id
let transfer_amount = 7; let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_private_account_id(supply_account_id), from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)), to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: transfer_amount, amount: transfer_amount,
@ -383,8 +395,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
// Burn 3 tokens from recipient_acc // Burn 3 tokens from recipient_acc
let burn_amount = 3; let burn_amount = 3;
let subcommand = TokenProgramAgnosticSubcommand::Burn { let subcommand = TokenProgramAgnosticSubcommand::Burn {
definition: format_public_account_id(definition_account_id), definition: Some(format_public_account_id(definition_account_id)),
holder: format_private_account_id(recipient_account_id), definition_label: None,
holder: Some(format_private_account_id(recipient_account_id)),
holder_label: None,
amount: burn_amount, amount: burn_amount,
}; };
@ -475,8 +489,10 @@ async fn create_token_with_private_definition() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_private_account_id(definition_account_id), definition_account_id: Some(format_private_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: name.clone(), name: name.clone(),
total_supply, total_supply,
}; };
@ -544,8 +560,10 @@ async fn create_token_with_private_definition() -> Result<()> {
// Mint to public account // Mint to public account
let mint_amount_public = 10; let mint_amount_public = 10;
let subcommand = TokenProgramAgnosticSubcommand::Mint { let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: format_private_account_id(definition_account_id), definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_public_account_id(recipient_account_id_public)), holder: Some(format_public_account_id(recipient_account_id_public)),
holder_label: None,
holder_npk: None, holder_npk: None,
holder_vpk: None, holder_vpk: None,
amount: mint_amount_public, amount: mint_amount_public,
@ -590,8 +608,10 @@ async fn create_token_with_private_definition() -> Result<()> {
// Mint to private account // Mint to private account
let mint_amount_private = 5; let mint_amount_private = 5;
let subcommand = TokenProgramAgnosticSubcommand::Mint { let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: format_private_account_id(definition_account_id), definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
holder: Some(format_private_account_id(recipient_account_id_private)), holder: Some(format_private_account_id(recipient_account_id_private)),
holder_label: None,
holder_npk: None, holder_npk: None,
holder_vpk: None, holder_vpk: None,
amount: mint_amount_private, amount: mint_amount_private,
@ -669,8 +689,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_private_account_id(definition_account_id), definition_account_id: Some(format_private_account_id(definition_account_id)),
supply_account_id: format_private_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
name, name,
total_supply, total_supply,
}; };
@ -728,8 +750,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
// Transfer tokens // Transfer tokens
let transfer_amount = 7; let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_private_account_id(supply_account_id), from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)), to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: transfer_amount, amount: transfer_amount,
@ -841,8 +865,10 @@ async fn shielded_token_transfer() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name, name,
total_supply, total_supply,
}; };
@ -855,8 +881,10 @@ async fn shielded_token_transfer() -> Result<()> {
// Perform shielded transfer: public supply -> private recipient // Perform shielded transfer: public supply -> private recipient
let transfer_amount = 7; let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_public_account_id(supply_account_id), from: Some(format_public_account_id(supply_account_id)),
from_label: None,
to: Some(format_private_account_id(recipient_account_id)), to: Some(format_private_account_id(recipient_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: transfer_amount, amount: transfer_amount,
@ -963,8 +991,10 @@ async fn deshielded_token_transfer() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_private_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
name, name,
total_supply, total_supply,
}; };
@ -977,8 +1007,10 @@ async fn deshielded_token_transfer() -> Result<()> {
// Perform deshielded transfer: private supply -> public recipient // Perform deshielded transfer: private supply -> public recipient
let transfer_amount = 7; let transfer_amount = 7;
let subcommand = TokenProgramAgnosticSubcommand::Send { let subcommand = TokenProgramAgnosticSubcommand::Send {
from: format_private_account_id(supply_account_id), from: Some(format_private_account_id(supply_account_id)),
from_label: None,
to: Some(format_public_account_id(recipient_account_id)), to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: transfer_amount, amount: transfer_amount,
@ -1069,8 +1101,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
let name = "A NAME".to_owned(); let name = "A NAME".to_owned();
let total_supply = 37; let total_supply = 37;
let subcommand = TokenProgramAgnosticSubcommand::New { let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: format_private_account_id(definition_account_id), definition_account_id: Some(format_private_account_id(definition_account_id)),
supply_account_id: format_private_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_private_account_id(supply_account_id)),
supply_account_label: None,
name, name,
total_supply, total_supply,
}; };
@ -1108,8 +1142,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
// Mint using claiming path (foreign account) // Mint using claiming path (foreign account)
let mint_amount = 9; let mint_amount = 9;
let subcommand = TokenProgramAgnosticSubcommand::Mint { let subcommand = TokenProgramAgnosticSubcommand::Mint {
definition: format_private_account_id(definition_account_id), definition: Some(format_private_account_id(definition_account_id)),
definition_label: None,
holder: None, holder: None,
holder_label: None,
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)), holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)),
amount: mint_amount, amount: mint_amount,
@ -1149,3 +1185,193 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
async fn create_token_using_labels() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create definition and supply accounts with labels
let def_label = "token-definition-label".to_owned();
let supply_label = "token-supply-label".to_owned();
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(def_label.clone()),
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = result
else {
anyhow::bail!("Expected RegisterAccount return value");
};
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(supply_label.clone()),
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = result
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create token using account labels instead of IDs
let name = "LABELED TOKEN".to_owned();
let total_supply = 100;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: None,
definition_account_label: Some(def_label),
supply_account_id: None,
supply_account_label: Some(supply_label),
name: name.clone(),
total_supply,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let definition_acc = ctx
.sequencer_client()
.get_account(definition_account_id)
.await?;
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
assert_eq!(definition_acc.program_owner, Program::token().id());
assert_eq!(
token_definition,
TokenDefinition::Fungible {
name,
total_supply,
metadata_id: None
}
);
let supply_acc = ctx
.sequencer_client()
.get_account(supply_account_id)
.await?;
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
assert_eq!(
token_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: total_supply
}
);
info!("Successfully created token using definition and supply account labels");
Ok(())
}
#[test]
async fn transfer_token_using_from_label() -> Result<()> {
let mut ctx = TestContext::new().await?;
// Create definition account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
account_id: definition_account_id,
} = result
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create supply account with a label
let supply_label = "token-supply-sender".to_owned();
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: Some(supply_label.clone()),
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
account_id: supply_account_id,
} = result
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create recipient account
let result = wallet::cli::execute_subcommand(
ctx.wallet_mut(),
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
cci: None,
label: None,
})),
)
.await?;
let SubcommandReturnValue::RegisterAccount {
account_id: recipient_account_id,
} = result
else {
anyhow::bail!("Expected RegisterAccount return value");
};
// Create token
let total_supply = 50;
let subcommand = TokenProgramAgnosticSubcommand::New {
definition_account_id: Some(format_public_account_id(definition_account_id)),
definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "LABEL TEST TOKEN".to_owned(),
total_supply,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Transfer token using from_label instead of from
let transfer_amount = 20;
let subcommand = TokenProgramAgnosticSubcommand::Send {
from: None,
from_label: Some(supply_label),
to: Some(format_public_account_id(recipient_account_id)),
to_label: None,
to_npk: None,
to_vpk: None,
amount: transfer_amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let recipient_acc = ctx
.sequencer_client()
.get_account(recipient_account_id)
.await?;
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
assert_eq!(
token_holding,
TokenHolding::Fungible {
definition_id: definition_account_id,
balance: transfer_amount
}
);
info!("Successfully transferred token using from_label");
Ok(())
}

View File

@ -249,10 +249,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
vec![sender_pre, recipient_pre], vec![sender_pre, recipient_pre],
Program::serialize_instruction(balance_to_move).unwrap(), Program::serialize_instruction(balance_to_move).unwrap(),
vec![1, 2], vec![1, 2],
vec![ vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)],
(sender_npk.clone(), sender_ss),
(recipient_npk.clone(), recipient_ss),
],
vec![sender_nsk], vec![sender_nsk],
vec![Some(proof)], vec![Some(proof)],
&program.into(), &program.into(),

View File

@ -24,7 +24,6 @@ use log::info;
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program}; use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
use nssa_core::program::DEFAULT_PROGRAM_ID; use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir; use tempfile::tempdir;
use wallet::WalletCore;
use wallet_ffi::{ use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, WalletHandle, error, 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> { fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
let config_path = home.join("wallet_config.json"); let config_path = home.join("wallet_config.json");
let storage_path = home.join("storage.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<()> { fn wallet_ffi_create_public_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
let n_accounts = 10; 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)?; // Create `n_accounts` public accounts with wallet FFI
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
let new_public_account_ids_ffi = unsafe { let new_public_account_ids_ffi = unsafe {
let mut account_ids = Vec::new(); let mut account_ids = Vec::new();
@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
account_ids 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(()) Ok(())
} }
@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> { fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
let n_accounts = 10; let n_accounts = 10;
// First `n_accounts` private accounts created with Rust wallet // Create `n_accounts` private accounts with wallet FFI
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
let new_private_account_ids_ffi = unsafe { let new_private_account_ids_ffi = unsafe {
let mut account_ids = Vec::new(); let mut account_ids = Vec::new();
@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
account_ids 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(()) Ok(())
} }
@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
fn test_wallet_ffi_list_accounts() -> Result<()> { fn test_wallet_ffi_list_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
// Create the wallet FFI // Create the wallet FFI and track which account IDs were created as public/private
let wallet_ffi_handle = unsafe { let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?; 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 { for _ in 0..5 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]); let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id); 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); wallet_ffi_create_account_private(handle, &raw mut out_account_id);
private_ids.push(out_account_id.data);
} }
handle (handle, public_ids, private_ids)
};
// 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
}; };
// Get the account list with FFI method // Get the account list with FFI method
@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
out_list 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 { let wallet_ffi_account_list_slice = unsafe {
core::slice::from_raw_parts( core::slice::from_raw_parts(
wallet_ffi_account_list.entries, 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 // All created accounts must appear in the list
assert_eq!( let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
wallet_rust_account_ids .iter()
.iter() .filter(|e| e.is_public)
.map(nssa::AccountId::value) .map(|e| e.account_id.data)
.collect::<HashSet<_>>(), .collect();
wallet_ffi_account_list_slice let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter() .iter()
.map(|entry| &entry.account_id.data) .filter(|e| !e.is_public)
.collect::<HashSet<_>>() .map(|e| e.account_id.data)
); .collect();
// Assert `is_pub` flag is correct in the FFI result for id in &created_public_ids {
for entry in wallet_ffi_account_list_slice { assert!(
let account_id = AccountId::new(entry.account_id.data); listed_public_ids.contains(id),
let is_pub_default_in_rust_wallet = wallet_rust "Created public account not found in list with is_public=true"
.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_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 { unsafe {
wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list); 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 home = tempfile::tempdir()?;
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; 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 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 amount: [u8; 16] = 100_u128.to_le_bytes();
let mut transfer_result = FfiTransferResult::default(); let mut transfer_result = FfiTransferResult::default();
@ -967,7 +948,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
}; };
assert_eq!(from_balance, 9900); assert_eq!(from_balance, 9900);
assert_eq!(to_balance, 100); assert_eq!(to_balance, 10100);
unsafe { unsafe {
wallet_ffi_free_transfer_result(&raw mut transfer_result); wallet_ffi_free_transfer_result(&raw mut transfer_result);

View File

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

View File

@ -137,11 +137,12 @@ impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use nssa_core::NullifierSecretKey; use nssa_core::{NullifierPublicKey, NullifierSecretKey};
use super::*; use super::*;
use crate::key_management::{self, secret_holders::ViewingSecretKey}; use crate::key_management::{self, secret_holders::ViewingSecretKey};
#[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")]
#[test] #[test]
fn master_key_generation() { fn master_key_generation() {
let seed: [u8; 64] = [ let seed: [u8; 64] = [
@ -153,7 +154,7 @@ mod tests {
let keys = ChildKeysPrivate::root(seed); 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, 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, 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, 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, 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, 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2,
]); ]);

View File

@ -1,4 +1,4 @@
use secp256k1::Scalar; use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyNode; use crate::key_management::key_tree::traits::KeyNode;
@ -13,7 +13,6 @@ pub struct ChildKeysPublic {
} }
impl ChildKeysPublic { impl ChildKeysPublic {
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
fn compute_hash_value(&self, cci: u32) -> [u8; 64] { fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![]; let mut hash_input = vec![];
@ -21,16 +20,17 @@ impl ChildKeysPublic {
// Non-harden. // Non-harden.
// BIP-032 compatibility requires 1-byte header from the public_key; // BIP-032 compatibility requires 1-byte header from the public_key;
// Not stored in `self.cpk.value()`. // 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"); .expect("32 bytes, within curve order");
let pk = secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk); let pk = sk.public_key();
hash_input.extend_from_slice(&secp256k1::PublicKey::serialize(&pk)); hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
} else { } else {
// Harden. // Harden.
hash_input.extend_from_slice(&[0_u8]); hash_input.extend_from_slice(&[0_u8]);
hash_input.extend_from_slice(self.csk.value()); 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()); hash_input.extend_from_slice(&cci.to_be_bytes());
hmac_sha512::HMAC::mac(hash_input, self.ccc) hmac_sha512::HMAC::mac(hash_input, self.ccc)
@ -41,7 +41,12 @@ impl KeyNode for ChildKeysPublic {
fn root(seed: [u8; 64]) -> Self { fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); 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 ccc = *hash_value.last_chunk::<32>().unwrap();
let cpk = nssa::PublicKey::new_from_private_key(&csk); let cpk = nssa::PublicKey::new_from_private_key(&csk);
@ -56,26 +61,20 @@ impl KeyNode for ChildKeysPublic {
fn nth_child(&self, cci: u32) -> Self { fn nth_child(&self, cci: u32) -> Self {
let hash_value = self.compute_hash_value(cci); 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 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) let value_1 =
.expect("Expect a valid Scalar") k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar");
.secret_bytes() 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(); .expect("Expect a valid private key");
assert!(
secp256k1::constants::CURVE_ORDER >= *csk.value(),
"Secret key cannot exceed curve order"
);
let ccc = *hash_value let ccc = *hash_value
.last_chunk::<32>() .last_chunk::<32>()

View File

@ -42,10 +42,10 @@ impl KeyChain {
} }
#[must_use] #[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. // Currently dropping SeedHolder at the end of initialization.
// Not entirely sure if we need it in the future. // 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 secret_spending_key = seed_holder.produce_top_secret_key_holder();
let private_key_holder = secret_spending_key.produce_private_key_holder(None); 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 nullifier_public_key = private_key_holder.generate_nullifier_public_key();
let viewing_public_key = private_key_holder.generate_viewing_public_key(); let viewing_public_key = private_key_holder.generate_viewing_public_key();
Self { (
secret_spending_key, Self {
private_key_holder, secret_spending_key,
nullifier_public_key, private_key_holder,
viewing_public_key, nullifier_public_key,
} viewing_public_key,
},
mnemonic,
)
} }
#[must_use] #[must_use]

View File

@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest as _, digest::FixedOutput as _}; 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. /// Seed holder. Non-clonable to ensure that different holders use different seeds.
/// Produces `TopSecretKeyHolder` objects. /// Produces `TopSecretKeyHolder` objects.
#[derive(Debug)] #[derive(Debug)]
@ -20,17 +18,16 @@ pub struct SeedHolder {
/// Secret spending key object. Can produce `PrivateKeyHolder` objects. /// Secret spending key object. Can produce `PrivateKeyHolder` objects.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SecretSpendingKey(pub(crate) [u8; 32]); pub struct SecretSpendingKey(pub [u8; 32]);
pub type ViewingSecretKey = Scalar; pub type ViewingSecretKey = Scalar;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
/// for recepient. /// for recepient.
#[expect(clippy::partial_pub_fields, reason = "TODO: fix later")]
pub struct PrivateKeyHolder { pub struct PrivateKeyHolder {
pub nullifier_secret_key: NullifierSecretKey, pub nullifier_secret_key: NullifierSecretKey,
pub(crate) viewing_secret_key: ViewingSecretKey, pub viewing_secret_key: ViewingSecretKey,
} }
impl SeedHolder { impl SeedHolder {
@ -49,9 +46,24 @@ impl SeedHolder {
} }
#[must_use] #[must_use]
pub fn new_mnemonic(passphrase: String) -> Self { pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) {
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) let mut entropy_bytes: [u8; 32] = [0; 32];
.expect("Enthropy must be a multiple of 32 bytes"); 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); let seed_wide = mnemonic.to_seed(passphrase);
Self { Self {
@ -176,12 +188,63 @@ mod tests {
} }
#[test] #[test]
fn two_seeds_generated_same_from_same_mnemonic() { fn two_seeds_recovered_same_from_same_mnemonic() {
let mnemonic = "test_pass"; let passphrase = "test_pass";
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned()); // Generate a mnemonic with random entropy
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned()); let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
assert_eq!(seed_holder1.seed, seed_holder2.seed); // Recover from the same mnemonic
let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase);
assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed);
}
#[test]
fn new_mnemonic_generates_different_seeds_each_time() {
let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic("");
let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic("");
// Different entropy should produce different mnemonics and seeds
assert_ne!(mnemonic1.to_string(), mnemonic2.to_string());
assert_ne!(seed_holder1.seed, seed_holder2.seed);
}
#[test]
fn new_mnemonic_generates_24_word_phrase() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
// 256 bits of entropy produces a 24-word mnemonic
let word_count = mnemonic.to_string().split_whitespace().count();
assert_eq!(word_count, 24);
}
#[test]
fn new_mnemonic_produces_valid_seed_length() {
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
assert_eq!(seed_holder.seed.len(), 64);
}
#[test]
fn different_passphrases_produce_different_seeds() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a");
let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b");
// Same mnemonic but different passphrases should produce different seeds
assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed);
}
#[test]
fn empty_passphrase_is_deterministic() {
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
let seed1 = SeedHolder::from_mnemonic(&mnemonic, "");
let seed2 = SeedHolder::from_mnemonic(&mnemonic, "");
// Same mnemonic and passphrase should always produce the same seed
assert_eq!(seed1.seed, seed2.seed);
} }
} }

View File

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

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies] [dependencies]
nssa_core = { workspace = true, features = ["host"] } nssa_core = { workspace = true, features = ["host"] }
clock_core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
@ -19,7 +20,7 @@ sha2.workspace = true
rand.workspace = true rand.workspace = true
borsh.workspace = true borsh.workspace = true
hex.workspace = true hex.workspace = true
secp256k1 = "0.31.1" k256.workspace = true
risc0-binfmt = "3.0.2" risc0-binfmt = "3.0.2"
log.workspace = true log.workspace = true

View File

@ -5,7 +5,7 @@ use crate::{
NullifierSecretKey, SharedSecretKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
encryption::Ciphertext, encryption::Ciphertext,
program::{ProgramId, ProgramOutput}, program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -17,6 +17,7 @@ pub struct PrivacyPreservingCircuitInput {
/// - `0` - public account /// - `0` - public account
/// - `1` - private account with authentication /// - `1` - private account with authentication
/// - `2` - private account without authentication /// - `2` - private account without authentication
/// - `3` - private PDA account
pub visibility_mask: Vec<u8>, pub visibility_mask: Vec<u8>,
/// Public keys of private accounts. /// Public keys of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
@ -36,6 +37,8 @@ pub struct PrivacyPreservingCircuitOutput {
pub ciphertexts: Vec<Ciphertext>, pub ciphertexts: Vec<Ciphertext>,
pub new_commitments: Vec<Commitment>, pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub block_validity_window: BlockValidityWindow,
pub timestamp_validity_window: TimestampValidityWindow,
} }
#[cfg(feature = "host")] #[cfg(feature = "host")]
@ -101,6 +104,8 @@ mod tests {
), ),
[0xab; 32], [0xab; 32],
)], )],
block_validity_window: (1..).into(),
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
}; };
let bytes = output.to_bytes(); let bytes = output.to_bytes();
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap(); let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();

View File

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

View File

@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
use crate::{Commitment, account::AccountId}; use crate::{Commitment, account::AccountId};
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))] #[cfg_attr(any(feature = "host", test), derive(Hash))]
pub struct NullifierPublicKey(pub [u8; 32]); pub struct NullifierPublicKey(pub [u8; 32]);
impl From<&NullifierPublicKey> for AccountId { impl From<&NullifierPublicKey> for AccountId {
@ -55,7 +55,7 @@ pub type NullifierSecretKey = [u8; 32];
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[cfg_attr( #[cfg_attr(
any(feature = "host", test), any(feature = "host", test),
derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash) derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
)] )]
pub struct Nullifier(pub(super) [u8; 32]); pub struct Nullifier(pub(super) [u8; 32]);

View File

@ -1,9 +1,14 @@
use std::collections::HashSet; use std::collections::HashSet;
#[cfg(any(feature = "host", test))]
use borsh::{BorshDeserialize, BorshSerialize};
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::account::{Account, AccountId, AccountWithMetadata}; use crate::{
BlockId, NullifierPublicKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
};
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
@ -11,6 +16,8 @@ pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub type ProgramId = [u32; 8]; pub type ProgramId = [u32; 8];
pub type InstructionData = Vec<u32>; pub type InstructionData = Vec<u32>;
pub struct ProgramInput<T> { pub struct ProgramInput<T> {
pub self_program_id: ProgramId,
pub caller_program_id: Option<ProgramId>,
pub pre_states: Vec<AccountWithMetadata>, pub pre_states: Vec<AccountWithMetadata>,
pub instruction: T, pub instruction: T,
} }
@ -20,7 +27,7 @@ pub struct ProgramInput<T> {
/// Each program can derive up to `2^256` unique account IDs by choosing different /// Each program can derive up to `2^256` unique account IDs by choosing different
/// seeds. PDAs allow programs to control namespaced account identifiers without /// seeds. PDAs allow programs to control namespaced account identifiers without
/// collisions between programs. /// collisions between programs.
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PdaSeed([u8; 32]); pub struct PdaSeed([u8; 32]);
impl PdaSeed { impl PdaSeed {
@ -30,8 +37,10 @@ impl PdaSeed {
} }
} }
impl From<(&ProgramId, &PdaSeed)> for AccountId { impl AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self { /// Derives an [`AccountId`] for a public PDA from the program ID and seed.
#[must_use]
pub fn for_public_pda(program_id: &ProgramId, seed: &PdaSeed) -> Self {
use risc0_zkvm::sha::{Impl, Sha256 as _}; use risc0_zkvm::sha::{Impl, Sha256 as _};
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] = const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00"; b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
@ -39,9 +48,38 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId {
let mut bytes = [0; 96]; let mut bytes = [0; 96];
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX); bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
let program_id_bytes: &[u8] = let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]"); bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes); bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..].copy_from_slice(&value.1.0); bytes[64..].copy_from_slice(&seed.0);
Self::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
/// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier
/// public key.
///
/// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the
/// derivation, making the address unique per group of controllers sharing viewing keys.
#[must_use]
pub fn for_private_pda(
program_id: &ProgramId,
seed: &PdaSeed,
npk: &NullifierPublicKey,
) -> Self {
use risc0_zkvm::sha::{Impl, Sha256 as _};
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
let mut bytes = [0_u8; 128];
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..96].copy_from_slice(&seed.0);
bytes[96..128].copy_from_slice(&npk.to_byte_array());
Self::new( Self::new(
Impl::hash_bytes(&bytes) Impl::hash_bytes(&bytes)
.as_bytes() .as_bytes()
@ -58,6 +96,9 @@ pub struct ChainedCall {
pub pre_states: Vec<AccountWithMetadata>, pub pre_states: Vec<AccountWithMetadata>,
/// The instruction data to pass. /// The instruction data to pass.
pub instruction_data: InstructionData, pub instruction_data: InstructionData,
/// PDA seeds authorized for the callee. For each seed, the callee is authorized to
/// mutate the `AccountId` derived from `(caller_program_id, seed)`, regardless of
/// whether the account is public or private.
pub pda_seeds: Vec<PdaSeed>, pub pda_seeds: Vec<PdaSeed>,
} }
@ -89,11 +130,28 @@ impl ChainedCall {
/// A post state may optionally request that the executing program /// A post state may optionally request that the executing program
/// becomes the owner of the account (a “claim”). This is used to signal /// becomes the owner of the account (a “claim”). This is used to signal
/// that the program intends to take ownership of the account. /// 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))] #[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))]
pub struct AccountPostState { pub struct AccountPostState {
account: Account, 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. The program emits the
/// seed; the `AccountId` is derived from `(program_id, seed)`, regardless of whether the
/// account is public or private.
Pda(PdaSeed),
} }
impl AccountPostState { impl AccountPostState {
@ -103,7 +161,7 @@ impl AccountPostState {
pub const fn new(account: Account) -> Self { pub const fn new(account: Account) -> Self {
Self { Self {
account, account,
claim: false, claim: None,
} }
} }
@ -111,25 +169,27 @@ impl AccountPostState {
/// This indicates that the executing program intends to claim the /// This indicates that the executing program intends to claim the
/// account as its own and is allowed to mutate it. /// account as its own and is allowed to mutate it.
#[must_use] #[must_use]
pub const fn new_claimed(account: Account) -> Self { pub const fn new_claimed(account: Account, claim: Claim) -> Self {
Self { Self {
account, account,
claim: true, claim: Some(claim),
} }
} }
/// Creates a post state that requests ownership of the account /// Creates a post state that requests ownership of the account
/// if the account's program owner is the default program ID. /// if the account's program owner is the default program ID.
#[must_use] #[must_use]
pub fn new_claimed_if_default(account: Account) -> Self { pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self {
let claim = account.program_owner == DEFAULT_PROGRAM_ID; let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID;
Self { account, claim } Self {
account,
claim: is_default_owner.then_some(claim),
}
} }
/// Returns `true` if this post state requests that the account /// Returns whether this post state requires a claim.
/// be claimed (owned) by the executing program.
#[must_use] #[must_use]
pub const fn requires_claim(&self) -> bool { pub const fn required_claim(&self) -> Option<Claim> {
self.claim self.claim
} }
@ -140,6 +200,7 @@ impl AccountPostState {
} }
/// Returns the underlying account. /// Returns the underlying account.
#[must_use]
pub const fn account_mut(&mut self) -> &mut Account { pub const fn account_mut(&mut self) -> &mut Account {
&mut self.account &mut self.account
} }
@ -151,20 +212,214 @@ impl AccountPostState {
} }
} }
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<T> {
from: Option<T>,
to: Option<T>,
}
impl<T> ValidityWindow<T> {
/// Creates a window with no bounds.
#[must_use]
pub const fn new_unbounded() -> Self {
Self {
from: None,
to: None,
}
}
}
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(&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`.
fn check_window(&self) -> Result<(), InvalidWindow> {
if let (Some(from), Some(to)) = (self.from, self.to)
&& from >= to
{
return Err(InvalidWindow);
}
Ok(())
}
/// Inclusive lower bound. `None` means no lower bound.
#[must_use]
pub const fn start(&self) -> Option<T> {
self.from
}
/// Exclusive upper bound. `None` means no upper bound.
#[must_use]
pub const fn end(&self) -> Option<T> {
self.to
}
}
impl<T: Copy + PartialOrd> TryFrom<(Option<T>, Option<T>)> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: (Option<T>, Option<T>)) -> Result<Self, Self::Error> {
let this = Self {
from: value.0,
to: value.1,
};
this.check_window()?;
Ok(this)
}
}
impl<T: Copy + PartialOrd> TryFrom<std::ops::Range<T>> for ValidityWindow<T> {
type Error = InvalidWindow;
fn try_from(value: std::ops::Range<T>) -> Result<Self, Self::Error> {
(Some(value.start), Some(value.end)).try_into()
}
}
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,
}
}
}
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),
}
}
}
impl<T> From<std::ops::RangeFull> for ValidityWindow<T> {
fn from(_: std::ops::RangeFull) -> Self {
Self::new_unbounded()
}
}
#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
#[error("Invalid window")]
pub struct InvalidWindow;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
#[must_use = "ProgramOutput does nothing unless written"]
pub struct ProgramOutput { pub struct ProgramOutput {
/// The program ID of the program that produced this output.
pub self_program_id: ProgramId,
/// The program ID of the caller that invoked this program via a chained call,
/// or `None` if this is a top-level call.
pub caller_program_id: Option<ProgramId>,
/// The instruction data the program received to produce this output. /// The instruction data the program received to produce this output.
pub instruction_data: InstructionData, pub instruction_data: InstructionData,
/// The account pre states the program received to produce this output. /// The account pre states the program received to produce this output.
pub pre_states: Vec<AccountWithMetadata>, pub pre_states: Vec<AccountWithMetadata>,
/// The account post states the program execution produced.
pub post_states: Vec<AccountPostState>, pub post_states: Vec<AccountPostState>,
/// The list of chained calls to other programs.
pub chained_calls: Vec<ChainedCall>, pub chained_calls: Vec<ChainedCall>,
/// 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 {
pub const fn new(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) -> Self {
Self {
self_program_id,
caller_program_id,
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
block_validity_window: ValidityWindow::new_unbounded(),
timestamp_validity_window: ValidityWindow::new_unbounded(),
}
}
pub fn write(self) {
env::commit(&self);
}
pub fn with_chained_calls(mut self, chained_calls: Vec<ChainedCall>) -> Self {
self.chained_calls = chained_calls;
self
}
/// 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 block ID validity window from a fallible range conversion (`1..5`).
/// Returns `Err` if the range is empty.
pub fn try_with_block_validity_window<
W: TryInto<BlockValidityWindow, Error = InvalidWindow>,
>(
mut self,
window: W,
) -> Result<Self, InvalidWindow> {
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)
}
} }
/// Representation of a number as `lo + hi * 2^128`. /// Representation of a number as `lo + hi * 2^128`.
#[derive(PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
struct WrappedBalanceSum { pub struct WrappedBalanceSum {
lo: u128, lo: u128,
hi: u128, hi: u128,
} }
@ -174,7 +429,7 @@ impl WrappedBalanceSum {
/// ///
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not /// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
/// expected in practical scenarios. /// expected in practical scenarios.
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> { pub fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
let mut wrapped = Self { lo: 0, hi: 0 }; let mut wrapped = Self { lo: 0, hi: 0 };
for balance in balances { for balance in balances {
@ -189,29 +444,107 @@ impl WrappedBalanceSum {
} }
} }
impl std::fmt::Display for WrappedBalanceSum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.hi == 0 {
write!(f, "{}", self.lo)
} else {
write!(f, "{} * 2^128 + {}", self.hi, self.lo)
}
}
}
impl From<u128> for WrappedBalanceSum {
fn from(value: u128) -> Self {
Self { lo: value, hi: 0 }
}
}
#[derive(thiserror::Error, Debug)]
pub enum ExecutionValidationError {
#[error("Pre-state account IDs are not unique")]
PreStateAccountIdsNotUnique,
#[error(
"Pre-state and post-state lengths do not match: pre-state length {pre_state_length}, post-state length {post_state_length}"
)]
MismatchedPreStatePostStateLength {
pre_state_length: usize,
post_state_length: usize,
},
#[error("Unallowed modification of nonce for account {account_id}")]
ModifiedNonce { account_id: AccountId },
#[error("Unallowed modification of program owner for account {account_id}")]
ModifiedProgramOwner { account_id: AccountId },
#[error(
"Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner"
)]
UnauthorizedBalanceDecrease {
account_id: AccountId,
owner_program_id: ProgramId,
executing_program_id: ProgramId,
},
#[error(
"Unauthorized modification of data for account {account_id} which is not default and not owned by executing program {executing_program_id:?}"
)]
UnauthorizedDataModification {
account_id: AccountId,
executing_program_id: ProgramId,
},
#[error(
"Post-state for account {account_id} has default program owner but pre-state was not default"
)]
NonDefaultAccountWithDefaultOwner { account_id: AccountId },
#[error("Total balance across accounts overflowed 2^256 - 1")]
BalanceSumOverflow,
#[error(
"Total balance across accounts is not preserved: total balance in pre-states {total_balance_pre_states}, total balance in post-states {total_balance_post_states}"
)]
MismatchedTotalBalance {
total_balance_pre_states: WrappedBalanceSum,
total_balance_post_states: WrappedBalanceSum,
},
}
/// Computes the set of public-PDA `AccountId`s the callee is authorized to mutate.
///
/// Returns only public-form derivations, suitable for contexts where all accounts are public
/// (e.g. the public-execution path). The privacy circuit must additionally check each mask-3
/// `pre_state` against [`AccountId::for_private_pda`] with the supplied npk for that
/// `pre_state`.
#[must_use] #[must_use]
pub fn compute_authorized_pdas( pub fn compute_public_authorized_pdas(
caller_program_id: Option<ProgramId>, caller_program_id: Option<ProgramId>,
pda_seeds: &[PdaSeed], pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> { ) -> HashSet<AccountId> {
caller_program_id let Some(caller) = caller_program_id else {
.map(|caller_program_id| { return HashSet::new();
pda_seeds };
.iter() pda_seeds
.map(|pda_seed| AccountId::from((&caller_program_id, pda_seed))) .iter()
.collect() .map(|seed| AccountId::for_public_pda(&caller, seed))
}) .collect()
.unwrap_or_default()
} }
/// Reads the NSSA inputs from the guest environment. /// Reads the NSSA inputs from the guest environment.
#[must_use] #[must_use]
pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) { pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) {
let self_program_id: ProgramId = env::read();
let caller_program_id: Option<ProgramId> = env::read();
let pre_states: Vec<AccountWithMetadata> = env::read(); let pre_states: Vec<AccountWithMetadata> = env::read();
let instruction_words: InstructionData = env::read(); let instruction_words: InstructionData = env::read();
let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap();
( (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction, instruction,
}, },
@ -219,66 +552,45 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
) )
} }
pub fn write_nssa_outputs(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls: Vec::new(),
};
env::commit(&output);
}
pub fn write_nssa_outputs_with_chained_call(
instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>,
chained_calls: Vec<ChainedCall>,
) {
let output = ProgramOutput {
instruction_data,
pre_states,
post_states,
chained_calls,
};
env::commit(&output);
}
/// Validates well-behaved program execution. /// Validates well-behaved program execution.
/// ///
/// # Parameters /// # Parameters
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata. /// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
/// - `post_states`: The list of resulting accounts after executing the program logic. /// - `post_states`: The list of resulting accounts after executing the program logic.
/// - `executing_program_id`: The identifier of the program that was executed. /// - `executing_program_id`: The identifier of the program that was executed.
#[must_use]
pub fn validate_execution( pub fn validate_execution(
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
post_states: &[AccountPostState], post_states: &[AccountPostState],
executing_program_id: ProgramId, executing_program_id: ProgramId,
) -> bool { ) -> Result<(), ExecutionValidationError> {
// 1. Check account ids are all different // 1. Check account ids are all different
if !validate_uniqueness_of_account_ids(pre_states) { if !validate_uniqueness_of_account_ids(pre_states) {
return false; return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
} }
// 2. Lengths must match // 2. Lengths must match
if pre_states.len() != post_states.len() { if pre_states.len() != post_states.len() {
return false; return Err(
ExecutionValidationError::MismatchedPreStatePostStateLength {
pre_state_length: pre_states.len(),
post_state_length: post_states.len(),
},
);
} }
for (pre, post) in pre_states.iter().zip(post_states) { for (pre, post) in pre_states.iter().zip(post_states) {
// 3. Nonce must remain unchanged // 3. Nonce must remain unchanged
if pre.account.nonce != post.account.nonce { if pre.account.nonce != post.account.nonce {
return false; return Err(ExecutionValidationError::ModifiedNonce {
account_id: pre.account_id,
});
} }
// 4. Program ownership changes are not allowed // 4. Program ownership changes are not allowed
if pre.account.program_owner != post.account.program_owner { if pre.account.program_owner != post.account.program_owner {
return false; return Err(ExecutionValidationError::ModifiedProgramOwner {
account_id: pre.account_id,
});
} }
let account_program_owner = pre.account.program_owner; let account_program_owner = pre.account.program_owner;
@ -287,7 +599,11 @@ pub fn validate_execution(
if post.account.balance < pre.account.balance if post.account.balance < pre.account.balance
&& account_program_owner != executing_program_id && account_program_owner != executing_program_id
{ {
return false; return Err(ExecutionValidationError::UnauthorizedBalanceDecrease {
account_id: pre.account_id,
owner_program_id: account_program_owner,
executing_program_id,
});
} }
// 6. Data changes only allowed if owned by executing program or if account pre state has // 6. Data changes only allowed if owned by executing program or if account pre state has
@ -296,13 +612,20 @@ pub fn validate_execution(
&& pre.account != Account::default() && pre.account != Account::default()
&& account_program_owner != executing_program_id && account_program_owner != executing_program_id
{ {
return false; return Err(ExecutionValidationError::UnauthorizedDataModification {
account_id: pre.account_id,
executing_program_id,
});
} }
// 7. If a post state has default program owner, the pre state must have been a default // 7. If a post state has default program owner, the pre state must have been a default
// account // account
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
return false; return Err(
ExecutionValidationError::NonDefaultAccountWithDefaultOwner {
account_id: pre.account_id,
},
);
} }
} }
@ -311,20 +634,23 @@ pub fn validate_execution(
let Some(total_balance_pre_states) = let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else { else {
return false; return Err(ExecutionValidationError::BalanceSumOverflow);
}; };
let Some(total_balance_post_states) = let Some(total_balance_post_states) =
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
else { else {
return false; return Err(ExecutionValidationError::BalanceSumOverflow);
}; };
if total_balance_pre_states != total_balance_post_states { if total_balance_pre_states != total_balance_post_states {
return false; return Err(ExecutionValidationError::MismatchedTotalBalance {
total_balance_pre_states,
total_balance_post_states,
});
} }
true Ok(())
} }
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
@ -342,6 +668,135 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
mod tests { mod tests {
use super::*; use super::*;
#[test]
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<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<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<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::<u64>::try_from((Some(5), Some(5))).is_err());
}
#[test]
fn validity_window_inverted_bounds_are_invalid() {
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
}
#[test]
fn validity_window_getters_match_construction() {
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<u64> = ValidityWindow::new_unbounded();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn validity_window_from_range() {
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::<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::<u64>::try_from(from..to).is_err());
}
#[test]
fn validity_window_from_range_from() {
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<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<u64> = (..).into();
assert_eq!(w.start(), None);
assert_eq!(w.end(), None);
}
#[test]
fn program_output_try_with_block_validity_window_range() {
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
.try_with_block_validity_window(10_u64..100)
.unwrap();
assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), Some(100));
}
#[test]
fn program_output_with_block_validity_window_range_from() {
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, 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_block_validity_window_range_to() {
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, 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_block_validity_window_empty_range_fails() {
let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
.try_with_block_validity_window(5_u64..5);
assert!(result.is_err());
}
#[test] #[test]
fn post_state_new_with_claim_constructor() { fn post_state_new_with_claim_constructor() {
let account = Account { let account = Account {
@ -351,10 +806,10 @@ mod tests {
nonce: 10_u128.into(), 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_eq!(account, account_post_state.account);
assert!(account_post_state.requires_claim()); assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
} }
#[test] #[test]
@ -369,7 +824,7 @@ mod tests {
let account_post_state = AccountPostState::new(account.clone()); let account_post_state = AccountPostState::new(account.clone());
assert_eq!(account, account_post_state.account); assert_eq!(account, account_post_state.account);
assert!(!account_post_state.requires_claim()); assert!(account_post_state.required_claim().is_none());
} }
#[test] #[test]
@ -386,4 +841,108 @@ mod tests {
assert_eq!(account_post_state.account(), &account); assert_eq!(account_post_state.account(), &account);
assert_eq!(account_post_state.account_mut(), &mut account); assert_eq!(account_post_state.account_mut(), &mut account);
} }
// ---- AccountId::for_private_pda tests ----
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
/// `(program_id, seed, npk)` triple. Any change to `PRIVATE_PDA_PREFIX`, byte ordering,
/// or the underlying hash breaks this test.
#[test]
fn for_private_pda_matches_pinned_value() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let expected = AccountId::new([
132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53,
133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45,
]);
assert_eq!(
AccountId::for_private_pda(&program_id, &seed, &npk),
expected
);
}
/// Two groups with different viewing keys at the same (program, seed) get different addresses.
#[test]
fn for_private_pda_differs_for_different_npk() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk_a = NullifierPublicKey([3; 32]);
let npk_b = NullifierPublicKey([4; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed, &npk_a),
AccountId::for_private_pda(&program_id, &seed, &npk_b),
);
}
/// Different seeds produce different addresses, even with the same program and npk.
#[test]
fn for_private_pda_differs_for_different_seed() {
let program_id: ProgramId = [1; 8];
let seed_a = PdaSeed::new([2; 32]);
let seed_b = PdaSeed::new([5; 32]);
let npk = NullifierPublicKey([3; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed_a, &npk),
AccountId::for_private_pda(&program_id, &seed_b, &npk),
);
}
/// Different programs produce different addresses, even with the same seed and npk.
#[test]
fn for_private_pda_differs_for_different_program_id() {
let program_id_a: ProgramId = [1; 8];
let program_id_b: ProgramId = [9; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id_a, &seed, &npk),
AccountId::for_private_pda(&program_id_b, &seed, &npk),
);
}
/// A private PDA at the same (program, seed) has a different address than a public PDA,
/// because the private formula uses a different prefix and includes npk.
#[test]
fn for_private_pda_differs_from_public_pda() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let public_id = AccountId::for_public_pda(&program_id, &seed);
assert_ne!(private_id, public_id);
}
/// A private PDA address differs from a standard private account address at the same `npk`,
/// because the private PDA formula includes `program_id` and `seed`.
#[test]
fn for_private_pda_differs_from_standard_private() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let standard_private_id = AccountId::from(&npk);
assert_ne!(private_pda_id, standard_private_id);
}
// ---- compute_public_authorized_pdas tests ----
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.
#[test]
fn compute_public_authorized_pdas_with_seeds() {
let caller: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
let expected = AccountId::for_public_pda(&caller, &seed);
assert!(result.contains(&expected));
assert_eq!(result.len(), 1);
}
/// With no caller (top-level call), the result is always empty.
#[test]
fn compute_public_authorized_pdas_no_caller_returns_empty() {
let seed = PdaSeed::new([2; 32]);
let result = compute_public_authorized_pdas(None, &[seed]);
assert!(result.is_empty());
}
} }

View File

@ -1,12 +1,16 @@
use std::io; use std::io;
use nssa_core::{
account::{Account, AccountId},
program::ProgramId,
};
use thiserror::Error; use thiserror::Error;
#[macro_export] #[macro_export]
macro_rules! ensure { macro_rules! ensure {
($cond:expr, $err:expr) => { ($cond:expr, $err:expr) => {
if !$cond { if !$cond {
return Err($err); return Err($err.into());
} }
}; };
} }
@ -17,7 +21,7 @@ pub enum NssaError {
InvalidInput(String), InvalidInput(String),
#[error("Program violated execution rules")] #[error("Program violated execution rules")]
InvalidProgramBehavior, InvalidProgramBehavior(#[from] InvalidProgramBehaviorError),
#[error("Serialization error: {0}")] #[error("Serialization error: {0}")]
InstructionSerializationError(String), InstructionSerializationError(String),
@ -29,15 +33,18 @@ pub enum NssaError {
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Invalid Public Key")] #[error("Invalid Public Key")]
InvalidPublicKey(#[source] secp256k1::Error), InvalidPublicKey(#[source] k256::schnorr::Error),
#[error("Risc0 error: {0}")] #[error("Invalid hex for public key")]
InvalidHexPublicKey(#[source] hex::FromHexError),
#[error("Failed to write program input: {0}")]
ProgramWriteInputFailed(String), ProgramWriteInputFailed(String),
#[error("Risc0 error: {0}")] #[error("Failed to execute program: {0}")]
ProgramExecutionFailed(String), ProgramExecutionFailed(String),
#[error("Risc0 error: {0}")] #[error("Failed to prove program: {0}")]
ProgramProveFailed(String), ProgramProveFailed(String),
#[error("Invalid transaction: {0}")] #[error("Invalid transaction: {0}")]
@ -69,6 +76,64 @@ pub enum NssaError {
#[error("Max account nonce reached")] #[error("Max account nonce reached")]
MaxAccountNonceReached, MaxAccountNonceReached,
#[error("Execution outside of the validity window")]
OutOfValidityWindow,
}
#[derive(Error, Debug)]
pub enum InvalidProgramBehaviorError {
#[error(
"Inconsistent pre-state for account {account_id} : expected {expected:?}, actual {actual:?}"
)]
InconsistentAccountPreState {
account_id: AccountId,
// Boxed to reduce the size of the error type
expected: Box<Account>,
actual: Box<Account>,
},
#[error(
"Inconsistent authorization for account {account_id} : expected {expected_authorization}, actual {actual_authorization}"
)]
InconsistentAccountAuthorization {
account_id: AccountId,
expected_authorization: bool,
actual_authorization: bool,
},
#[error("Program ID mismatch: expected {expected:?}, actual {actual:?}")]
MismatchedProgramId {
expected: ProgramId,
actual: ProgramId,
},
#[error("Caller program ID mismatch: expected {expected:?}, actual {actual:?}")]
MismatchedCallerProgramId {
expected: Option<ProgramId>,
actual: Option<ProgramId>,
},
#[error(transparent)]
ExecutionValidationFailed(#[from] nssa_core::program::ExecutionValidationError),
#[error("Trying to claim account {account_id} which is not default")]
ClaimedNonDefaultAccount { account_id: AccountId },
#[error("Trying to claim account {account_id} which is not authorized")]
ClaimedUnauthorizedAccount { account_id: AccountId },
#[error("PDA claim mismatch: expected {expected:?}, actual {actual:?}")]
MismatchedPdaClaim {
expected: AccountId,
actual: AccountId,
},
#[error("Default account {account_id} was modified without being claimed")]
DefaultAccountModifiedWithoutClaim { account_id: AccountId },
#[error("Called program {program_id:?} which is not listed in dependencies")]
UndeclaredProgramDependency { program_id: ProgramId },
} }
#[cfg(test)] #[cfg(test)]

View File

@ -16,7 +16,11 @@ pub use program_deployment_transaction::ProgramDeploymentTransaction;
pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
pub use public_transaction::PublicTransaction; pub use public_transaction::PublicTransaction;
pub use signature::{PrivateKey, PublicKey, Signature}; pub use signature::{PrivateKey, PublicKey, Signature};
pub use state::V03State; pub use state::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
};
pub use validated_state_diff::ValidatedStateDiff;
pub mod encoding; pub mod encoding;
pub mod error; pub mod error;
@ -27,6 +31,7 @@ pub mod program_deployment_transaction;
pub mod public_transaction; pub mod public_transaction;
mod signature; mod signature;
mod state; mod state;
mod validated_state_diff;
pub mod program_methods { pub mod program_methods {
include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs"));

View File

@ -10,7 +10,7 @@ use nssa_core::{
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
use crate::{ use crate::{
error::NssaError, error::{InvalidProgramBehaviorError, NssaError},
program::Program, program::Program,
program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}, program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID},
state::MAX_NUMBER_CHAINED_CALLS, state::MAX_NUMBER_CHAINED_CALLS,
@ -87,15 +87,16 @@ pub fn execute_and_prove(
pda_seeds: vec![], pda_seeds: vec![],
}; };
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program)]); let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
let mut chain_calls_counter = 0; let mut chain_calls_counter = 0;
while let Some((chained_call, program)) = chained_calls.pop_front() { while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded); return Err(NssaError::MaxChainedCallsDepthExceeded);
} }
let inner_receipt = execute_and_prove_program( let inner_receipt = execute_and_prove_program(
program, program,
caller_program_id,
&chained_call.pre_states, &chained_call.pre_states,
&chained_call.instruction_data, &chained_call.instruction_data,
)?; )?;
@ -112,10 +113,12 @@ pub fn execute_and_prove(
env_builder.add_assumption(inner_receipt); env_builder.add_assumption(inner_receipt);
for new_call in program_output.chained_calls.into_iter().rev() { for new_call in program_output.chained_calls.into_iter().rev() {
let next_program = dependencies let next_program = dependencies.get(&new_call.program_id).ok_or(
.get(&new_call.program_id) InvalidProgramBehaviorError::UndeclaredProgramDependency {
.ok_or(NssaError::InvalidProgramBehavior)?; program_id: new_call.program_id,
chained_calls.push_front((new_call, next_program)); },
)?;
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
} }
chain_calls_counter = chain_calls_counter chain_calls_counter = chain_calls_counter
@ -153,12 +156,19 @@ pub fn execute_and_prove(
fn execute_and_prove_program( fn execute_and_prove_program(
program: &Program, program: &Program,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData, instruction_data: &InstructionData,
) -> Result<Receipt, NssaError> { ) -> Result<Receipt, NssaError> {
// Write inputs to the program // Write inputs to the program
let mut env_builder = ExecutorEnv::builder(); let mut env_builder = ExecutorEnv::builder();
Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; Program::write_inputs(
program.id(),
caller_program_id,
pre_states,
instruction_data,
&mut env_builder,
)?;
let env = env_builder.build().unwrap(); let env = env_builder.build().unwrap();
// Prove the program // Prove the program
@ -174,12 +184,13 @@ mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
use nssa_core::{ use nssa_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
}; };
use super::*; use super::*;
use crate::{ use crate::{
error::NssaError,
privacy_preserving_transaction::circuit::execute_and_prove, privacy_preserving_transaction::circuit::execute_and_prove,
program::Program, program::Program,
state::{ state::{
@ -364,4 +375,46 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!(recipient_post, expected_private_account_2); assert_eq!(recipient_post, expected_private_account_2);
} }
#[test]
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
let account_keys = test_private_account_keys_1();
let pre = AccountWithMetadata::new(
Account::default(),
false,
AccountId::from(&account_keys.npk()),
);
let validity_window_chain_caller = Program::validity_window_chain_caller();
let validity_window = Program::validity_window();
let instruction = Program::serialize_instruction((
Some(1_u64),
Some(4_u64),
validity_window.id(),
Some(4_u64),
Some(7_u64),
))
.unwrap();
let esk = [3; 32];
let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk());
let program_with_deps = ProgramWithDependencies::new(
validity_window_chain_caller,
[(validity_window.id(), validity_window)].into(),
);
let result = execute_and_prove(
vec![pre],
instruction,
vec![2],
vec![(account_keys.npk(), shared_secret)],
vec![],
vec![None],
&program_with_deps,
);
assert!(matches!(result, Err(NssaError::CircuitProvingError(_))));
}
} }

View File

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

View File

@ -1,21 +1,10 @@
use std::{ use std::collections::HashSet;
collections::{HashMap, HashSet},
hash::Hash,
};
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{ use nssa_core::account::AccountId;
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
account::{Account, AccountWithMetadata},
};
use sha2::{Digest as _, digest::FixedOutput as _}; use sha2::{Digest as _, digest::FixedOutput as _};
use super::{message::Message, witness_set::WitnessSet}; use super::{message::Message, witness_set::WitnessSet};
use crate::{
AccountId, V03State,
error::NssaError,
privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData},
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PrivacyPreservingTransaction { pub struct PrivacyPreservingTransaction {
@ -32,102 +21,6 @@ impl PrivacyPreservingTransaction {
} }
} }
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = &self.message;
let witness_set = &self.witness_set;
// 1. Commitments or nullifiers are non empty
if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() {
return Err(NssaError::InvalidInput(
"Empty commitments and empty nullifiers found in message".into(),
));
}
// 2. Check there are no duplicate account_ids in the public_account_ids list.
if n_unique(&message.public_account_ids) != message.public_account_ids.len() {
return Err(NssaError::InvalidInput(
"Duplicate account_ids found in message".into(),
));
}
// Check there are no duplicate nullifiers in the new_nullifiers list
if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() {
return Err(NssaError::InvalidInput(
"Duplicate nullifiers found in message".into(),
));
}
// Check there are no duplicate commitments in the new_commitments list
if n_unique(&message.new_commitments) != message.new_commitments.len() {
return Err(NssaError::InvalidInput(
"Duplicate commitments found in message".into(),
));
}
// 3. Nonce checks and Valid signatures
// Check exactly one nonce is provided for each signature
if message.nonces.len() != witness_set.signatures_and_public_keys.len() {
return Err(NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
));
}
// Check the signatures are valid
if !witness_set.signatures_are_valid_for(message) {
return Err(NssaError::InvalidInput(
"Invalid signature for given message and public key".into(),
));
}
let signer_account_ids = self.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
if current_nonce != *nonce {
return Err(NssaError::InvalidInput("Nonce mismatch".into()));
}
}
// Build pre_states for proof verification
let public_pre_states: Vec<_> = message
.public_account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
// 4. Proof verification
check_privacy_preserving_circuit_proof_is_valid(
&witness_set.proof,
&public_pre_states,
&message.public_post_states,
&message.encrypted_private_post_states,
&message.new_commitments,
&message.new_nullifiers,
)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
// 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
Ok(message
.public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect())
}
#[must_use] #[must_use]
pub const fn message(&self) -> &Message { pub const fn message(&self) -> &Message {
&self.message &self.message
@ -166,36 +59,6 @@ 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)],
) -> 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
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: new_commitments.to_vec(),
new_nullifiers: new_nullifiers.to_vec(),
};
proof
.is_valid_for(&output)
.then_some(())
.ok_or(NssaError::InvalidPrivacyPreservingProof)
}
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
let set: HashSet<&T> = data.iter().collect();
set.len()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -8,7 +8,11 @@ use serde::Serialize;
use crate::{ use crate::{
error::NssaError, error::NssaError,
program_methods::{AMM_ELF, AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF}, program_methods::{
AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, TOKEN_ELF, TOKEN_ID,
},
}; };
/// Maximum number of cycles for a public execution. /// Maximum number of cycles for a public execution.
@ -50,13 +54,20 @@ impl Program {
pub(crate) fn execute( pub(crate) fn execute(
&self, &self,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData, instruction_data: &InstructionData,
) -> Result<ProgramOutput, NssaError> { ) -> Result<ProgramOutput, NssaError> {
// Write inputs to the program // Write inputs to the program
let mut env_builder = ExecutorEnv::builder(); let mut env_builder = ExecutorEnv::builder();
env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION)); env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION));
Self::write_inputs(pre_states, instruction_data, &mut env_builder)?; Self::write_inputs(
self.id,
caller_program_id,
pre_states,
instruction_data,
&mut env_builder,
)?;
let env = env_builder.build().unwrap(); let env = env_builder.build().unwrap();
// Execute the program (without proving) // Execute the program (without proving)
@ -76,34 +87,66 @@ impl Program {
/// Writes inputs to `env_builder` in the order expected by the programs. /// Writes inputs to `env_builder` in the order expected by the programs.
pub(crate) fn write_inputs( pub(crate) fn write_inputs(
program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
instruction_data: &[u32], instruction_data: &[u32],
env_builder: &mut ExecutorEnvBuilder, env_builder: &mut ExecutorEnvBuilder,
) -> Result<(), NssaError> { ) -> Result<(), NssaError> {
env_builder
.write(&program_id)
.map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?;
env_builder
.write(&caller_program_id)
.map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?;
let pre_states = pre_states.to_vec(); let pre_states = pre_states.to_vec();
env_builder env_builder
.write(&(pre_states, instruction_data)) .write(&pre_states)
.map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?;
env_builder
.write(&instruction_data)
.map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?;
Ok(()) Ok(())
} }
#[must_use] #[must_use]
pub fn authenticated_transfer_program() -> Self { pub fn authenticated_transfer_program() -> Self {
// This unwrap won't panic since the `AUTHENTICATED_TRANSFER_ELF` comes from risc0 build of Self {
// `program_methods` id: AUTHENTICATED_TRANSFER_ID,
Self::new(AUTHENTICATED_TRANSFER_ELF.to_vec()).unwrap() elf: AUTHENTICATED_TRANSFER_ELF.to_vec(),
}
} }
#[must_use] #[must_use]
pub fn token() -> Self { pub fn token() -> Self {
// This unwrap won't panic since the `TOKEN_ELF` comes from risc0 build of Self {
// `program_methods` id: TOKEN_ID,
Self::new(TOKEN_ELF.to_vec()).unwrap() elf: TOKEN_ELF.to_vec(),
}
} }
#[must_use] #[must_use]
pub fn amm() -> Self { pub fn amm() -> Self {
Self::new(AMM_ELF.to_vec()).expect("The AMM program must be a valid Risc0 program") Self {
id: AMM_ID,
elf: AMM_ELF.to_vec(),
}
}
#[must_use]
pub fn clock() -> Self {
Self {
id: CLOCK_ID,
elf: CLOCK_ELF.to_vec(),
}
}
#[must_use]
pub fn ata() -> Self {
Self {
id: ASSOCIATED_TOKEN_ACCOUNT_ID,
elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(),
}
} }
} }
@ -111,16 +154,19 @@ impl Program {
impl Program { impl Program {
#[must_use] #[must_use]
pub fn pinata() -> Self { pub fn pinata() -> Self {
// This unwrap won't panic since the `PINATA_ELF` comes from risc0 build of Self {
// `program_methods` id: PINATA_ID,
Self::new(PINATA_ELF.to_vec()).unwrap() elf: PINATA_ELF.to_vec(),
}
} }
#[must_use] #[must_use]
#[expect(clippy::non_ascii_literal, reason = "More readable")]
pub fn pinata_token() -> Self { pub fn pinata_token() -> Self {
use crate::program_methods::PINATA_TOKEN_ELF; use crate::program_methods::{PINATA_TOKEN_ELF, PINATA_TOKEN_ID};
Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file") Self {
id: PINATA_TOKEN_ID,
elf: PINATA_TOKEN_ELF.to_vec(),
}
} }
} }
@ -131,8 +177,9 @@ mod tests {
use crate::{ use crate::{
program::Program, program::Program,
program_methods::{ program_methods::{
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID,
TOKEN_ELF, TOKEN_ID, AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF,
PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID,
}, },
}; };
@ -245,6 +292,36 @@ mod tests {
} }
} }
#[must_use]
pub fn pda_claimer() -> Self {
use test_program_methods::{PDA_CLAIMER_ELF, PDA_CLAIMER_ID};
Self {
id: PDA_CLAIMER_ID,
elf: PDA_CLAIMER_ELF.to_vec(),
}
}
#[must_use]
pub fn private_pda_delegator() -> Self {
use test_program_methods::{PRIVATE_PDA_DELEGATOR_ELF, PRIVATE_PDA_DELEGATOR_ID};
Self {
id: PRIVATE_PDA_DELEGATOR_ID,
elf: PRIVATE_PDA_DELEGATOR_ELF.to_vec(),
}
}
#[must_use]
pub fn two_pda_claimer() -> Self {
use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID};
Self {
id: TWO_PDA_CLAIMER_ID,
elf: TWO_PDA_CLAIMER_ELF.to_vec(),
}
}
#[must_use] #[must_use]
pub fn changer_claimer() -> Self { pub fn changer_claimer() -> Self {
use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID};
@ -265,6 +342,16 @@ mod tests {
} }
} }
#[must_use]
pub fn auth_asserting_noop() -> Self {
use test_program_methods::{AUTH_ASSERTING_NOOP_ELF, AUTH_ASSERTING_NOOP_ID};
Self {
id: AUTH_ASSERTING_NOOP_ID,
elf: AUTH_ASSERTING_NOOP_ELF.to_vec(),
}
}
#[must_use] #[must_use]
pub fn malicious_authorization_changer() -> Self { pub fn malicious_authorization_changer() -> Self {
use test_program_methods::{ use test_program_methods::{
@ -279,10 +366,71 @@ mod tests {
#[must_use] #[must_use]
pub fn modified_transfer_program() -> Self { pub fn modified_transfer_program() -> Self {
use test_program_methods::MODIFIED_TRANSFER_ELF; use test_program_methods::{MODIFIED_TRANSFER_ELF, MODIFIED_TRANSFER_ID};
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of Self {
// `program_methods` id: MODIFIED_TRANSFER_ID,
Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() elf: MODIFIED_TRANSFER_ELF.to_vec(),
}
}
#[must_use]
pub fn validity_window() -> Self {
use test_program_methods::{VALIDITY_WINDOW_ELF, VALIDITY_WINDOW_ID};
Self {
id: VALIDITY_WINDOW_ID,
elf: VALIDITY_WINDOW_ELF.to_vec(),
}
}
#[must_use]
pub fn validity_window_chain_caller() -> Self {
use test_program_methods::{
VALIDITY_WINDOW_CHAIN_CALLER_ELF, VALIDITY_WINDOW_CHAIN_CALLER_ID,
};
Self {
id: VALIDITY_WINDOW_CHAIN_CALLER_ID,
elf: VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec(),
}
}
#[must_use]
pub fn flash_swap_initiator() -> Self {
use test_program_methods::FLASH_SWAP_INITIATOR_ELF;
Self::new(FLASH_SWAP_INITIATOR_ELF.to_vec())
.expect("flash_swap_initiator must be a valid Risc0 program")
}
#[must_use]
pub fn flash_swap_callback() -> Self {
use test_program_methods::FLASH_SWAP_CALLBACK_ELF;
Self::new(FLASH_SWAP_CALLBACK_ELF.to_vec())
.expect("flash_swap_callback must be a valid Risc0 program")
}
#[must_use]
pub fn malicious_self_program_id() -> Self {
use test_program_methods::MALICIOUS_SELF_PROGRAM_ID_ELF;
Self::new(MALICIOUS_SELF_PROGRAM_ID_ELF.to_vec())
.expect("malicious_self_program_id must be a valid Risc0 program")
}
#[must_use]
pub fn malicious_caller_program_id() -> Self {
use test_program_methods::MALICIOUS_CALLER_PROGRAM_ID_ELF;
Self::new(MALICIOUS_CALLER_PROGRAM_ID_ELF.to_vec())
.expect("malicious_caller_program_id must be a valid Risc0 program")
}
#[must_use]
pub fn time_locked_transfer() -> Self {
use test_program_methods::TIME_LOCKED_TRANSFER_ELF;
Self::new(TIME_LOCKED_TRANSFER_ELF.to_vec()).unwrap()
}
#[must_use]
pub fn pinata_cooldown() -> Self {
use test_program_methods::PINATA_COOLDOWN_ELF;
Self::new(PINATA_COOLDOWN_ELF.to_vec()).unwrap()
} }
} }
@ -311,7 +459,7 @@ mod tests {
..Account::default() ..Account::default()
}; };
let program_output = program let program_output = program
.execute(&[sender, recipient], &instruction_data) .execute(None, &[sender, recipient], &instruction_data)
.unwrap(); .unwrap();
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
@ -333,4 +481,21 @@ mod tests {
assert_eq!(pinata_program.id, PINATA_ID); assert_eq!(pinata_program.id, PINATA_ID);
assert_eq!(pinata_program.elf, PINATA_ELF); assert_eq!(pinata_program.elf, PINATA_ELF);
} }
#[test]
fn builtin_program_ids_match_elfs() {
let cases: &[(&[u8], [u32; 8])] = &[
(AMM_ELF, AMM_ID),
(AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID),
(ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID),
(CLOCK_ELF, CLOCK_ID),
(PINATA_ELF, PINATA_ID),
(PINATA_TOKEN_ELF, PINATA_TOKEN_ID),
(TOKEN_ELF, TOKEN_ID),
];
for (elf, expected_id) in cases {
let program = Program::new(elf.to_vec()).unwrap();
assert_eq!(program.id(), *expected_id);
}
}
} }

View File

@ -2,9 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::account::AccountId; use nssa_core::account::AccountId;
use sha2::{Digest as _, digest::FixedOutput as _}; use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{ use crate::program_deployment_transaction::message::Message;
V03State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct ProgramDeploymentTransaction { pub struct ProgramDeploymentTransaction {
@ -22,19 +20,6 @@ impl ProgramDeploymentTransaction {
self.message self.message
} }
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
) -> Result<Program, NssaError> {
// TODO: remove clone
let program = Program::new(self.message.bytecode.clone())?;
if state.programs().contains_key(&program.id()) {
Err(NssaError::ProgramAlreadyExists)
} else {
Ok(program)
}
}
#[must_use] #[must_use]
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
let bytes = self.to_bytes(); let bytes = self.to_bytes();

View File

@ -1,19 +1,10 @@
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::HashSet;
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use log::debug; use nssa_core::account::AccountId;
use nssa_core::{
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest as _, digest::FixedOutput as _}; use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{ use crate::public_transaction::{Message, WitnessSet};
V03State, ensure,
error::NssaError,
public_transaction::{Message, WitnessSet},
state::MAX_NUMBER_CHAINED_CALLS,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PublicTransaction { pub struct PublicTransaction {
@ -66,180 +57,6 @@ impl PublicTransaction {
hasher.update(&bytes); hasher.update(&bytes);
hasher.finalize_fixed().into() hasher.finalize_fixed().into()
} }
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
) -> Result<HashMap<AccountId, Account>, NssaError> {
let message = self.message();
let witness_set = self.witness_set();
// All account_ids must be different
ensure!(
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
NssaError::InvalidInput("Duplicate account_ids found in message".into(),)
);
// Check exactly one nonce is provided for each signature
ensure!(
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
NssaError::InvalidInput(
"Mismatch between number of nonces and signatures/public keys".into(),
)
);
// Check the signatures are valid
ensure!(
witness_set.is_valid_for(message),
NssaError::InvalidInput("Invalid signature for given message and public key".into())
);
let signer_account_ids = self.signer_account_ids();
// Check nonces corresponds to the current nonces on the public state.
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
let current_nonce = state.get_account_by_id(*account_id).nonce;
ensure!(
current_nonce == *nonce,
NssaError::InvalidInput("Nonce mismatch".into())
);
}
// Build pre_states for execution
let input_pre_states: Vec<_> = message
.account_ids
.iter()
.map(|account_id| {
AccountWithMetadata::new(
state.get_account_by_id(*account_id),
signer_account_ids.contains(account_id),
*account_id,
)
})
.collect();
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
let initial_call = ChainedCall {
program_id: message.program_id,
instruction_data: message.instruction_data.clone(),
pre_states: input_pre_states,
pda_seeds: vec![],
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
ensure!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
NssaError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
return Err(NssaError::InvalidInput("Unknown program".into()));
};
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
);
let mut program_output =
program.execute(&chained_call.pre_states, &chained_call.instruction_data)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coincide with the values in the public
// state or with any modifications to those values during the chain of calls.
let expected_pre = state_diff
.get(&account_id)
.cloned()
.unwrap_or_else(|| state.get_account_by_id(account_id));
ensure!(
pre.account == expected_pre,
NssaError::InvalidProgramBehavior
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let is_authorized = signer_account_ids.contains(&account_id)
|| authorized_pdas.contains(&account_id);
ensure!(
pre.is_authorized == is_authorized,
NssaError::InvalidProgramBehavior
);
}
// Verify execution corresponds to a well-behaved program.
// See the # Programs section for the definition of the `validate_execution` method.
ensure!(
validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
),
NssaError::InvalidProgramBehavior
);
for post in program_output
.post_states
.iter_mut()
.filter(|post| post.requires_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 = chained_call.program_id;
} else {
return Err(NssaError::InvalidProgramBehavior);
}
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
state_diff.insert(pre.account_id, post.account().clone());
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter = chain_calls_counter
.checked_add(1)
.expect("we check the max depth at the beginning of the loop");
}
// Check that all modified uninitialized accounts where claimed
for post in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(*account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some(post)
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
}
Ok(state_diff)
}
} }
#[cfg(test)] #[cfg(test)]
@ -251,6 +68,7 @@ pub mod tests {
error::NssaError, error::NssaError,
program::Program, program::Program,
public_transaction::{Message, WitnessSet}, public_transaction::{Message, WitnessSet},
validated_state_diff::ValidatedStateDiff,
}; };
fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) { fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) {
@ -264,7 +82,7 @@ pub mod tests {
fn state_for_tests() -> V03State { fn state_for_tests() -> V03State {
let (_, _, addr1, addr2) = keys_for_tests(); let (_, _, addr1, addr2) = keys_for_tests();
let initial_data = [(addr1, 10000), (addr2, 20000)]; let initial_data = [(addr1, 10000), (addr2, 20000)];
V03State::new_with_genesis_accounts(&initial_data, &[]) V03State::new_with_genesis_accounts(&initial_data, vec![], 0)
} }
fn transaction_for_tests() -> PublicTransaction { fn transaction_for_tests() -> PublicTransaction {
@ -359,7 +177,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -379,7 +197,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -400,7 +218,7 @@ pub mod tests {
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -420,7 +238,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -436,7 +254,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
} }

Some files were not shown because too many files have changed in this diff Show More