mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-25 09:29:33 +00:00
Merge remote-tracking branch 'upstream/main' into feat/sequencer-healthcheck
This commit is contained in:
commit
48e5b10bf7
@ -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-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-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" },
|
||||
]
|
||||
yanked = "deny"
|
||||
unused-ignored-advisory = "deny"
|
||||
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -11,6 +11,10 @@ on:
|
||||
- "**.md"
|
||||
- "!.github/workflows/*.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
name: General
|
||||
|
||||
jobs:
|
||||
@ -19,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install nightly toolchain for rustfmt
|
||||
run: rustup install nightly --profile minimal --component rustfmt
|
||||
@ -32,7 +36,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install taplo-cli
|
||||
run: cargo install --locked taplo-cli
|
||||
@ -45,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
@ -61,7 +65,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: cargo install --locked cargo-deny
|
||||
@ -77,7 +81,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
@ -106,7 +110,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
@ -126,7 +130,7 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests
|
||||
run: cargo nextest run --workspace --exclude integration_tests --all-features
|
||||
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@ -134,7 +138,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
@ -162,7 +166,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
@ -182,7 +186,7 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
@ -190,7 +194,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
@ -216,7 +220,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
|
||||
2
.github/workflows/publish_images.yml
vendored
2
.github/workflows/publish_images.yml
vendored
@ -50,7 +50,7 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
|
||||
595
Cargo.lock
generated
595
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@ -15,8 +15,11 @@ members = [
|
||||
"nssa/core",
|
||||
"programs/amm/core",
|
||||
"programs/amm",
|
||||
"programs/clock/core",
|
||||
"programs/token/core",
|
||||
"programs/token",
|
||||
"programs/associated_token_account/core",
|
||||
"programs/associated_token_account",
|
||||
"sequencer/core",
|
||||
"sequencer/service",
|
||||
"sequencer/service/protocol",
|
||||
@ -34,6 +37,7 @@ members = [
|
||||
"examples/program_deployment/methods",
|
||||
"examples/program_deployment/methods/guest",
|
||||
"bedrock_client",
|
||||
"testnet_initial_state",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -53,12 +57,16 @@ indexer_service_protocol = { path = "indexer/service/protocol" }
|
||||
indexer_service_rpc = { path = "indexer/service/rpc" }
|
||||
wallet = { path = "wallet" }
|
||||
wallet-ffi = { path = "wallet-ffi", default-features = false }
|
||||
clock_core = { path = "programs/clock/core" }
|
||||
token_core = { path = "programs/token/core" }
|
||||
token_program = { path = "programs/token" }
|
||||
amm_core = { path = "programs/amm/core" }
|
||||
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" }
|
||||
bedrock_client = { path = "bedrock_client" }
|
||||
testnet_initial_state = { path = "testnet_initial_state" }
|
||||
|
||||
tokio = { version = "1.50", features = [
|
||||
"net",
|
||||
@ -112,11 +120,11 @@ tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" }
|
||||
logos-blockchain-chain-service = { 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", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
|
||||
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", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" }
|
||||
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = [
|
||||
"snappy",
|
||||
@ -144,6 +152,14 @@ opt-level = 'z'
|
||||
lto = true
|
||||
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]
|
||||
warnings = "deny"
|
||||
|
||||
|
||||
Binary file not shown.
BIN
artifacts/program_methods/associated_token_account.bin
Normal file
BIN
artifacts/program_methods/associated_token_account.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/program_methods/clock.bin
Normal file
BIN
artifacts/program_methods/clock.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/auth_asserting_noop.bin
Normal file
BIN
artifacts/test_program_methods/auth_asserting_noop.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
BIN
artifacts/test_program_methods/chain_caller_pda_drop.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/clock_chain_caller.bin
Normal file
BIN
artifacts/test_program_methods/clock_chain_caller.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/flash_swap_callback.bin
Normal file
BIN
artifacts/test_program_methods/flash_swap_callback.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/flash_swap_initiator.bin
Normal file
BIN
artifacts/test_program_methods/flash_swap_initiator.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/malicious_caller_program_id.bin
Normal file
BIN
artifacts/test_program_methods/malicious_caller_program_id.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/malicious_self_program_id.bin
Normal file
BIN
artifacts/test_program_methods/malicious_self_program_id.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/pda_claimer.bin
Normal file
BIN
artifacts/test_program_methods/pda_claimer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
BIN
artifacts/test_program_methods/pinata_cooldown.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/private_pda_delegator.bin
Normal file
BIN
artifacts/test_program_methods/private_pda_delegator.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
BIN
artifacts/test_program_methods/time_locked_transfer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/two_pda_claimer.bin
Normal file
BIN
artifacts/test_program_methods/two_pda_claimer.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/validity_window.bin
Normal file
BIN
artifacts/test_program_methods/validity_window.bin
Normal file
Binary file not shown.
BIN
artifacts/test_program_methods/validity_window_chain_caller.bin
Normal file
BIN
artifacts/test_program_methods/validity_window_chain_caller.bin
Normal file
Binary file not shown.
@ -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
|
||||
82
bedrock/deployment-settings.yaml
Normal file
82
bedrock/deployment-settings.yaml
Normal 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
|
||||
@ -1,46 +1,12 @@
|
||||
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:
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:c5243681b353278cabb562a176f0a5cfbefc2056f18cebc47fe0e3720c29fb12
|
||||
ports:
|
||||
- "${PORT:-8080}:18080/tcp"
|
||||
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-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
|
||||
- ./node-config.yaml:/etc/logos-blockchain/node-config.yaml:z
|
||||
- ./deployment-settings.yaml:/etc/logos-blockchain/deployment-settings.yaml:z
|
||||
entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh
|
||||
|
||||
54
bedrock/node-config.yaml
Normal file
54
bedrock/node-config.yaml
Normal 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"
|
||||
@ -2,12 +2,19 @@
|
||||
|
||||
set -e
|
||||
|
||||
export CFG_FILE_PATH="/config.yaml" \
|
||||
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
|
||||
export POL_PROOF_DEV_MODE=true
|
||||
|
||||
/usr/bin/logos-blockchain-cfgsync-client && \
|
||||
exec /usr/bin/logos-blockchain-node /config.yaml
|
||||
# Use static configs mounted from host. Both node-config.yaml and
|
||||
# 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
|
||||
|
||||
@ -46,7 +46,7 @@ impl BedrockClient {
|
||||
info!("Creating Bedrock client with node URL {node_url}");
|
||||
let client = Client::builder()
|
||||
//Add more fields if needed
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.timeout(std::time::Duration::from_mins(1))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ workspace = true
|
||||
[dependencies]
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
clock_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa::AccountId;
|
||||
use nssa_core::BlockId;
|
||||
pub use nssa_core::Timestamp;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
|
||||
|
||||
use crate::{HashType, transaction::NSSATransaction};
|
||||
|
||||
pub type MantleMsgId = [u8; 32];
|
||||
pub type BlockHash = HashType;
|
||||
pub type BlockId = u64;
|
||||
pub type TimeStamp = u64;
|
||||
|
||||
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||
pub struct BlockMeta {
|
||||
@ -36,7 +34,7 @@ pub struct BlockHeader {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: BlockHash,
|
||||
pub hash: BlockHash,
|
||||
pub timestamp: TimeStamp,
|
||||
pub timestamp: Timestamp,
|
||||
pub signature: nssa::Signature,
|
||||
}
|
||||
|
||||
@ -76,7 +74,7 @@ impl<'de> Deserialize<'de> for Block {
|
||||
pub struct HashableBlockData {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: BlockHash,
|
||||
pub timestamp: TimeStamp,
|
||||
pub timestamp: Timestamp,
|
||||
pub transactions: Vec<NSSATransaction>,
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
mod tests {
|
||||
use crate::{HashType, block::HashableBlockData, test_utils};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use log::warn;
|
||||
use nssa::{AccountId, V03State};
|
||||
use nssa::{AccountId, V03State, ValidatedStateDiff};
|
||||
use nssa_core::{BlockId, Timestamp};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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(
|
||||
self,
|
||||
state: &mut V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, nssa::error::NssaError> {
|
||||
match &self {
|
||||
Self::Public(tx) => state.transition_from_public_transaction(tx),
|
||||
Self::PrivacyPreserving(tx) => state.transition_from_privacy_preserving_transaction(tx),
|
||||
Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx),
|
||||
}
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
|
||||
let diff = self
|
||||
.validate_on_state(state, block_id, timestamp)
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
@ -116,3 +153,20 @@ pub enum TransactionMalformationError {
|
||||
#[error("Transaction size {size} exceeds maximum allowed size of {max} bytes")]
|
||||
TransactionTooLarge { size: usize, max: usize },
|
||||
}
|
||||
|
||||
/// Returns the canonical Clock Program invocation transaction for the given block timestamp.
|
||||
/// Every valid block must end with exactly one occurrence of this transaction.
|
||||
#[must_use]
|
||||
pub fn clock_invocation(timestamp: clock_core::Instruction) -> nssa::PublicTransaction {
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
nssa::program::Program::clock().id(),
|
||||
clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(),
|
||||
vec![],
|
||||
timestamp,
|
||||
)
|
||||
.expect("Clock invocation message should always be constructable");
|
||||
nssa::PublicTransaction::new(
|
||||
message,
|
||||
nssa::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
)
|
||||
}
|
||||
|
||||
@ -22,6 +22,20 @@ _wallet_complete_account_id() {
|
||||
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() {
|
||||
local cur prev words cword
|
||||
_init_completion 2>/dev/null || {
|
||||
@ -91,20 +105,32 @@ _wallet() {
|
||||
--account-id)
|
||||
_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
|
||||
;;
|
||||
send)
|
||||
case "$prev" in
|
||||
--from | --to)
|
||||
--from)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
@ -147,8 +173,11 @@ _wallet() {
|
||||
-a | --account-id)
|
||||
_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
|
||||
;;
|
||||
@ -186,10 +215,13 @@ _wallet() {
|
||||
-a | --account-id)
|
||||
_wallet_complete_account_id "$cur"
|
||||
;;
|
||||
--account-label)
|
||||
_wallet_complete_account_label "$cur"
|
||||
;;
|
||||
-l | --label)
|
||||
;; # 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
|
||||
;;
|
||||
@ -206,8 +238,11 @@ _wallet() {
|
||||
--to)
|
||||
_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
|
||||
;;
|
||||
@ -221,49 +256,85 @@ _wallet() {
|
||||
;;
|
||||
new)
|
||||
case "$prev" in
|
||||
--definition-account-id | --supply-account-id)
|
||||
--definition-account-id)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
send)
|
||||
case "$prev" in
|
||||
--from | --to)
|
||||
--from)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
burn)
|
||||
case "$prev" in
|
||||
--definition | --holder)
|
||||
--definition)
|
||||
_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)
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--definition --holder --amount" -- "$cur"))
|
||||
COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --amount" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
mint)
|
||||
case "$prev" in
|
||||
--definition | --holder)
|
||||
--definition)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
@ -277,49 +348,103 @@ _wallet() {
|
||||
;;
|
||||
new)
|
||||
case "$prev" in
|
||||
--user-holding-a | --user-holding-b | --user-holding-lp)
|
||||
--user-holding-a)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
swap)
|
||||
case "$prev" in
|
||||
--user-holding-a | --user-holding-b)
|
||||
--user-holding-a)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
add-liquidity)
|
||||
case "$prev" in
|
||||
--user-holding-a | --user-holding-b | --user-holding-lp)
|
||||
--user-holding-a)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
remove-liquidity)
|
||||
case "$prev" in
|
||||
--user-holding-a | --user-holding-b | --user-holding-lp)
|
||||
--user-holding-a)
|
||||
_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)
|
||||
;; # 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
|
||||
;;
|
||||
|
||||
@ -90,12 +90,15 @@ _wallet_auth_transfer() {
|
||||
case $line[1] in
|
||||
init)
|
||||
_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)
|
||||
_arguments \
|
||||
'--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-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
|
||||
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
|
||||
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
|
||||
'--amount[Amount of native tokens to send]:amount:'
|
||||
@ -165,7 +168,8 @@ _wallet_account() {
|
||||
_arguments \
|
||||
'(-r --raw)'{-r,--raw}'[Get raw account data]' \
|
||||
'(-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)
|
||||
_arguments \
|
||||
@ -189,6 +193,7 @@ _wallet_account() {
|
||||
label)
|
||||
_arguments \
|
||||
'(-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:'
|
||||
;;
|
||||
esac
|
||||
@ -216,7 +221,8 @@ _wallet_pinata() {
|
||||
case $line[1] in
|
||||
claim)
|
||||
_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
|
||||
;;
|
||||
@ -249,12 +255,16 @@ _wallet_token() {
|
||||
'--name[Token name]:name:' \
|
||||
'--total-supply[Total supply of tokens to mint]:total_supply:' \
|
||||
'--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)
|
||||
_arguments \
|
||||
'--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-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \
|
||||
'--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \
|
||||
'--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \
|
||||
'--amount[Amount of tokens to send]:amount:'
|
||||
@ -262,13 +272,17 @@ _wallet_token() {
|
||||
burn)
|
||||
_arguments \
|
||||
'--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-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
|
||||
'--amount[Amount of tokens to burn]:amount:'
|
||||
;;
|
||||
mint)
|
||||
_arguments \
|
||||
'--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-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \
|
||||
'--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \
|
||||
'--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \
|
||||
'--amount[Amount of tokens to mint]:amount:'
|
||||
@ -302,15 +316,20 @@ _wallet_amm() {
|
||||
new)
|
||||
_arguments \
|
||||
'--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-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-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-b[Amount of token B to deposit]:balance_b:'
|
||||
;;
|
||||
swap)
|
||||
_arguments \
|
||||
'--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-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \
|
||||
'--amount-in[Amount of tokens to swap]:amount_in:' \
|
||||
'--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \
|
||||
'--token-definition[Definition ID of the token being provided]:token_def:'
|
||||
@ -318,8 +337,11 @@ _wallet_amm() {
|
||||
add-liquidity)
|
||||
_arguments \
|
||||
'--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-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-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-b[Maximum amount of token B to deposit]:max_amount_b:' \
|
||||
'--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:'
|
||||
@ -327,8 +349,11 @@ _wallet_amm() {
|
||||
remove-liquidity)
|
||||
_arguments \
|
||||
'--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-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-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:' \
|
||||
'--min-amount-a[Minimum token A to receive]:min_amount_a:' \
|
||||
'--min-amount-b[Minimum token B to receive]:min_amount_b:'
|
||||
@ -424,7 +449,7 @@ _wallet_help() {
|
||||
_wallet_account_ids() {
|
||||
local -a accounts
|
||||
local line
|
||||
|
||||
|
||||
# Try to get accounts from wallet account list command
|
||||
# Filter to lines starting with /N (numbered accounts) and extract the account ID
|
||||
if command -v wallet &>/dev/null; then
|
||||
@ -433,14 +458,35 @@ _wallet_account_ids() {
|
||||
[[ -n "$line" ]] && accounts+=("${line%,}")
|
||||
done < <(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}')
|
||||
fi
|
||||
|
||||
|
||||
# Provide type prefixes as fallback if command fails or returns nothing
|
||||
if (( ${#accounts} == 0 )); then
|
||||
compadd -S '' -- 'Public/' 'Private/'
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
_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 "$@"
|
||||
|
||||
369
docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md
Normal file
369
docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md
Normal 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}
|
||||
```
|
||||
@ -1,6 +1,4 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
};
|
||||
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
// Hello-world example program.
|
||||
//
|
||||
@ -21,6 +19,8 @@ fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: greeting,
|
||||
},
|
||||
@ -45,16 +45,19 @@ fn main() {
|
||||
|
||||
// Wrap the post state account values inside a `AccountPostState` instance.
|
||||
// This is used to forward the account claiming request if any
|
||||
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
};
|
||||
let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
|
||||
|
||||
// The output is a proposed state difference. It will only succeed if the pre states coincide
|
||||
// with the previous values of the accounts, and the transition to the post states conforms
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
};
|
||||
use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs};
|
||||
|
||||
// Hello-world with authorization example program.
|
||||
//
|
||||
@ -21,6 +19,8 @@ fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: greeting,
|
||||
},
|
||||
@ -52,16 +52,19 @@ fn main() {
|
||||
|
||||
// Wrap the post state account values inside a `AccountPostState` instance.
|
||||
// This is used to forward the account claiming request if any
|
||||
let post_state = if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
};
|
||||
let post_state = AccountPostState::new_claimed_if_default(post_account, Claim::Authorized);
|
||||
|
||||
// The output is a proposed state difference. It will only succeed if the pre states coincide
|
||||
// with the previous values of the accounts, and the transition to the post states conforms
|
||||
// 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();
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
use nssa_core::{
|
||||
account::{Account, AccountWithMetadata, Data},
|
||||
program::{
|
||||
AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, read_nssa_inputs, write_nssa_outputs,
|
||||
},
|
||||
account::{AccountWithMetadata, Data},
|
||||
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs},
|
||||
};
|
||||
|
||||
// Hello-world with write + move_data example program.
|
||||
@ -26,16 +24,6 @@ const MOVE_DATA_FUNCTION_ID: u8 = 1;
|
||||
|
||||
type Instruction = (u8, Vec<u8>);
|
||||
|
||||
fn build_post_state(post_account: Account) -> AccountPostState {
|
||||
if post_account.program_owner == DEFAULT_PROGRAM_ID {
|
||||
// This produces a claim request
|
||||
AccountPostState::new_claimed(post_account)
|
||||
} else {
|
||||
// This doesn't produce a claim request
|
||||
AccountPostState::new(post_account)
|
||||
}
|
||||
}
|
||||
|
||||
fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
|
||||
// Construct the post state account values
|
||||
let post_account = {
|
||||
@ -48,7 +36,7 @@ fn write(pre_state: AccountWithMetadata, greeting: &[u8]) -> AccountPostState {
|
||||
this
|
||||
};
|
||||
|
||||
build_post_state(post_account)
|
||||
AccountPostState::new_claimed_if_default(post_account, Claim::Authorized)
|
||||
}
|
||||
|
||||
fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<AccountPostState> {
|
||||
@ -58,7 +46,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<
|
||||
let from_post = {
|
||||
let mut this = from_pre.account;
|
||||
this.data = Data::default();
|
||||
build_post_state(this)
|
||||
AccountPostState::new_claimed_if_default(this, Claim::Authorized)
|
||||
};
|
||||
|
||||
let to_post = {
|
||||
@ -68,7 +56,7 @@ fn move_data(from_pre: AccountWithMetadata, to_pre: AccountWithMetadata) -> Vec<
|
||||
this.data = bytes
|
||||
.try_into()
|
||||
.expect("Data should fit within the allowed limits");
|
||||
build_post_state(this)
|
||||
AccountPostState::new_claimed_if_default(this, Claim::Authorized)
|
||||
};
|
||||
|
||||
vec![from_post, to_post]
|
||||
@ -78,6 +66,8 @@ fn main() {
|
||||
// Read input accounts.
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (function_id, data),
|
||||
},
|
||||
@ -95,5 +85,14 @@ fn main() {
|
||||
_ => 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();
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, ProgramId, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs,
|
||||
};
|
||||
|
||||
// Tail Call example program.
|
||||
@ -28,6 +27,8 @@ fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (),
|
||||
},
|
||||
@ -53,11 +54,16 @@ fn main() {
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
// Write the outputs
|
||||
write_nssa_outputs_with_chained_call(
|
||||
// Write the outputs.
|
||||
// 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],
|
||||
vec![chained_call],
|
||||
);
|
||||
)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use nssa_core::program::{
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, read_nssa_inputs,
|
||||
write_nssa_outputs_with_chained_call,
|
||||
AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput,
|
||||
read_nssa_inputs,
|
||||
};
|
||||
|
||||
// Tail Call with PDA example program.
|
||||
@ -33,6 +33,8 @@ fn main() {
|
||||
// Read inputs
|
||||
let (
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction: (),
|
||||
},
|
||||
@ -65,11 +67,16 @@ fn main() {
|
||||
pda_seeds: vec![PDA_SEED],
|
||||
};
|
||||
|
||||
// Write the outputs
|
||||
write_nssa_outputs_with_chained_call(
|
||||
// Write the outputs.
|
||||
// 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],
|
||||
vec![chained_call],
|
||||
);
|
||||
)
|
||||
.with_chained_calls(vec![chained_call])
|
||||
.write();
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ async fn main() {
|
||||
let program = Program::new(bytecode).unwrap();
|
||||
|
||||
// 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 instruction_data = ();
|
||||
let nonces = vec![];
|
||||
|
||||
@ -177,12 +177,13 @@ pub fn TransactionPage() -> impl IntoView {
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = message;
|
||||
let WitnessSet {
|
||||
signatures_and_public_keys: _,
|
||||
proof,
|
||||
} = witness_set;
|
||||
|
||||
let proof_len = proof.map_or(0, |p| p.0.len());
|
||||
view! {
|
||||
<div class="transaction-details">
|
||||
@ -212,6 +213,14 @@ pub fn TransactionPage() -> impl IntoView {
|
||||
<span class="info-label">"Proof Size:"</span>
|
||||
<span class="info-value">{format!("{proof_len} bytes")}</span>
|
||||
</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>
|
||||
|
||||
<h3>"Public Accounts"</h3>
|
||||
|
||||
@ -13,6 +13,7 @@ bedrock_client.workspace = true
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
storage.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@ -3,10 +3,11 @@ use std::{path::Path, sync::Arc};
|
||||
use anyhow::Result;
|
||||
use bedrock_client::HeaderId;
|
||||
use common::{
|
||||
block::{BedrockStatus, Block, BlockId},
|
||||
transaction::NSSATransaction,
|
||||
block::{BedrockStatus, Block},
|
||||
transaction::{NSSATransaction, clock_invocation},
|
||||
};
|
||||
use nssa::{Account, AccountId, V03State};
|
||||
use nssa_core::BlockId;
|
||||
use storage::indexer::RocksDBIO;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@ -121,12 +122,37 @@ impl IndexerStore {
|
||||
{
|
||||
let mut state_guard = self.current_state.write().await;
|
||||
|
||||
for transaction in &block.body.transactions {
|
||||
let (clock_tx, user_txs) = block
|
||||
.body
|
||||
.transactions
|
||||
.split_last()
|
||||
.ok_or_else(|| anyhow::anyhow!("Block has no transactions"))?;
|
||||
|
||||
anyhow::ensure!(
|
||||
*clock_tx == NSSATransaction::Public(clock_invocation(block.header.timestamp)),
|
||||
"Last transaction in block must be the clock invocation for the block timestamp"
|
||||
);
|
||||
|
||||
for transaction in user_txs {
|
||||
transaction
|
||||
.clone()
|
||||
.transaction_stateless_check()?
|
||||
.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
|
||||
@ -172,7 +198,11 @@ mod tests {
|
||||
let storage = IndexerStore::open_db_with_genesis(
|
||||
home.as_ref(),
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(
|
||||
&[(acc1(), 10000), (acc2(), 20000)],
|
||||
vec![],
|
||||
0,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -190,7 +220,11 @@ mod tests {
|
||||
let storage = IndexerStore::open_db_with_genesis(
|
||||
home.as_ref(),
|
||||
&genesis_block(),
|
||||
&nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]),
|
||||
&nssa::V03State::new_with_genesis_accounts(
|
||||
&[(acc1(), 10000), (acc2(), 20000)],
|
||||
vec![],
|
||||
0,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -208,11 +242,14 @@ mod tests {
|
||||
10,
|
||||
&sign_key,
|
||||
);
|
||||
let block_id = u64::try_from(i).unwrap();
|
||||
let block_timestamp = block_id.saturating_mul(100);
|
||||
let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp));
|
||||
|
||||
let next_block = common::test_utils::produce_dummy_block(
|
||||
u64::try_from(i).unwrap(),
|
||||
block_id,
|
||||
Some(prev_hash),
|
||||
vec![tx],
|
||||
vec![tx, clock_tx],
|
||||
);
|
||||
prev_hash = next_block.header.hash;
|
||||
|
||||
|
||||
@ -7,13 +7,11 @@ use std::{
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
pub use bedrock_client::BackoffConfig;
|
||||
use common::{
|
||||
block::{AccountInitialData, CommitmentsInitialData},
|
||||
config::BasicAuth,
|
||||
};
|
||||
use common::config::BasicAuth;
|
||||
use humantime_serde;
|
||||
pub use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use testnet_initial_state::{PrivateAccountPublicInitialData, PublicAccountPublicInitialData};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -29,16 +27,16 @@ pub struct ClientConfig {
|
||||
pub struct IndexerConfig {
|
||||
/// Home dir of sequencer storage.
|
||||
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.
|
||||
pub signing_key: [u8; 32],
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub consensus_info_polling_interval: Duration,
|
||||
pub bedrock_client_config: ClientConfig,
|
||||
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 {
|
||||
|
||||
@ -2,14 +2,17 @@ use std::collections::VecDeque;
|
||||
|
||||
use anyhow::Result;
|
||||
use bedrock_client::{BedrockClient, HeaderId};
|
||||
use common::block::{Block, HashableBlockData};
|
||||
// ToDo: Remove after testnet
|
||||
use common::{HashType, PINATA_BASE58};
|
||||
use common::{
|
||||
HashType, PINATA_BASE58,
|
||||
block::{Block, HashableBlockData},
|
||||
};
|
||||
use log::{debug, error, info};
|
||||
use logos_blockchain_core::mantle::{
|
||||
Op, SignedMantleTx,
|
||||
ops::channel::{ChannelId, inscribe::InscriptionOp},
|
||||
};
|
||||
use nssa::V03State;
|
||||
use testnet_initial_state::initial_state_testnet;
|
||||
|
||||
use crate::{block_store::IndexerStore, config::IndexerConfig};
|
||||
|
||||
@ -54,36 +57,52 @@ impl IndexerCore {
|
||||
let channel_genesis_msg_id = [0; 32];
|
||||
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
|
||||
// affect this. And indexer can not reliably ask this data from sequencer
|
||||
// because indexer must be independent from it.
|
||||
// ToDo: move initial state generation into common and use the same method
|
||||
// for indexer and sequencer. This way both services buit at same version
|
||||
// could be in sync.
|
||||
let initial_commitments: Vec<nssa_core::Commitment> = config
|
||||
.initial_commitments
|
||||
.iter()
|
||||
.map(|init_comm_data| {
|
||||
let npk = &init_comm_data.npk;
|
||||
let initial_private_accounts: Option<Vec<(nssa_core::Commitment, nssa_core::Nullifier)>> =
|
||||
config.initial_private_accounts.as_ref().map(|accounts| {
|
||||
accounts
|
||||
.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)
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
nssa_core::Commitment::new(npk, &acc),
|
||||
nssa_core::Nullifier::for_account_initialization(npk),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
let init_accs: Vec<(nssa::AccountId, u128)> = config
|
||||
.initial_accounts
|
||||
.iter()
|
||||
.map(|acc_data| (acc_data.account_id, acc_data.balance))
|
||||
.collect();
|
||||
let init_accs: Option<Vec<(nssa::AccountId, u128)>> = config
|
||||
.initial_public_accounts
|
||||
.as_ref()
|
||||
.map(|initial_accounts| {
|
||||
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
|
||||
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
||||
// ToDo: Remove after testnet
|
||||
state.add_pinata_program(PINATA_BASE58.parse().unwrap());
|
||||
|
||||
state
|
||||
} else {
|
||||
initial_state_testnet()
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::{
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId,
|
||||
Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
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,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
Self {
|
||||
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()
|
||||
.map(|(n, d)| (n.into(), d.into()))
|
||||
.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 {
|
||||
type Error = nssa_core::account::data::DataTooBigError;
|
||||
type Error = nssa::error::NssaError;
|
||||
|
||||
fn try_from(value: PrivacyPreservingMessage) -> Result<Self, Self::Error> {
|
||||
let PrivacyPreservingMessage {
|
||||
@ -316,6 +320,8 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
block_validity_window,
|
||||
timestamp_validity_window,
|
||||
} = value;
|
||||
Ok(Self {
|
||||
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
|
||||
.into_iter()
|
||||
.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
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
@ -336,6 +343,12 @@ impl TryFrom<PrivacyPreservingMessage> for nssa::privacy_preserving_transaction:
|
||||
.into_iter()
|
||||
.map(|(n, d)| (n.into(), d.into()))
|
||||
.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,
|
||||
} = value;
|
||||
|
||||
Ok(Self::new(
|
||||
message
|
||||
.try_into()
|
||||
.map_err(|err: nssa_core::account::data::DataTooBigError| {
|
||||
nssa::error::NssaError::InvalidInput(err.to_string())
|
||||
})?,
|
||||
witness_set.try_into()?,
|
||||
))
|
||||
Ok(Self::new(message.try_into()?, witness_set.try_into()?))
|
||||
}
|
||||
}
|
||||
|
||||
@ -687,3 +693,21 @@ impl From<HashType> for common::HashType {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,7 +138,7 @@ pub struct Account {
|
||||
}
|
||||
|
||||
pub type BlockId = u64;
|
||||
pub type TimeStamp = u64;
|
||||
pub type Timestamp = u64;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Block {
|
||||
@ -153,7 +153,7 @@ pub struct BlockHeader {
|
||||
pub block_id: BlockId,
|
||||
pub prev_block_hash: HashType,
|
||||
pub hash: HashType,
|
||||
pub timestamp: TimeStamp,
|
||||
pub timestamp: Timestamp,
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
@ -235,6 +235,8 @@ pub struct PrivacyPreservingMessage {
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
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)]
|
||||
@ -300,6 +302,20 @@ pub struct Nullifier(
|
||||
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)]
|
||||
pub struct CommitmentSetDigest(
|
||||
#[serde(with = "base64::arr")]
|
||||
|
||||
@ -13,7 +13,7 @@ use indexer_service_protocol::{
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
|
||||
Transaction, WitnessSet,
|
||||
Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
use jsonrpsee::{
|
||||
core::{SubscriptionResult, async_trait},
|
||||
@ -124,6 +124,8 @@ impl MockIndexerService {
|
||||
indexer_service_protocol::Nullifier([tx_idx as u8; 32]),
|
||||
CommitmentSetDigest([0xff; 32]),
|
||||
)],
|
||||
block_validity_window: ValidityWindow((None, None)),
|
||||
timestamp_validity_window: ValidityWindow((None, None)),
|
||||
},
|
||||
witness_set: WitnessSet {
|
||||
signatures_and_public_keys: vec![],
|
||||
|
||||
@ -18,9 +18,11 @@ key_protocol.workspace = true
|
||||
indexer_service.workspace = true
|
||||
serde_json.workspace = true
|
||||
token_core.workspace = true
|
||||
ata_core.workspace = true
|
||||
indexer_service_rpc.workspace = true
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
wallet-ffi.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
|
||||
|
||||
@ -2,16 +2,17 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use common::block::{AccountInitialData, CommitmentsInitialData};
|
||||
use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use nssa::{Account, AccountId, PrivateKey, PublicKey};
|
||||
use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID};
|
||||
use sequencer_core::config::{BedrockConfig, SequencerConfig};
|
||||
use url::Url;
|
||||
use wallet::config::{
|
||||
InitialAccountData, InitialAccountDataPrivate, InitialAccountDataPublic, WalletConfig,
|
||||
use testnet_initial_state::{
|
||||
PrivateAccountPrivateInitialData, PrivateAccountPublicInitialData,
|
||||
PublicAccountPrivateInitialData, PublicAccountPublicInitialData,
|
||||
};
|
||||
use url::Url;
|
||||
use wallet::config::{InitialAccountData, WalletConfig};
|
||||
|
||||
/// Sequencer config options available for custom changes in integration tests.
|
||||
#[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
|
||||
.iter()
|
||||
.map(|(priv_key, balance)| {
|
||||
let pub_key = PublicKey::new_from_private_key(priv_key);
|
||||
let account_id = AccountId::from(&pub_key);
|
||||
AccountInitialData {
|
||||
PublicAccountPublicInitialData {
|
||||
account_id,
|
||||
balance: *balance,
|
||||
}
|
||||
@ -116,11 +117,11 @@ impl InitialData {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sequencer_initial_commitments(&self) -> Vec<CommitmentsInitialData> {
|
||||
fn sequencer_initial_private_accounts(&self) -> Vec<PrivateAccountPublicInitialData> {
|
||||
self.private_accounts
|
||||
.iter()
|
||||
.map(|(key_chain, account)| CommitmentsInitialData {
|
||||
npk: key_chain.nullifier_public_key.clone(),
|
||||
.map(|(key_chain, account)| PrivateAccountPublicInitialData {
|
||||
npk: key_chain.nullifier_public_key,
|
||||
account: account.clone(),
|
||||
})
|
||||
.collect()
|
||||
@ -132,14 +133,14 @@ impl InitialData {
|
||||
.map(|(priv_key, _)| {
|
||||
let pub_key = PublicKey::new_from_private_key(priv_key);
|
||||
let account_id = AccountId::from(&pub_key);
|
||||
InitialAccountData::Public(InitialAccountDataPublic {
|
||||
InitialAccountData::Public(PublicAccountPrivateInitialData {
|
||||
account_id,
|
||||
pub_sign_key: priv_key.clone(),
|
||||
})
|
||||
})
|
||||
.chain(self.private_accounts.iter().map(|(key_chain, account)| {
|
||||
let account_id = AccountId::from(&key_chain.nullifier_public_key);
|
||||
InitialAccountData::Private(Box::new(InitialAccountDataPrivate {
|
||||
InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData {
|
||||
account_id,
|
||||
account: account.clone(),
|
||||
key_chain: key_chain.clone(),
|
||||
@ -181,8 +182,8 @@ pub fn indexer_config(
|
||||
max_retries: 10,
|
||||
},
|
||||
},
|
||||
initial_accounts: initial_data.sequencer_initial_accounts(),
|
||||
initial_commitments: initial_data.sequencer_initial_commitments(),
|
||||
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
|
||||
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
|
||||
signing_key: [37; 32],
|
||||
channel_id: bedrock_channel_id(),
|
||||
})
|
||||
@ -210,9 +211,9 @@ pub fn sequencer_config(
|
||||
max_block_size,
|
||||
mempool_max_size,
|
||||
block_create_timeout,
|
||||
retry_pending_blocks_timeout: Duration::from_secs(120),
|
||||
initial_accounts: initial_data.sequencer_initial_accounts(),
|
||||
initial_commitments: initial_data.sequencer_initial_commitments(),
|
||||
retry_pending_blocks_timeout: Duration::from_secs(5),
|
||||
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
|
||||
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
|
||||
signing_key: [37; 32],
|
||||
bedrock_config: BedrockConfig {
|
||||
backoff: BackoffConfig {
|
||||
@ -240,7 +241,7 @@ pub fn wallet_config(
|
||||
seq_tx_poll_max_blocks: 15,
|
||||
seq_poll_max_retries: 10,
|
||||
seq_block_poll_max_amount: 100,
|
||||
initial_accounts: initial_data.wallet_initial_accounts(),
|
||||
initial_accounts: Some(initial_data.wallet_initial_accounts()),
|
||||
basic_auth: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -256,11 +256,11 @@ impl TestContext {
|
||||
let config_overrides = WalletConfigOverrides::default();
|
||||
|
||||
let wallet_password = "test_pass".to_owned();
|
||||
let wallet = WalletCore::new_init_storage(
|
||||
let (wallet, _mnemonic) = WalletCore::new_init_storage(
|
||||
config_path,
|
||||
storage_path,
|
||||
Some(config_overrides),
|
||||
wallet_password.clone(),
|
||||
&wallet_password,
|
||||
)
|
||||
.context("Failed to init wallet")?;
|
||||
wallet
|
||||
|
||||
@ -113,9 +113,12 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Create new token
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id_1),
|
||||
supply_account_id: format_public_account_id(supply_account_id_1),
|
||||
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: "A NAM1".to_owned(),
|
||||
|
||||
total_supply: 37,
|
||||
};
|
||||
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`
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 7,
|
||||
@ -137,9 +142,12 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Create new token
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id_2),
|
||||
supply_account_id: format_public_account_id(supply_account_id_2),
|
||||
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: "A NAM2".to_owned(),
|
||||
|
||||
total_supply: 37,
|
||||
};
|
||||
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`
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 7,
|
||||
@ -181,9 +191,12 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Send creation tx
|
||||
let subcommand = AmmProgramAgnosticSubcommand::New {
|
||||
user_holding_a: format_public_account_id(recipient_account_id_1),
|
||||
user_holding_b: format_public_account_id(recipient_account_id_2),
|
||||
user_holding_lp: format_public_account_id(user_holding_lp),
|
||||
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
|
||||
user_holding_a_label: None,
|
||||
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_b: 3,
|
||||
};
|
||||
@ -223,9 +236,11 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Make swap
|
||||
|
||||
let subcommand = AmmProgramAgnosticSubcommand::Swap {
|
||||
user_holding_a: format_public_account_id(recipient_account_id_1),
|
||||
user_holding_b: format_public_account_id(recipient_account_id_2),
|
||||
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
|
||||
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
|
||||
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,
|
||||
min_amount_out: 1,
|
||||
token_definition: definition_account_id_1.to_string(),
|
||||
@ -266,9 +281,11 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Make swap
|
||||
|
||||
let subcommand = AmmProgramAgnosticSubcommand::Swap {
|
||||
user_holding_a: format_public_account_id(recipient_account_id_1),
|
||||
user_holding_b: format_public_account_id(recipient_account_id_2),
|
||||
let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput {
|
||||
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
|
||||
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,
|
||||
min_amount_out: 1,
|
||||
token_definition: definition_account_id_2.to_string(),
|
||||
@ -310,9 +327,12 @@ async fn amm_public() -> Result<()> {
|
||||
// Add liquidity
|
||||
|
||||
let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity {
|
||||
user_holding_a: format_public_account_id(recipient_account_id_1),
|
||||
user_holding_b: format_public_account_id(recipient_account_id_2),
|
||||
user_holding_lp: format_public_account_id(user_holding_lp),
|
||||
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
|
||||
user_holding_a_label: None,
|
||||
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,
|
||||
max_amount_a: 2,
|
||||
max_amount_b: 2,
|
||||
@ -354,9 +374,12 @@ async fn amm_public() -> Result<()> {
|
||||
// Remove liquidity
|
||||
|
||||
let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity {
|
||||
user_holding_a: format_public_account_id(recipient_account_id_1),
|
||||
user_holding_b: format_public_account_id(recipient_account_id_2),
|
||||
user_holding_lp: format_public_account_id(user_holding_lp),
|
||||
user_holding_a: Some(format_public_account_id(recipient_account_id_1)),
|
||||
user_holding_a_label: None,
|
||||
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,
|
||||
min_amount_a: 1,
|
||||
min_amount_b: 1,
|
||||
@ -397,3 +420,188 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
674
integration_tests/tests/ata.rs
Normal file
674
integration_tests/tests/ata.rs
Normal 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(())
|
||||
}
|
||||
@ -24,8 +24,10 @@ async fn private_transfer_to_owned_account() -> Result<()> {
|
||||
let to: AccountId = ctx.existing_private_accounts()[1];
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -63,8 +65,10 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
let to_vpk = Secp256k1Point::from_scalar(to_npk.0);
|
||||
|
||||
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_label: None,
|
||||
to_npk: Some(to_npk_string),
|
||||
to_vpk: Some(hex::encode(to_vpk.0)),
|
||||
amount: 100,
|
||||
@ -111,8 +115,10 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
assert_eq!(from_acc.balance, 10000);
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
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)
|
||||
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_label: None,
|
||||
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
|
||||
amount: 100,
|
||||
@ -222,8 +230,10 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
|
||||
let to: AccountId = ctx.existing_private_accounts()[1];
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -264,8 +274,10 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_public_accounts()[0];
|
||||
|
||||
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_label: None,
|
||||
to_npk: Some(to_npk_string),
|
||||
to_vpk: Some(hex::encode(to_vpk.0)),
|
||||
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
|
||||
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_label: None,
|
||||
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
|
||||
amount: 100,
|
||||
@ -383,7 +397,8 @@ async fn initialize_private_account() -> Result<()> {
|
||||
};
|
||||
|
||||
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?;
|
||||
|
||||
@ -415,3 +430,100 @@ async fn initialize_private_account() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -17,8 +17,10 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
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");
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -109,8 +113,10 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 1_000_000,
|
||||
@ -147,8 +153,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
|
||||
// First transfer
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -179,8 +187,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
|
||||
// Second transfer
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -226,7 +236,8 @@ async fn initialize_public_account() -> Result<()> {
|
||||
};
|
||||
|
||||
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?;
|
||||
|
||||
@ -245,3 +256,97 @@ async fn initialize_public_account() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -1,38 +1,68 @@
|
||||
#![expect(
|
||||
clippy::shadow_unrelated,
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
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 nssa::AccountId;
|
||||
use tokio::test;
|
||||
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
|
||||
|
||||
/// Timeout in milliseconds to reliably await for block finalization.
|
||||
const L2_TO_L1_TIMEOUT_MILLIS: u64 = 600_000;
|
||||
/// Maximum time to wait for the indexer to catch up to the sequencer.
|
||||
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]
|
||||
async fn indexer_test_run() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
// RUN OBSERVATION
|
||||
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_seq =
|
||||
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?;
|
||||
|
||||
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}");
|
||||
|
||||
assert!(last_block_indexer > 1);
|
||||
@ -44,15 +74,8 @@ async fn indexer_test_run() -> Result<()> {
|
||||
async fn indexer_block_batching() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
// WAIT
|
||||
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 = ctx
|
||||
.indexer_client()
|
||||
.get_last_finalized_block_id()
|
||||
.await
|
||||
.unwrap();
|
||||
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await;
|
||||
|
||||
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 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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -113,9 +138,40 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
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");
|
||||
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
|
||||
.indexer_client()
|
||||
@ -147,3 +203,76 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
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_label: None,
|
||||
to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)),
|
||||
to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)),
|
||||
amount: 100,
|
||||
@ -143,8 +145,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
// Send to first private account
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 100,
|
||||
@ -153,8 +157,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
// Send to second private account
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 101,
|
||||
@ -191,8 +197,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
// Send to first public account
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 102,
|
||||
@ -201,8 +209,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
// Send to second public account
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 103,
|
||||
@ -264,8 +274,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
// Test that restored accounts can send transactions
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 10,
|
||||
@ -273,8 +285,10 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: 11,
|
||||
|
||||
@ -52,7 +52,8 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to: winner_account_id_formatted,
|
||||
to: Some(winner_account_id_formatted),
|
||||
to_label: None,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@ -106,7 +107,8 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to: winner_account_id_formatted,
|
||||
to: Some(winner_account_id_formatted),
|
||||
to_label: None,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@ -137,7 +139,8 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
|
||||
|
||||
let pinata_prize = 150;
|
||||
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
|
||||
@ -175,7 +178,10 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
||||
|
||||
let pinata_prize = 150;
|
||||
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
|
||||
@ -239,7 +245,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
||||
|
||||
// Initialize account under auth transfer program
|
||||
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?;
|
||||
|
||||
@ -254,7 +261,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
||||
|
||||
// Claim pinata to the new private account
|
||||
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
|
||||
|
||||
@ -11,10 +11,13 @@ use integration_tests::{
|
||||
NSSA_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext,
|
||||
};
|
||||
use log::info;
|
||||
use nssa::{AccountId, program::Program};
|
||||
use nssa::program::Program;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::cli::Command;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
};
|
||||
|
||||
#[test]
|
||||
async fn deploy_and_execute_program() -> Result<()> {
|
||||
@ -40,14 +43,31 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
// logic)
|
||||
let bytecode = std::fs::read(binary_filepath)?;
|
||||
let data_changer = Program::new(bytecode)?;
|
||||
let account_id: AccountId = "11".repeat(16).parse()?;
|
||||
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
panic!("Expected RegisterAccount return value");
|
||||
};
|
||||
|
||||
let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?;
|
||||
let private_key = ctx
|
||||
.wallet()
|
||||
.get_account_public_signing_key(account_id)
|
||||
.unwrap();
|
||||
let message = nssa::public_transaction::Message::try_new(
|
||||
data_changer.id(),
|
||||
vec![account_id],
|
||||
vec![],
|
||||
nonces,
|
||||
vec![0],
|
||||
)?;
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[private_key]);
|
||||
let transaction = nssa::PublicTransaction::new(message, witness_set);
|
||||
let _response = ctx
|
||||
.sequencer_client()
|
||||
@ -64,7 +84,7 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
assert_eq!(post_state_account.program_owner, data_changer.id());
|
||||
assert_eq!(post_state_account.balance, 0);
|
||||
assert_eq!(post_state_account.data.as_ref(), &[0]);
|
||||
assert_eq!(post_state_account.nonce.0, 0);
|
||||
assert_eq!(post_state_account.nonce.0, 1);
|
||||
|
||||
info!("Successfully deployed and executed program");
|
||||
|
||||
|
||||
@ -79,8 +79,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id),
|
||||
supply_account_id: format_public_account_id(supply_account_id),
|
||||
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: name.clone(),
|
||||
total_supply,
|
||||
};
|
||||
@ -126,8 +128,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
// Transfer 7 tokens from supply_acc to recipient_account_id
|
||||
let transfer_amount = 7;
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: transfer_amount,
|
||||
@ -171,8 +175,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
// Burn 3 tokens from recipient_acc
|
||||
let burn_amount = 3;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
||||
definition: format_public_account_id(definition_account_id),
|
||||
holder: format_public_account_id(recipient_account_id),
|
||||
definition: Some(format_public_account_id(definition_account_id)),
|
||||
definition_label: None,
|
||||
holder: Some(format_public_account_id(recipient_account_id)),
|
||||
holder_label: None,
|
||||
amount: burn_amount,
|
||||
};
|
||||
|
||||
@ -215,8 +221,10 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
// Mint 10 tokens at recipient_acc
|
||||
let mint_amount = 10;
|
||||
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_label: None,
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
amount: mint_amount,
|
||||
@ -319,8 +327,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id),
|
||||
supply_account_id: format_private_account_id(supply_account_id),
|
||||
definition_account_id: Some(format_public_account_id(definition_account_id)),
|
||||
definition_account_label: None,
|
||||
supply_account_id: Some(format_private_account_id(supply_account_id)),
|
||||
supply_account_label: None,
|
||||
name: name.clone(),
|
||||
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
|
||||
let transfer_amount = 7;
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: transfer_amount,
|
||||
@ -383,8 +395,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
// Burn 3 tokens from recipient_acc
|
||||
let burn_amount = 3;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Burn {
|
||||
definition: format_public_account_id(definition_account_id),
|
||||
holder: format_private_account_id(recipient_account_id),
|
||||
definition: Some(format_public_account_id(definition_account_id)),
|
||||
definition_label: None,
|
||||
holder: Some(format_private_account_id(recipient_account_id)),
|
||||
holder_label: None,
|
||||
amount: burn_amount,
|
||||
};
|
||||
|
||||
@ -475,8 +489,10 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_private_account_id(definition_account_id),
|
||||
supply_account_id: format_public_account_id(supply_account_id),
|
||||
definition_account_id: Some(format_private_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: name.clone(),
|
||||
total_supply,
|
||||
};
|
||||
@ -544,8 +560,10 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
// Mint to public account
|
||||
let mint_amount_public = 10;
|
||||
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_label: None,
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
amount: mint_amount_public,
|
||||
@ -590,8 +608,10 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
// Mint to private account
|
||||
let mint_amount_private = 5;
|
||||
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_label: None,
|
||||
holder_npk: None,
|
||||
holder_vpk: None,
|
||||
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 total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_private_account_id(definition_account_id),
|
||||
supply_account_id: format_private_account_id(supply_account_id),
|
||||
definition_account_id: Some(format_private_account_id(definition_account_id)),
|
||||
definition_account_label: None,
|
||||
supply_account_id: Some(format_private_account_id(supply_account_id)),
|
||||
supply_account_label: None,
|
||||
name,
|
||||
total_supply,
|
||||
};
|
||||
@ -728,8 +750,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
||||
// Transfer tokens
|
||||
let transfer_amount = 7;
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: transfer_amount,
|
||||
@ -841,8 +865,10 @@ async fn shielded_token_transfer() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id),
|
||||
supply_account_id: format_public_account_id(supply_account_id),
|
||||
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,
|
||||
total_supply,
|
||||
};
|
||||
@ -855,8 +881,10 @@ async fn shielded_token_transfer() -> Result<()> {
|
||||
// Perform shielded transfer: public supply -> private recipient
|
||||
let transfer_amount = 7;
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: transfer_amount,
|
||||
@ -963,8 +991,10 @@ async fn deshielded_token_transfer() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_public_account_id(definition_account_id),
|
||||
supply_account_id: format_private_account_id(supply_account_id),
|
||||
definition_account_id: Some(format_public_account_id(definition_account_id)),
|
||||
definition_account_label: None,
|
||||
supply_account_id: Some(format_private_account_id(supply_account_id)),
|
||||
supply_account_label: None,
|
||||
name,
|
||||
total_supply,
|
||||
};
|
||||
@ -977,8 +1007,10 @@ async fn deshielded_token_transfer() -> Result<()> {
|
||||
// Perform deshielded transfer: private supply -> public recipient
|
||||
let transfer_amount = 7;
|
||||
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_label: None,
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
amount: transfer_amount,
|
||||
@ -1069,8 +1101,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
let name = "A NAME".to_owned();
|
||||
let total_supply = 37;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: format_private_account_id(definition_account_id),
|
||||
supply_account_id: format_private_account_id(supply_account_id),
|
||||
definition_account_id: Some(format_private_account_id(definition_account_id)),
|
||||
definition_account_label: None,
|
||||
supply_account_id: Some(format_private_account_id(supply_account_id)),
|
||||
supply_account_label: None,
|
||||
name,
|
||||
total_supply,
|
||||
};
|
||||
@ -1108,8 +1142,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
// Mint using claiming path (foreign account)
|
||||
let mint_amount = 9;
|
||||
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_label: None,
|
||||
holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)),
|
||||
holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)),
|
||||
amount: mint_amount,
|
||||
@ -1149,3 +1185,193 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -249,10 +249,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![1, 2],
|
||||
vec![
|
||||
(sender_npk.clone(), sender_ss),
|
||||
(recipient_npk.clone(), recipient_ss),
|
||||
],
|
||||
vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)],
|
||||
vec![sender_nsk],
|
||||
vec![Some(proof)],
|
||||
&program.into(),
|
||||
|
||||
@ -24,7 +24,6 @@ use log::info;
|
||||
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
|
||||
use nssa_core::program::DEFAULT_PROGRAM_ID;
|
||||
use tempfile::tempdir;
|
||||
use wallet::WalletCore;
|
||||
use wallet_ffi::{
|
||||
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
|
||||
FfiTransferResult, WalletHandle, error,
|
||||
@ -211,14 +210,6 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl
|
||||
})
|
||||
}
|
||||
|
||||
fn new_wallet_rust_with_default_config(password: &str) -> Result<WalletCore> {
|
||||
let tempdir = tempdir()?;
|
||||
let config_path = tempdir.path().join("wallet_config.json");
|
||||
let storage_path = tempdir.path().join("storage.json");
|
||||
|
||||
WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned())
|
||||
}
|
||||
|
||||
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
|
||||
let config_path = home.join("wallet_config.json");
|
||||
let storage_path = home.join("storage.json");
|
||||
@ -232,19 +223,8 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
|
||||
fn wallet_ffi_create_public_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
let n_accounts = 10;
|
||||
// First `n_accounts` public accounts created with Rust wallet
|
||||
let new_public_account_ids_rust = {
|
||||
let mut account_ids = Vec::new();
|
||||
|
||||
let mut wallet_rust = new_wallet_rust_with_default_config(password)?;
|
||||
for _ in 0..n_accounts {
|
||||
let account_id = wallet_rust.create_new_account_public(None).0;
|
||||
account_ids.push(*account_id.value());
|
||||
}
|
||||
account_ids
|
||||
};
|
||||
|
||||
// First `n_accounts` public accounts created with wallet FFI
|
||||
// Create `n_accounts` public accounts with wallet FFI
|
||||
let new_public_account_ids_ffi = unsafe {
|
||||
let mut account_ids = Vec::new();
|
||||
|
||||
@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
|
||||
account_ids
|
||||
};
|
||||
|
||||
assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust);
|
||||
// All returned IDs must be unique and non-zero
|
||||
assert_eq!(new_public_account_ids_ffi.len(), n_accounts);
|
||||
let unique: HashSet<_> = new_public_account_ids_ffi.iter().collect();
|
||||
assert_eq!(
|
||||
unique.len(),
|
||||
n_accounts,
|
||||
"Duplicate public account IDs returned"
|
||||
);
|
||||
assert!(
|
||||
new_public_account_ids_ffi
|
||||
.iter()
|
||||
.all(|id| *id != [0_u8; 32]),
|
||||
"Zero account ID returned"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
|
||||
fn wallet_ffi_create_private_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
let n_accounts = 10;
|
||||
// First `n_accounts` private accounts created with Rust wallet
|
||||
let new_private_account_ids_rust = {
|
||||
let mut account_ids = Vec::new();
|
||||
|
||||
let mut wallet_rust = new_wallet_rust_with_default_config(password)?;
|
||||
for _ in 0..n_accounts {
|
||||
let account_id = wallet_rust.create_new_account_private(None).0;
|
||||
account_ids.push(*account_id.value());
|
||||
}
|
||||
account_ids
|
||||
};
|
||||
|
||||
// First `n_accounts` private accounts created with wallet FFI
|
||||
// Create `n_accounts` private accounts with wallet FFI
|
||||
let new_private_account_ids_ffi = unsafe {
|
||||
let mut account_ids = Vec::new();
|
||||
|
||||
@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
|
||||
account_ids
|
||||
};
|
||||
|
||||
assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust);
|
||||
// All returned IDs must be unique and non-zero
|
||||
assert_eq!(new_private_account_ids_ffi.len(), n_accounts);
|
||||
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect();
|
||||
assert_eq!(
|
||||
unique.len(),
|
||||
n_accounts,
|
||||
"Duplicate private account IDs returned"
|
||||
);
|
||||
assert!(
|
||||
new_private_account_ids_ffi
|
||||
.iter()
|
||||
.all(|id| *id != [0_u8; 32]),
|
||||
"Zero account ID returned"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
|
||||
fn test_wallet_ffi_list_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
|
||||
// Create the wallet FFI
|
||||
let wallet_ffi_handle = unsafe {
|
||||
// Create the wallet FFI and track which account IDs were created as public/private
|
||||
let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
|
||||
let handle = new_wallet_ffi_with_default_config(password)?;
|
||||
// Create 5 public accounts and 5 private accounts
|
||||
let mut public_ids: Vec<[u8; 32]> = Vec::new();
|
||||
let mut private_ids: Vec<[u8; 32]> = Vec::new();
|
||||
|
||||
// Create 5 public accounts and 5 private accounts, recording their IDs
|
||||
for _ in 0..5 {
|
||||
let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
|
||||
wallet_ffi_create_account_public(handle, &raw mut out_account_id);
|
||||
public_ids.push(out_account_id.data);
|
||||
|
||||
wallet_ffi_create_account_private(handle, &raw mut out_account_id);
|
||||
private_ids.push(out_account_id.data);
|
||||
}
|
||||
|
||||
handle
|
||||
};
|
||||
|
||||
// Create the wallet Rust
|
||||
let wallet_rust = {
|
||||
let mut wallet = new_wallet_rust_with_default_config(password)?;
|
||||
// Create 5 public accounts and 5 private accounts
|
||||
for _ in 0..5 {
|
||||
wallet.create_new_account_public(None);
|
||||
wallet.create_new_account_private(None);
|
||||
}
|
||||
wallet
|
||||
(handle, public_ids, private_ids)
|
||||
};
|
||||
|
||||
// Get the account list with FFI method
|
||||
@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
|
||||
out_list
|
||||
};
|
||||
|
||||
let wallet_rust_account_ids = wallet_rust
|
||||
.storage()
|
||||
.user_data
|
||||
.account_ids()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Assert same number of elements between Rust and FFI result
|
||||
assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count);
|
||||
|
||||
let wallet_ffi_account_list_slice = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
wallet_ffi_account_list.entries,
|
||||
@ -396,37 +376,38 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
// Assert same account ids between Rust and FFI result
|
||||
assert_eq!(
|
||||
wallet_rust_account_ids
|
||||
.iter()
|
||||
.map(nssa::AccountId::value)
|
||||
.collect::<HashSet<_>>(),
|
||||
wallet_ffi_account_list_slice
|
||||
.iter()
|
||||
.map(|entry| &entry.account_id.data)
|
||||
.collect::<HashSet<_>>()
|
||||
);
|
||||
// All created accounts must appear in the list
|
||||
let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
|
||||
.iter()
|
||||
.filter(|e| e.is_public)
|
||||
.map(|e| e.account_id.data)
|
||||
.collect();
|
||||
let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
|
||||
.iter()
|
||||
.filter(|e| !e.is_public)
|
||||
.map(|e| e.account_id.data)
|
||||
.collect();
|
||||
|
||||
// Assert `is_pub` flag is correct in the FFI result
|
||||
for entry in wallet_ffi_account_list_slice {
|
||||
let account_id = AccountId::new(entry.account_id.data);
|
||||
let is_pub_default_in_rust_wallet = wallet_rust
|
||||
.storage()
|
||||
.user_data
|
||||
.default_pub_account_signing_keys
|
||||
.contains_key(&account_id);
|
||||
let is_pub_key_tree_wallet_rust = wallet_rust
|
||||
.storage()
|
||||
.user_data
|
||||
.public_key_tree
|
||||
.account_id_map
|
||||
.contains_key(&account_id);
|
||||
|
||||
let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust;
|
||||
|
||||
assert_eq!(entry.is_public, is_public_in_rust_wallet);
|
||||
for id in &created_public_ids {
|
||||
assert!(
|
||||
listed_public_ids.contains(id),
|
||||
"Created public account not found in list with is_public=true"
|
||||
);
|
||||
}
|
||||
for id in &created_private_ids {
|
||||
assert!(
|
||||
listed_private_ids.contains(id),
|
||||
"Created private account not found in list with is_public=false"
|
||||
);
|
||||
}
|
||||
|
||||
// Total listed accounts must be at least the number we created
|
||||
assert!(
|
||||
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(),
|
||||
"Listed account count ({}) is less than the number of created accounts ({})",
|
||||
wallet_ffi_account_list.count,
|
||||
created_public_ids.len() + created_private_ids.len()
|
||||
);
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list);
|
||||
@ -924,7 +905,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
|
||||
let home = tempfile::tempdir()?;
|
||||
let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
let from: FfiBytes32 = (&ctx.ctx().existing_private_accounts()[0]).into();
|
||||
let to = FfiBytes32::from_bytes([37; 32]);
|
||||
let to: FfiBytes32 = (&ctx.ctx().existing_public_accounts()[0]).into();
|
||||
let amount: [u8; 16] = 100_u128.to_le_bytes();
|
||||
|
||||
let mut transfer_result = FfiTransferResult::default();
|
||||
@ -967,7 +948,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
|
||||
};
|
||||
|
||||
assert_eq!(from_balance, 9900);
|
||||
assert_eq!(to_balance, 100);
|
||||
assert_eq!(to_balance, 10100);
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
|
||||
@ -8,8 +8,6 @@ license = { workspace = true }
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
secp256k1 = "0.31.1"
|
||||
|
||||
nssa.workspace = true
|
||||
nssa_core.workspace = true
|
||||
common.workspace = true
|
||||
|
||||
@ -137,11 +137,12 @@ impl<'a> From<&'a mut ChildKeysPrivate> for &'a mut (KeyChain, nssa::Account) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nssa_core::NullifierSecretKey;
|
||||
use nssa_core::{NullifierPublicKey, NullifierSecretKey};
|
||||
|
||||
use super::*;
|
||||
use crate::key_management::{self, secret_holders::ViewingSecretKey};
|
||||
|
||||
#[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")]
|
||||
#[test]
|
||||
fn master_key_generation() {
|
||||
let seed: [u8; 64] = [
|
||||
@ -153,7 +154,7 @@ mod tests {
|
||||
|
||||
let keys = ChildKeysPrivate::root(seed);
|
||||
|
||||
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
|
||||
let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([
|
||||
246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139,
|
||||
222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191,
|
||||
]);
|
||||
@ -168,7 +169,7 @@ mod tests {
|
||||
34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163,
|
||||
];
|
||||
|
||||
let expected_npk = nssa_core::NullifierPublicKey([
|
||||
let expected_npk: NullifierPublicKey = nssa_core::NullifierPublicKey([
|
||||
7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97,
|
||||
247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2,
|
||||
]);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use secp256k1::Scalar;
|
||||
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyNode;
|
||||
@ -13,7 +13,6 @@ pub struct ChildKeysPublic {
|
||||
}
|
||||
|
||||
impl ChildKeysPublic {
|
||||
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
|
||||
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
|
||||
let mut hash_input = vec![];
|
||||
|
||||
@ -21,16 +20,17 @@ impl ChildKeysPublic {
|
||||
// Non-harden.
|
||||
// BIP-032 compatibility requires 1-byte header from the public_key;
|
||||
// Not stored in `self.cpk.value()`.
|
||||
let sk = secp256k1::SecretKey::from_byte_array(*self.csk.value())
|
||||
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
|
||||
.expect("32 bytes, within curve order");
|
||||
let pk = secp256k1::PublicKey::from_secret_key(&secp256k1::Secp256k1::new(), &sk);
|
||||
hash_input.extend_from_slice(&secp256k1::PublicKey::serialize(&pk));
|
||||
let pk = sk.public_key();
|
||||
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
|
||||
} else {
|
||||
// Harden.
|
||||
hash_input.extend_from_slice(&[0_u8]);
|
||||
hash_input.extend_from_slice(self.csk.value());
|
||||
}
|
||||
|
||||
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
|
||||
hash_input.extend_from_slice(&cci.to_be_bytes());
|
||||
|
||||
hmac_sha512::HMAC::mac(hash_input, self.ccc)
|
||||
@ -41,7 +41,12 @@ impl KeyNode for ChildKeysPublic {
|
||||
fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
|
||||
|
||||
let csk = nssa::PrivateKey::try_new(*hash_value.first_chunk::<32>().unwrap()).unwrap();
|
||||
let csk = nssa::PrivateKey::try_new(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.expect("Expect a valid Private Key");
|
||||
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||
let cpk = nssa::PublicKey::new_from_private_key(&csk);
|
||||
|
||||
@ -56,26 +61,20 @@ impl KeyNode for ChildKeysPublic {
|
||||
fn nth_child(&self, cci: u32) -> Self {
|
||||
let hash_value = self.compute_hash_value(cci);
|
||||
|
||||
let csk = secp256k1::SecretKey::from_byte_array(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let csk = nssa::PrivateKey::try_new({
|
||||
let scalar = Scalar::from_be_bytes(*self.csk.value()).unwrap();
|
||||
let hash_value = hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32");
|
||||
|
||||
csk.add_tweak(&scalar)
|
||||
.expect("Expect a valid Scalar")
|
||||
.secret_bytes()
|
||||
let value_1 =
|
||||
k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar");
|
||||
let value_2 = k256::Scalar::from_repr((*self.csk.value()).into())
|
||||
.expect("Expect a valid k256 scalar");
|
||||
|
||||
let sum = value_1.add(&value_2);
|
||||
sum.to_bytes().into()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
secp256k1::constants::CURVE_ORDER >= *csk.value(),
|
||||
"Secret key cannot exceed curve order"
|
||||
);
|
||||
.expect("Expect a valid private key");
|
||||
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
|
||||
@ -42,10 +42,10 @@ impl KeyChain {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_mnemonic(passphrase);
|
||||
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||
@ -53,12 +53,15 @@ impl KeyChain {
|
||||
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
||||
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifier_public_key,
|
||||
viewing_public_key,
|
||||
}
|
||||
(
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifier_public_key,
|
||||
viewing_public_key,
|
||||
},
|
||||
mnemonic,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32];
|
||||
|
||||
/// Seed holder. Non-clonable to ensure that different holders use different seeds.
|
||||
/// Produces `TopSecretKeyHolder` objects.
|
||||
#[derive(Debug)]
|
||||
@ -20,17 +18,16 @@ pub struct SeedHolder {
|
||||
|
||||
/// Secret spending key object. Can produce `PrivateKeyHolder` objects.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SecretSpendingKey(pub(crate) [u8; 32]);
|
||||
pub struct SecretSpendingKey(pub [u8; 32]);
|
||||
|
||||
pub type ViewingSecretKey = Scalar;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
/// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret
|
||||
/// for recepient.
|
||||
#[expect(clippy::partial_pub_fields, reason = "TODO: fix later")]
|
||||
pub struct PrivateKeyHolder {
|
||||
pub nullifier_secret_key: NullifierSecretKey,
|
||||
pub(crate) viewing_secret_key: ViewingSecretKey,
|
||||
pub viewing_secret_key: ViewingSecretKey,
|
||||
}
|
||||
|
||||
impl SeedHolder {
|
||||
@ -49,9 +46,24 @@ impl SeedHolder {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_mnemonic(passphrase: String) -> Self {
|
||||
let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) {
|
||||
let mut entropy_bytes: [u8; 32] = [0; 32];
|
||||
OsRng.fill_bytes(&mut entropy_bytes);
|
||||
|
||||
let mnemonic =
|
||||
Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
(
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
},
|
||||
mnemonic,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self {
|
||||
let seed_wide = mnemonic.to_seed(passphrase);
|
||||
|
||||
Self {
|
||||
@ -176,12 +188,63 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_seeds_generated_same_from_same_mnemonic() {
|
||||
let mnemonic = "test_pass";
|
||||
fn two_seeds_recovered_same_from_same_mnemonic() {
|
||||
let passphrase = "test_pass";
|
||||
|
||||
let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned());
|
||||
let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned());
|
||||
// Generate a mnemonic with random entropy
|
||||
let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
|
||||
assert_eq!(seed_holder1.seed, seed_holder2.seed);
|
||||
// Recover from the same mnemonic
|
||||
let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase);
|
||||
|
||||
assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_generates_different_seeds_each_time() {
|
||||
let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic("");
|
||||
let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic("");
|
||||
|
||||
// Different entropy should produce different mnemonics and seeds
|
||||
assert_ne!(mnemonic1.to_string(), mnemonic2.to_string());
|
||||
assert_ne!(seed_holder1.seed, seed_holder2.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_generates_24_word_phrase() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
// 256 bits of entropy produces a 24-word mnemonic
|
||||
let word_count = mnemonic.to_string().split_whitespace().count();
|
||||
assert_eq!(word_count, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_mnemonic_produces_valid_seed_length() {
|
||||
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
assert_eq!(seed_holder.seed.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_passphrases_produce_different_seeds() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a");
|
||||
let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b");
|
||||
|
||||
// Same mnemonic but different passphrases should produce different seeds
|
||||
assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_passphrase_is_deterministic() {
|
||||
let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic("");
|
||||
|
||||
let seed1 = SeedHolder::from_mnemonic(&mnemonic, "");
|
||||
let seed2 = SeedHolder::from_mnemonic(&mnemonic, "");
|
||||
|
||||
// Same mnemonic and passphrase should always produce the same seed
|
||||
assert_eq!(seed1.seed, seed2.seed);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,11 +181,12 @@ impl NSSAUserData {
|
||||
|
||||
impl Default for NSSAUserData {
|
||||
fn default() -> Self {
|
||||
let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic("");
|
||||
Self::new_with_accounts(
|
||||
BTreeMap::new(),
|
||||
BTreeMap::new(),
|
||||
KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())),
|
||||
KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())),
|
||||
KeyTreePublic::new(&seed_holder),
|
||||
KeyTreePrivate::new(&seed_holder),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
nssa_core = { workspace = true, features = ["host"] }
|
||||
clock_core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
@ -19,7 +20,7 @@ sha2.workspace = true
|
||||
rand.workspace = true
|
||||
borsh.workspace = true
|
||||
hex.workspace = true
|
||||
secp256k1 = "0.31.1"
|
||||
k256.workspace = true
|
||||
risc0-binfmt = "3.0.2"
|
||||
log.workspace = true
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::{
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::Ciphertext,
|
||||
program::{ProgramId, ProgramOutput},
|
||||
program::{BlockValidityWindow, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -17,6 +17,7 @@ pub struct PrivacyPreservingCircuitInput {
|
||||
/// - `0` - public account
|
||||
/// - `1` - private account with authentication
|
||||
/// - `2` - private account without authentication
|
||||
/// - `3` - private PDA account
|
||||
pub visibility_mask: Vec<u8>,
|
||||
/// Public keys of private accounts.
|
||||
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,
|
||||
@ -36,6 +37,8 @@ pub struct PrivacyPreservingCircuitOutput {
|
||||
pub ciphertexts: Vec<Ciphertext>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
@ -101,6 +104,8 @@ mod tests {
|
||||
),
|
||||
[0xab; 32],
|
||||
)],
|
||||
block_validity_window: (1..).into(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
let bytes = output.to_bytes();
|
||||
let output_from_slice: PrivacyPreservingCircuitOutput = from_slice(&bytes).unwrap();
|
||||
|
||||
@ -21,3 +21,7 @@ pub mod program;
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub mod error;
|
||||
|
||||
pub type BlockId = u64;
|
||||
/// Unix timestamp in milliseconds.
|
||||
pub type Timestamp = u64;
|
||||
|
||||
@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Commitment, account::AccountId};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Hash))]
|
||||
pub struct NullifierPublicKey(pub [u8; 32]);
|
||||
|
||||
impl From<&NullifierPublicKey> for AccountId {
|
||||
@ -55,7 +55,7 @@ pub type NullifierSecretKey = [u8; 32];
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)
|
||||
)]
|
||||
pub struct Nullifier(pub(super) [u8; 32]);
|
||||
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(any(feature = "host", test))]
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::account::{Account, AccountId, AccountWithMetadata};
|
||||
use crate::{
|
||||
BlockId, NullifierPublicKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
};
|
||||
|
||||
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
|
||||
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 InstructionData = Vec<u32>;
|
||||
pub struct ProgramInput<T> {
|
||||
pub self_program_id: ProgramId,
|
||||
pub caller_program_id: Option<ProgramId>,
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
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
|
||||
/// seeds. PDAs allow programs to control namespaced account identifiers without
|
||||
/// collisions between programs.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct PdaSeed([u8; 32]);
|
||||
|
||||
impl PdaSeed {
|
||||
@ -30,8 +37,10 @@ impl PdaSeed {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ProgramId, &PdaSeed)> for AccountId {
|
||||
fn from(value: (&ProgramId, &PdaSeed)) -> Self {
|
||||
impl AccountId {
|
||||
/// 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 _};
|
||||
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
|
||||
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];
|
||||
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
|
||||
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[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(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
@ -58,6 +96,9 @@ pub struct ChainedCall {
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
/// The instruction data to pass.
|
||||
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>,
|
||||
}
|
||||
|
||||
@ -89,11 +130,28 @@ impl ChainedCall {
|
||||
/// A post state may optionally request that the executing program
|
||||
/// becomes the owner of the account (a “claim”). This is used to signal
|
||||
/// that the program intends to take ownership of the account.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))]
|
||||
pub struct AccountPostState {
|
||||
account: Account,
|
||||
claim: bool,
|
||||
claim: Option<Claim>,
|
||||
}
|
||||
|
||||
/// A claim request for an account, indicating that the executing program intends to take ownership
|
||||
/// of the account.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Claim {
|
||||
/// The program requests ownership of the account which was authorized by the signer.
|
||||
///
|
||||
/// Note that it's possible to successfully execute program outputting [`AccountPostState`] with
|
||||
/// `is_authorized == false` and `claim == Some(Claim::Authorized)`.
|
||||
/// This will give no error if program had authorization in pre state and may be useful
|
||||
/// if program decides to give up authorization for a chained call.
|
||||
Authorized,
|
||||
/// The program requests ownership of the account through a PDA. 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 {
|
||||
@ -103,7 +161,7 @@ impl AccountPostState {
|
||||
pub const fn new(account: Account) -> Self {
|
||||
Self {
|
||||
account,
|
||||
claim: false,
|
||||
claim: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,25 +169,27 @@ impl AccountPostState {
|
||||
/// This indicates that the executing program intends to claim the
|
||||
/// account as its own and is allowed to mutate it.
|
||||
#[must_use]
|
||||
pub const fn new_claimed(account: Account) -> Self {
|
||||
pub const fn new_claimed(account: Account, claim: Claim) -> Self {
|
||||
Self {
|
||||
account,
|
||||
claim: true,
|
||||
claim: Some(claim),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a post state that requests ownership of the account
|
||||
/// if the account's program owner is the default program ID.
|
||||
#[must_use]
|
||||
pub fn new_claimed_if_default(account: Account) -> Self {
|
||||
let claim = account.program_owner == DEFAULT_PROGRAM_ID;
|
||||
Self { account, claim }
|
||||
pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self {
|
||||
let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID;
|
||||
Self {
|
||||
account,
|
||||
claim: is_default_owner.then_some(claim),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this post state requests that the account
|
||||
/// be claimed (owned) by the executing program.
|
||||
/// Returns whether this post state requires a claim.
|
||||
#[must_use]
|
||||
pub const fn requires_claim(&self) -> bool {
|
||||
pub const fn required_claim(&self) -> Option<Claim> {
|
||||
self.claim
|
||||
}
|
||||
|
||||
@ -140,6 +200,7 @@ impl AccountPostState {
|
||||
}
|
||||
|
||||
/// Returns the underlying account.
|
||||
#[must_use]
|
||||
pub const fn account_mut(&mut self) -> &mut 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)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
#[must_use = "ProgramOutput does nothing unless written"]
|
||||
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.
|
||||
pub instruction_data: InstructionData,
|
||||
/// The account pre states the program received to produce this output.
|
||||
pub pre_states: Vec<AccountWithMetadata>,
|
||||
/// The account post states the program execution produced.
|
||||
pub post_states: Vec<AccountPostState>,
|
||||
/// The list of chained calls to other programs.
|
||||
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`.
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct WrappedBalanceSum {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WrappedBalanceSum {
|
||||
lo: u128,
|
||||
hi: u128,
|
||||
}
|
||||
@ -174,7 +429,7 @@ impl WrappedBalanceSum {
|
||||
///
|
||||
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
|
||||
/// 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 };
|
||||
|
||||
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]
|
||||
pub fn compute_authorized_pdas(
|
||||
pub fn compute_public_authorized_pdas(
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pda_seeds: &[PdaSeed],
|
||||
) -> HashSet<AccountId> {
|
||||
caller_program_id
|
||||
.map(|caller_program_id| {
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|pda_seed| AccountId::from((&caller_program_id, pda_seed)))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
let Some(caller) = caller_program_id else {
|
||||
return HashSet::new();
|
||||
};
|
||||
pda_seeds
|
||||
.iter()
|
||||
.map(|seed| AccountId::for_public_pda(&caller, seed))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reads the NSSA inputs from the guest environment.
|
||||
#[must_use]
|
||||
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 instruction_words: InstructionData = env::read();
|
||||
let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap();
|
||||
(
|
||||
ProgramInput {
|
||||
self_program_id,
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
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.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
|
||||
/// - `post_states`: The list of resulting accounts after executing the program logic.
|
||||
/// - `executing_program_id`: The identifier of the program that was executed.
|
||||
#[must_use]
|
||||
pub fn validate_execution(
|
||||
pre_states: &[AccountWithMetadata],
|
||||
post_states: &[AccountPostState],
|
||||
executing_program_id: ProgramId,
|
||||
) -> bool {
|
||||
) -> Result<(), ExecutionValidationError> {
|
||||
// 1. Check account ids are all different
|
||||
if !validate_uniqueness_of_account_ids(pre_states) {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
|
||||
}
|
||||
|
||||
// 2. Lengths must match
|
||||
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) {
|
||||
// 3. Nonce must remain unchanged
|
||||
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
|
||||
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;
|
||||
@ -287,7 +599,11 @@ pub fn validate_execution(
|
||||
if post.account.balance < pre.account.balance
|
||||
&& 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
|
||||
@ -296,13 +612,20 @@ pub fn validate_execution(
|
||||
&& pre.account != Account::default()
|
||||
&& 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
|
||||
// account
|
||||
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) =
|
||||
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
let Some(total_balance_post_states) =
|
||||
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
|
||||
else {
|
||||
return false;
|
||||
return Err(ExecutionValidationError::BalanceSumOverflow);
|
||||
};
|
||||
|
||||
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 {
|
||||
@ -342,6 +668,135 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
|
||||
mod tests {
|
||||
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]
|
||||
fn post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
@ -351,10 +806,10 @@ mod tests {
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone());
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.requires_claim());
|
||||
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -369,7 +824,7 @@ mod tests {
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(!account_post_state.requires_claim());
|
||||
assert!(account_post_state.required_claim().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -386,4 +841,108 @@ mod tests {
|
||||
assert_eq!(account_post_state.account(), &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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
use std::io;
|
||||
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! ensure {
|
||||
($cond:expr, $err:expr) => {
|
||||
if !$cond {
|
||||
return Err($err);
|
||||
return Err($err.into());
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -17,7 +21,7 @@ pub enum NssaError {
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Program violated execution rules")]
|
||||
InvalidProgramBehavior,
|
||||
InvalidProgramBehavior(#[from] InvalidProgramBehaviorError),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
InstructionSerializationError(String),
|
||||
@ -29,15 +33,18 @@ pub enum NssaError {
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[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),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to execute program: {0}")]
|
||||
ProgramExecutionFailed(String),
|
||||
|
||||
#[error("Risc0 error: {0}")]
|
||||
#[error("Failed to prove program: {0}")]
|
||||
ProgramProveFailed(String),
|
||||
|
||||
#[error("Invalid transaction: {0}")]
|
||||
@ -69,6 +76,64 @@ pub enum NssaError {
|
||||
|
||||
#[error("Max account nonce reached")]
|
||||
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)]
|
||||
|
||||
@ -16,7 +16,11 @@ pub use program_deployment_transaction::ProgramDeploymentTransaction;
|
||||
pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID;
|
||||
pub use public_transaction::PublicTransaction;
|
||||
pub use signature::{PrivateKey, PublicKey, Signature};
|
||||
pub use state::V03State;
|
||||
pub use state::{
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
|
||||
CLOCK_PROGRAM_ACCOUNT_IDS, V03State,
|
||||
};
|
||||
pub use validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
pub mod encoding;
|
||||
pub mod error;
|
||||
@ -27,6 +31,7 @@ pub mod program_deployment_transaction;
|
||||
pub mod public_transaction;
|
||||
mod signature;
|
||||
mod state;
|
||||
mod validated_state_diff;
|
||||
|
||||
pub mod program_methods {
|
||||
include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs"));
|
||||
|
||||
@ -10,7 +10,7 @@ use nssa_core::{
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
error::NssaError,
|
||||
error::{InvalidProgramBehaviorError, NssaError},
|
||||
program::Program,
|
||||
program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
@ -87,15 +87,16 @@ pub fn execute_and_prove(
|
||||
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;
|
||||
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 {
|
||||
return Err(NssaError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
@ -112,10 +113,12 @@ pub fn execute_and_prove(
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies
|
||||
.get(&new_call.program_id)
|
||||
.ok_or(NssaError::InvalidProgramBehavior)?;
|
||||
chained_calls.push_front((new_call, next_program));
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
@ -153,12 +156,19 @@ pub fn execute_and_prove(
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<Receipt, NssaError> {
|
||||
// Write inputs to the program
|
||||
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();
|
||||
|
||||
// Prove the program
|
||||
@ -174,12 +184,13 @@ mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use nssa_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::NssaError,
|
||||
privacy_preserving_transaction::circuit::execute_and_prove,
|
||||
program::Program,
|
||||
state::{
|
||||
@ -364,4 +375,46 @@ mod tests {
|
||||
.unwrap();
|
||||
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(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, Nonce},
|
||||
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
@ -52,6 +53,8 @@ pub struct Message {
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
pub timestamp_validity_window: TimestampValidityWindow,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Message {
|
||||
@ -77,6 +80,8 @@ impl std::fmt::Debug for Message {
|
||||
)
|
||||
.field("new_commitments", &self.new_commitments)
|
||||
.field("new_nullifiers", &nullifiers)
|
||||
.field("block_validity_window", &self.block_validity_window)
|
||||
.field("timestamp_validity_window", &self.timestamp_validity_window)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@ -109,6 +114,8 @@ impl Message {
|
||||
encrypted_private_post_states,
|
||||
new_commitments: output.new_commitments,
|
||||
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,
|
||||
account::Account,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
@ -161,6 +169,8 @@ pub mod tests {
|
||||
encrypted_private_post_states,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
hash::Hash,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use super::{message::Message, witness_set::WitnessSet};
|
||||
use crate::{
|
||||
AccountId, V03State,
|
||||
error::NssaError,
|
||||
privacy_preserving_transaction::{circuit::Proof, message::EncryptedAccountData},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
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]
|
||||
pub const fn 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)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
|
||||
@ -8,7 +8,11 @@ use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
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.
|
||||
@ -50,13 +54,20 @@ impl Program {
|
||||
|
||||
pub(crate) fn execute(
|
||||
&self,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<ProgramOutput, NssaError> {
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
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();
|
||||
|
||||
// Execute the program (without proving)
|
||||
@ -76,34 +87,66 @@ impl Program {
|
||||
|
||||
/// Writes inputs to `env_builder` in the order expected by the programs.
|
||||
pub(crate) fn write_inputs(
|
||||
program_id: ProgramId,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &[u32],
|
||||
env_builder: &mut ExecutorEnvBuilder,
|
||||
) -> 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();
|
||||
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()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn authenticated_transfer_program() -> Self {
|
||||
// This unwrap won't panic since the `AUTHENTICATED_TRANSFER_ELF` comes from risc0 build of
|
||||
// `program_methods`
|
||||
Self::new(AUTHENTICATED_TRANSFER_ELF.to_vec()).unwrap()
|
||||
Self {
|
||||
id: AUTHENTICATED_TRANSFER_ID,
|
||||
elf: AUTHENTICATED_TRANSFER_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn token() -> Self {
|
||||
// This unwrap won't panic since the `TOKEN_ELF` comes from risc0 build of
|
||||
// `program_methods`
|
||||
Self::new(TOKEN_ELF.to_vec()).unwrap()
|
||||
Self {
|
||||
id: TOKEN_ID,
|
||||
elf: TOKEN_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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 {
|
||||
#[must_use]
|
||||
pub fn pinata() -> Self {
|
||||
// This unwrap won't panic since the `PINATA_ELF` comes from risc0 build of
|
||||
// `program_methods`
|
||||
Self::new(PINATA_ELF.to_vec()).unwrap()
|
||||
Self {
|
||||
id: PINATA_ID,
|
||||
elf: PINATA_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::non_ascii_literal, reason = "More readable")]
|
||||
pub fn pinata_token() -> Self {
|
||||
use crate::program_methods::PINATA_TOKEN_ELF;
|
||||
Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file")
|
||||
use crate::program_methods::{PINATA_TOKEN_ELF, PINATA_TOKEN_ID};
|
||||
Self {
|
||||
id: PINATA_TOKEN_ID,
|
||||
elf: PINATA_TOKEN_ELF.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,8 +177,9 @@ mod tests {
|
||||
use crate::{
|
||||
program::Program,
|
||||
program_methods::{
|
||||
AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID,
|
||||
TOKEN_ELF, TOKEN_ID,
|
||||
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, 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]
|
||||
pub fn changer_claimer() -> Self {
|
||||
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]
|
||||
pub fn malicious_authorization_changer() -> Self {
|
||||
use test_program_methods::{
|
||||
@ -279,10 +366,71 @@ mod tests {
|
||||
|
||||
#[must_use]
|
||||
pub fn modified_transfer_program() -> Self {
|
||||
use test_program_methods::MODIFIED_TRANSFER_ELF;
|
||||
// This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of
|
||||
// `program_methods`
|
||||
Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap()
|
||||
use test_program_methods::{MODIFIED_TRANSFER_ELF, MODIFIED_TRANSFER_ID};
|
||||
Self {
|
||||
id: MODIFIED_TRANSFER_ID,
|
||||
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()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(&[sender, recipient], &instruction_data)
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use crate::{
|
||||
V03State, error::NssaError, program::Program, program_deployment_transaction::message::Message,
|
||||
};
|
||||
use crate::program_deployment_transaction::message::Message;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct ProgramDeploymentTransaction {
|
||||
@ -22,19 +20,6 @@ impl ProgramDeploymentTransaction {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub(crate) fn validate_and_produce_public_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
) -> Result<Program, NssaError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(self.message.bytecode.clone())?;
|
||||
if state.programs().contains_key(&program.id()) {
|
||||
Err(NssaError::ProgramAlreadyExists)
|
||||
} else {
|
||||
Ok(program)
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
let bytes = self.to_bytes();
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use log::debug;
|
||||
use nssa_core::{
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{ChainedCall, DEFAULT_PROGRAM_ID, validate_execution},
|
||||
};
|
||||
use nssa_core::account::AccountId;
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::NssaError,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
use crate::public_transaction::{Message, WitnessSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct PublicTransaction {
|
||||
@ -66,180 +57,6 @@ impl PublicTransaction {
|
||||
hasher.update(&bytes);
|
||||
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)]
|
||||
@ -251,6 +68,7 @@ pub mod tests {
|
||||
error::NssaError,
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) {
|
||||
@ -264,7 +82,7 @@ pub mod tests {
|
||||
fn state_for_tests() -> V03State {
|
||||
let (_, _, addr1, addr2) = keys_for_tests();
|
||||
let initial_data = [(addr1, 10000), (addr2, 20000)];
|
||||
V03State::new_with_genesis_accounts(&initial_data, &[])
|
||||
V03State::new_with_genesis_accounts(&initial_data, vec![], 0)
|
||||
}
|
||||
|
||||
fn transaction_for_tests() -> PublicTransaction {
|
||||
@ -359,7 +177,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = tx.validate_and_produce_public_state_diff(&state);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -379,7 +197,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = tx.validate_and_produce_public_state_diff(&state);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -400,7 +218,7 @@ pub mod tests {
|
||||
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = tx.validate_and_produce_public_state_diff(&state);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -420,7 +238,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = tx.validate_and_produce_public_state_diff(&state);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
|
||||
@ -436,7 +254,7 @@ pub mod tests {
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = tx.validate_and_produce_public_state_diff(&state);
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
assert!(matches!(result, Err(NssaError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user