Merge remote-tracking branch 'upstream/main' into feat/validity-window-display

This commit is contained in:
ygd58 2026-04-24 22:38:03 +02:00
commit 75bca03d59
No known key found for this signature in database
GPG Key ID: 82B49AE8D5B28600
165 changed files with 8522 additions and 2441 deletions

View File

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

View File

@ -11,6 +11,10 @@ on:
- "**.md" - "**.md"
- "!.github/workflows/*.yml" - "!.github/workflows/*.yml"
permissions:
contents: read
pull-requests: read
name: General name: General
jobs: jobs:
@ -19,7 +23,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install nightly toolchain for rustfmt - name: Install nightly toolchain for rustfmt
run: rustup install nightly --profile minimal --component rustfmt run: rustup install nightly --profile minimal --component rustfmt
@ -32,7 +36,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install taplo-cli - name: Install taplo-cli
run: cargo install --locked taplo-cli run: cargo install --locked taplo-cli
@ -45,7 +49,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install active toolchain - name: Install active toolchain
run: rustup install run: rustup install
@ -61,7 +65,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- name: Install cargo-deny - name: Install cargo-deny
run: cargo install --locked cargo-deny run: cargo install --locked cargo-deny
@ -77,7 +81,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -106,7 +110,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -126,7 +130,7 @@ jobs:
env: env:
RISC0_DEV_MODE: "1" RISC0_DEV_MODE: "1"
RUST_LOG: "info" RUST_LOG: "info"
run: cargo nextest run --workspace --exclude integration_tests run: cargo nextest run --workspace --exclude integration_tests --all-features
integration-tests: integration-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -134,7 +138,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -156,35 +160,33 @@ jobs:
RUST_LOG: "info" RUST_LOG: "info"
run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer
# # TODO: Bring this back once we find the source of the errors. integration-tests-indexer:
# # runs-on: ubuntu-latest
# integration-tests-indexer: timeout-minutes: 60
# runs-on: ubuntu-latest steps:
# timeout-minutes: 60 - uses: actions/checkout@v5
# steps: with:
# - uses: actions/checkout@v5 ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
# with:
# ref: ${{ github.head_ref }}
# - uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
# - uses: ./.github/actions/install-risc0 - uses: ./.github/actions/install-risc0
# - uses: ./.github/actions/install-logos-blockchain-circuits - uses: ./.github/actions/install-logos-blockchain-circuits
# with: with:
# github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
# - name: Install active toolchain - name: Install active toolchain
# run: rustup install run: rustup install
# - name: Install nextest - name: Install nextest
# run: cargo install --locked cargo-nextest run: cargo install --locked cargo-nextest
# - name: Run tests - name: Run tests
# env: env:
# RISC0_DEV_MODE: "1" RISC0_DEV_MODE: "1"
# RUST_LOG: "info" RUST_LOG: "info"
# run: cargo nextest run -p integration_tests indexer -- --skip tps_test run: cargo nextest run -p integration_tests indexer -- --skip tps_test
valid-proof-test: valid-proof-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -192,7 +194,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-system-deps - uses: ./.github/actions/install-system-deps
@ -218,7 +220,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
- uses: ./.github/actions/install-risc0 - uses: ./.github/actions/install-risc0

View File

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

439
Cargo.lock generated
View File

@ -503,7 +503,7 @@ dependencies = [
"ark-ff 0.4.2", "ark-ff 0.4.2",
"ark-std 0.4.0", "ark-std 0.4.0",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber 0.2.25",
] ]
[[package]] [[package]]
@ -515,7 +515,7 @@ dependencies = [
"ark-ff 0.5.0", "ark-ff 0.5.0",
"ark-std 0.5.0", "ark-std 0.5.0",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber 0.2.25",
] ]
[[package]] [[package]]
@ -1137,7 +1137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
dependencies = [ dependencies = [
"prost 0.14.3", "prost 0.14.3",
"prost-types", "prost-types 0.14.3",
"tonic", "tonic",
"tonic-prost", "tonic-prost",
"ureq", "ureq",
@ -1462,6 +1462,14 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "clock_core"
version = "0.1.0"
dependencies = [
"borsh",
"nssa_core",
]
[[package]] [[package]]
name = "cobs" name = "cobs"
version = "0.3.0" version = "0.3.0"
@ -1511,6 +1519,7 @@ dependencies = [
"anyhow", "anyhow",
"base64 0.22.1", "base64 0.22.1",
"borsh", "borsh",
"clock_core",
"hex", "hex",
"log", "log",
"logos-blockchain-common-http-client", "logos-blockchain-common-http-client",
@ -1706,15 +1715,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "cpp_demangle" name = "cpp_demangle"
version = "0.4.5" version = "0.4.5"
@ -1757,6 +1757,15 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@ -1950,7 +1959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"syn 2.0.117", "syn 1.0.109",
] ]
[[package]] [[package]]
@ -3064,6 +3073,17 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]] [[package]]
name = "html-escape" name = "html-escape"
version = "0.2.13" version = "0.2.13"
@ -4370,7 +4390,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "logos-blockchain-blend-crypto" name = "logos-blockchain-blend-crypto"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"blake2", "blake2",
"logos-blockchain-groth16", "logos-blockchain-groth16",
@ -4384,10 +4404,11 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-blend-message" name = "logos-blockchain-blend-message"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"blake2", "blake2",
"derivative", "derivative",
"hex",
"itertools 0.14.0", "itertools 0.14.0",
"logos-blockchain-blend-crypto", "logos-blockchain-blend-crypto",
"logos-blockchain-blend-proofs", "logos-blockchain-blend-proofs",
@ -4406,7 +4427,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-blend-proofs" name = "logos-blockchain-blend-proofs"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"generic-array 1.3.5", "generic-array 1.3.5",
@ -4419,12 +4440,13 @@ dependencies = [
"num-bigint 0.4.6", "num-bigint 0.4.6",
"serde", "serde",
"thiserror 1.0.69", "thiserror 1.0.69",
"zeroize",
] ]
[[package]] [[package]]
name = "logos-blockchain-chain-broadcast-service" name = "logos-blockchain-chain-broadcast-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"derivative", "derivative",
@ -4440,7 +4462,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-chain-service" name = "logos-blockchain-chain-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -4455,6 +4477,7 @@ dependencies = [
"logos-blockchain-services-utils", "logos-blockchain-services-utils",
"logos-blockchain-storage-service", "logos-blockchain-storage-service",
"logos-blockchain-time-service", "logos-blockchain-time-service",
"logos-blockchain-tracing",
"logos-blockchain-utils", "logos-blockchain-utils",
"num-bigint 0.4.6", "num-bigint 0.4.6",
"overwatch", "overwatch",
@ -4470,7 +4493,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-circuits-prover" name = "logos-blockchain-circuits-prover"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"logos-blockchain-circuits-utils", "logos-blockchain-circuits-utils",
"tempfile", "tempfile",
@ -4479,7 +4502,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-circuits-utils" name = "logos-blockchain-circuits-utils"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"dirs", "dirs",
] ]
@ -4487,7 +4510,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-common-http-client" name = "logos-blockchain-common-http-client"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"futures", "futures",
"hex", "hex",
@ -4507,7 +4530,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-core" name = "logos-blockchain-core"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"ark-ff 0.4.2", "ark-ff 0.4.2",
"bincode", "bincode",
@ -4537,7 +4560,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-cryptarchia-engine" name = "logos-blockchain-cryptarchia-engine"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"cfg_eval", "cfg_eval",
"logos-blockchain-pol", "logos-blockchain-pol",
@ -4553,7 +4576,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-cryptarchia-sync" name = "logos-blockchain-cryptarchia-sync"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures", "futures",
@ -4570,7 +4593,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-groth16" name = "logos-blockchain-groth16"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"ark-bn254 0.4.0", "ark-bn254 0.4.0",
"ark-ec 0.4.2", "ark-ec 0.4.2",
@ -4588,11 +4611,12 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-http-api-common" name = "logos-blockchain-http-api-common"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"axum 0.7.9", "axum 0.7.9",
"logos-blockchain-core", "logos-blockchain-core",
"logos-blockchain-key-management-system-keys", "logos-blockchain-key-management-system-keys",
"logos-blockchain-tracing",
"serde", "serde",
"serde_json", "serde_json",
"serde_with", "serde_with",
@ -4602,7 +4626,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-key-management-system-keys" name = "logos-blockchain-key-management-system-keys"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -4628,7 +4652,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-key-management-system-macros" name = "logos-blockchain-key-management-system-macros"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4638,7 +4662,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-key-management-system-operators" name = "logos-blockchain-key-management-system-operators"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"logos-blockchain-blend-proofs", "logos-blockchain-blend-proofs",
@ -4654,12 +4678,13 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-key-management-system-service" name = "logos-blockchain-key-management-system-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"log", "log",
"logos-blockchain-key-management-system-keys", "logos-blockchain-key-management-system-keys",
"logos-blockchain-key-management-system-operators", "logos-blockchain-key-management-system-operators",
"logos-blockchain-tracing",
"overwatch", "overwatch",
"serde", "serde",
"thiserror 2.0.18", "thiserror 2.0.18",
@ -4670,7 +4695,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-ledger" name = "logos-blockchain-ledger"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"derivative", "derivative",
"logos-blockchain-blend-crypto", "logos-blockchain-blend-crypto",
@ -4687,6 +4712,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"rpds", "rpds",
"serde", "serde",
"serde_arrays",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
] ]
@ -4694,12 +4720,13 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-network-service" name = "logos-blockchain-network-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"futures", "futures",
"logos-blockchain-core", "logos-blockchain-core",
"logos-blockchain-cryptarchia-sync", "logos-blockchain-cryptarchia-sync",
"logos-blockchain-tracing",
"overwatch", "overwatch",
"serde", "serde",
"tokio", "tokio",
@ -4710,7 +4737,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-poc" name = "logos-blockchain-poc"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"logos-blockchain-circuits-prover", "logos-blockchain-circuits-prover",
"logos-blockchain-circuits-utils", "logos-blockchain-circuits-utils",
@ -4726,7 +4753,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-pol" name = "logos-blockchain-pol"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"astro-float", "astro-float",
"logos-blockchain-circuits-prover", "logos-blockchain-circuits-prover",
@ -4745,7 +4772,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-poq" name = "logos-blockchain-poq"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"logos-blockchain-circuits-prover", "logos-blockchain-circuits-prover",
"logos-blockchain-circuits-utils", "logos-blockchain-circuits-utils",
@ -4762,7 +4789,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-poseidon2" name = "logos-blockchain-poseidon2"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"ark-bn254 0.4.0", "ark-bn254 0.4.0",
"ark-ff 0.4.2", "ark-ff 0.4.2",
@ -4773,7 +4800,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-services-utils" name = "logos-blockchain-services-utils"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"futures", "futures",
@ -4788,13 +4815,14 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-storage-service" name = "logos-blockchain-storage-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"futures", "futures",
"logos-blockchain-core", "logos-blockchain-core",
"logos-blockchain-cryptarchia-engine", "logos-blockchain-cryptarchia-engine",
"logos-blockchain-tracing",
"overwatch", "overwatch",
"serde", "serde",
"thiserror 1.0.69", "thiserror 1.0.69",
@ -4805,12 +4833,13 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-time-service" name = "logos-blockchain-time-service"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"futures", "futures",
"log", "log",
"logos-blockchain-cryptarchia-engine", "logos-blockchain-cryptarchia-engine",
"logos-blockchain-tracing",
"overwatch", "overwatch",
"sntpc", "sntpc",
"thiserror 2.0.18", "thiserror 2.0.18",
@ -4820,10 +4849,33 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "logos-blockchain-tracing"
version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [
"opentelemetry",
"opentelemetry-appender-tracing",
"opentelemetry-http",
"opentelemetry-otlp",
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"rand 0.8.5",
"serde",
"tokio",
"tracing",
"tracing-appender",
"tracing-gelf",
"tracing-loki",
"tracing-opentelemetry",
"tracing-subscriber 0.3.23",
"url",
]
[[package]] [[package]]
name = "logos-blockchain-utils" name = "logos-blockchain-utils"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"blake2", "blake2",
@ -4840,7 +4892,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-utxotree" name = "logos-blockchain-utxotree"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"ark-ff 0.4.2", "ark-ff 0.4.2",
"logos-blockchain-groth16", "logos-blockchain-groth16",
@ -4854,7 +4906,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-witness-generator" name = "logos-blockchain-witness-generator"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"tempfile", "tempfile",
] ]
@ -4862,7 +4914,7 @@ dependencies = [
[[package]] [[package]]
name = "logos-blockchain-zksign" name = "logos-blockchain-zksign"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4"
dependencies = [ dependencies = [
"logos-blockchain-circuits-prover", "logos-blockchain-circuits-prover",
"logos-blockchain-circuits-utils", "logos-blockchain-circuits-utils",
@ -4876,6 +4928,16 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "loki-api"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdc38a304f59a03e6efa3876766a48c70a766a93f88341c3fff4212834b8e327"
dependencies = [
"prost 0.13.5",
"prost-types 0.13.5",
]
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
version = "0.1.2" version = "0.1.2"
@ -4989,6 +5051,21 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "matchit" name = "matchit"
version = "0.7.3" version = "0.7.3"
@ -5163,11 +5240,11 @@ dependencies = [
[[package]] [[package]]
name = "multihash" name = "multihash"
version = "0.19.3" version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c"
dependencies = [ dependencies = [
"core2", "no_std_io2",
"unsigned-varint", "unsigned-varint",
] ]
@ -5228,6 +5305,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "no_std_io2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "no_std_strings" name = "no_std_strings"
version = "0.1.3" version = "0.1.3"
@ -5259,6 +5345,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"borsh", "borsh",
"clock_core",
"env_logger", "env_logger",
"hex", "hex",
"hex-literal 1.1.0", "hex-literal 1.1.0",
@ -5295,6 +5382,15 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "num" name = "num"
version = "0.4.3" version = "0.4.3"
@ -5535,6 +5631,98 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "opentelemetry"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0"
dependencies = [
"futures-core",
"futures-sink",
"js-sys",
"pin-project-lite",
"thiserror 2.0.18",
]
[[package]]
name = "opentelemetry-appender-tracing"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2"
dependencies = [
"opentelemetry",
"tracing",
"tracing-core",
"tracing-subscriber 0.3.23",
]
[[package]]
name = "opentelemetry-http"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d"
dependencies = [
"async-trait",
"bytes",
"http",
"opentelemetry",
"reqwest",
]
[[package]]
name = "opentelemetry-otlp"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f"
dependencies = [
"http",
"opentelemetry",
"opentelemetry-http",
"opentelemetry-proto",
"opentelemetry_sdk",
"prost 0.14.3",
"reqwest",
"thiserror 2.0.18",
"tokio",
"tonic",
]
[[package]]
name = "opentelemetry-proto"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
dependencies = [
"opentelemetry",
"opentelemetry_sdk",
"prost 0.14.3",
"tonic",
"tonic-prost",
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846"
[[package]]
name = "opentelemetry_sdk"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd"
dependencies = [
"futures-channel",
"futures-executor",
"futures-util",
"opentelemetry",
"percent-encoding",
"rand 0.9.2",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
]
[[package]] [[package]]
name = "optfield" name = "optfield"
version = "0.4.0" version = "0.4.0"
@ -5897,6 +6085,7 @@ dependencies = [
"amm_program", "amm_program",
"ata_core", "ata_core",
"ata_program", "ata_program",
"clock_core",
"nssa_core", "nssa_core",
"risc0-zkvm", "risc0-zkvm",
"serde", "serde",
@ -5964,6 +6153,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "prost-types"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
"prost 0.13.5",
]
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.14.3" version = "0.14.3"
@ -6934,7 +7132,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs 0.26.11", "webpki-root-certs 0.26.11",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -6945,9 +7143,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.10" version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@ -7151,6 +7349,7 @@ dependencies = [
"serde_json", "serde_json",
"storage", "storage",
"tempfile", "tempfile",
"test_program_methods",
"testnet_initial_state", "testnet_initial_state",
"tokio", "tokio",
"url", "url",
@ -7215,6 +7414,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_arrays"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@ -7463,6 +7671,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -7516,6 +7733,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "snap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]] [[package]]
name = "sntpc" name = "sntpc"
version = "0.5.2" version = "0.5.2"
@ -7831,8 +8054,10 @@ dependencies = [
name = "test_programs" name = "test_programs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clock_core",
"nssa_core", "nssa_core",
"risc0-zkvm", "risc0-zkvm",
"serde",
] ]
[[package]] [[package]]
@ -7919,6 +8144,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "throw_error" name = "throw_error"
version = "0.3.1" version = "0.3.1"
@ -8319,6 +8553,18 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"thiserror 2.0.18",
"time",
"tracing-subscriber 0.3.23",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.31"
@ -8350,6 +8596,82 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tracing-gelf"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c0170f1bf67b749d4377c2da1d99d6e722600051ee53870cfb6f618611e29e"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"hostname",
"serde_json",
"thiserror 1.0.69",
"tokio",
"tokio-util",
"tracing-core",
"tracing-futures",
"tracing-subscriber 0.3.23",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-loki"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3beec919fbdf99d719de8eda6adae3281f8a5b71ae40431f44dc7423053d34"
dependencies = [
"loki-api",
"reqwest",
"serde",
"serde_json",
"snap",
"tokio",
"tokio-stream",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
"tracing-subscriber 0.3.23",
"url",
]
[[package]]
name = "tracing-opentelemetry"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc"
dependencies = [
"js-sys",
"opentelemetry",
"smallvec",
"tracing",
"tracing-core",
"tracing-subscriber 0.3.23",
"web-time",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-subscriber"
version = "0.2.25" version = "0.2.25"
@ -8359,6 +8681,24 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]] [[package]]
name = "triomphe" name = "triomphe"
version = "0.1.15" version = "0.1.15"
@ -8657,6 +8997,7 @@ dependencies = [
"async-stream", "async-stream",
"ata_core", "ata_core",
"base58", "base58",
"bip39",
"clap", "clap",
"common", "common",
"env_logger", "env_logger",

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{BlockId, Timestamp}; use nssa_core::BlockId;
pub use nssa_core::Timestamp;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
use crate::{HashType, transaction::NSSATransaction}; use crate::{HashType, transaction::NSSATransaction};
pub type MantleMsgId = [u8; 32]; pub type MantleMsgId = [u8; 32];
pub type BlockHash = HashType; pub type BlockHash = HashType;

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ The derivation works as follows:
``` ```
seed = SHA256(owner_id || definition_id) seed = SHA256(owner_id || definition_id)
ata_address = AccountId::from((ata_program_id, seed)) 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. Because the computation is pure, anyone who knows the owner and definition can reproduce the exact same ATA address — no network call required.

View File

@ -19,6 +19,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: greeting, instruction: greeting,
}, },
@ -50,5 +52,12 @@ fn main() {
// with the NSSA program rules. // with the NSSA program rules.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output. // called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.write();
} }

View File

@ -19,6 +19,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: greeting, instruction: greeting,
}, },
@ -57,5 +59,12 @@ fn main() {
// with the NSSA program rules. // with the NSSA program rules.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output. // called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.write();
} }

View File

@ -66,6 +66,8 @@ fn main() {
// Read input accounts. // Read input accounts.
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (function_id, data), instruction: (function_id, data),
}, },
@ -85,5 +87,12 @@ fn main() {
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output. // called to commit the output.
ProgramOutput::new(instruction_words, pre_states, post_states).write(); ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states,
post_states,
)
.write();
} }

View File

@ -27,6 +27,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (), instruction: (),
}, },
@ -55,7 +57,13 @@ fn main() {
// Write the outputs. // Write the outputs.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output. // called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) ProgramOutput::new(
.with_chained_calls(vec![chained_call]) self_program_id,
.write(); caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.with_chained_calls(vec![chained_call])
.write();
} }

View File

@ -33,6 +33,8 @@ fn main() {
// Read inputs // Read inputs
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: (), instruction: (),
}, },
@ -68,7 +70,13 @@ fn main() {
// Write the outputs. // Write the outputs.
// WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be
// called to commit the output. // called to commit the output.
ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) ProgramOutput::new(
.with_chained_calls(vec![chained_call]) self_program_id,
.write(); caller_program_id,
instruction_data,
vec![pre_state],
vec![post_state],
)
.with_chained_calls(vec![chained_call])
.write();
} }

View File

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

View File

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

View File

@ -57,11 +57,9 @@ impl IndexerCore {
let channel_genesis_msg_id = [0; 32]; let channel_genesis_msg_id = [0; 32];
let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id); let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id);
let initial_commitments: Option<Vec<nssa_core::Commitment>> = config let initial_private_accounts: Option<Vec<(nssa_core::Commitment, nssa_core::Nullifier)>> =
.initial_private_accounts config.initial_private_accounts.as_ref().map(|accounts| {
.as_ref() accounts
.map(|initial_commitments| {
initial_commitments
.iter() .iter()
.map(|init_comm_data| { .map(|init_comm_data| {
let npk = &init_comm_data.npk; let npk = &init_comm_data.npk;
@ -71,7 +69,10 @@ impl IndexerCore {
acc.program_owner = acc.program_owner =
nssa::program::Program::authenticated_transfer_program().id(); nssa::program::Program::authenticated_transfer_program().id();
nssa_core::Commitment::new(npk, &acc) (
nssa_core::Commitment::new(npk, &acc),
nssa_core::Nullifier::for_account_initialization(npk),
)
}) })
.collect() .collect()
}); });
@ -88,10 +89,11 @@ impl IndexerCore {
// If initial commitments or accounts are present in config, need to construct state from // If initial commitments or accounts are present in config, need to construct state from
// them // them
let state = if initial_commitments.is_some() || init_accs.is_some() { let state = if initial_private_accounts.is_some() || init_accs.is_some() {
let mut state = V03State::new_with_genesis_accounts( let mut state = V03State::new_with_genesis_accounts(
&init_accs.unwrap_or_default(), &init_accs.unwrap_or_default(),
&initial_commitments.unwrap_or_default(), initial_private_accounts.unwrap_or_default(),
genesis_block.header.timestamp,
); );
// ToDo: Remove after testnet // ToDo: Remove after testnet
@ -142,7 +144,22 @@ impl IndexerCore {
info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids);
for l2_block in l2_block_vec { for l2_block in l2_block_vec {
self.store.put_block(l2_block.clone(), l1_header).await?; // TODO: proper fix is to make the sequencer's genesis include a
// trailing `clock_invocation(0)` (and have the indexer's
// `open_db_with_genesis` not pre-apply state transitions) so the
// inscribed genesis can flow through `put_block` like any other
// block. For now we skip re-applying it.
//
// The channel-start (block_id == 1) is the sequencer's genesis
// inscription that we re-discover during initial search. The
// indexer already has its own locally-constructed genesis in
// the store from `open_db_with_genesis`, so re-applying the
// inscribed copy is both redundant and would fail the strict
// block validation in `put_block` (the inscribed genesis lacks
// the trailing clock invocation).
if l2_block.header.block_id != 1 {
self.store.put_block(l2_block.clone(), l1_header).await?;
}
yield Ok(l2_block); yield Ok(l2_block);
} }

View File

@ -138,7 +138,7 @@ pub struct Account {
} }
pub type BlockId = u64; pub type BlockId = u64;
pub type TimeStamp = u64; pub type Timestamp = u64;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct Block { pub struct Block {
@ -153,7 +153,7 @@ pub struct BlockHeader {
pub block_id: BlockId, pub block_id: BlockId,
pub prev_block_hash: HashType, pub prev_block_hash: HashType,
pub hash: HashType, pub hash: HashType,
pub timestamp: TimeStamp, pub timestamp: Timestamp,
pub signature: Signature, pub signature: Signature,
} }

View File

@ -121,7 +121,7 @@ impl InitialData {
self.private_accounts self.private_accounts
.iter() .iter()
.map(|(key_chain, account)| PrivateAccountPublicInitialData { .map(|(key_chain, account)| PrivateAccountPublicInitialData {
npk: key_chain.nullifier_public_key.clone(), npk: key_chain.nullifier_public_key,
account: account.clone(), account: account.clone(),
}) })
.collect() .collect()
@ -211,7 +211,7 @@ pub fn sequencer_config(
max_block_size, max_block_size,
mempool_max_size, mempool_max_size,
block_create_timeout, block_create_timeout,
retry_pending_blocks_timeout: Duration::from_mins(2), retry_pending_blocks_timeout: Duration::from_secs(5),
initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()),
initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()),
signing_key: [37; 32], signing_key: [37; 32],

View File

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

View File

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

View File

@ -68,8 +68,10 @@ async fn create_ata_initializes_holding_account() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply, total_supply,
}), }),
@ -130,8 +132,10 @@ async fn create_ata_is_idempotent() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply: 100, total_supply: 100,
}), }),
@ -208,8 +212,10 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply, total_supply,
}), }),
@ -256,8 +262,10 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send { Command::Token(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(sender_ata_id)), to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: fund_amount, amount: fund_amount,
@ -362,8 +370,10 @@ async fn create_ata_with_private_owner() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply: 100, total_supply: 100,
}), }),
@ -434,8 +444,10 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply, total_supply,
}), }),
@ -482,8 +494,10 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send { Command::Token(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(sender_ata_id)), to: Some(format_public_account_id(sender_ata_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: fund_amount, amount: fund_amount,
@ -556,8 +570,10 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::New { Command::Token(TokenProgramAgnosticSubcommand::New {
definition_account_id: format_public_account_id(definition_account_id), definition_account_id: Some(format_public_account_id(definition_account_id)),
supply_account_id: format_public_account_id(supply_account_id), definition_account_label: None,
supply_account_id: Some(format_public_account_id(supply_account_id)),
supply_account_label: None,
name: "TEST".to_owned(), name: "TEST".to_owned(),
total_supply, total_supply,
}), }),
@ -592,8 +608,10 @@ async fn burn_via_ata_private_owner() -> Result<()> {
wallet::cli::execute_subcommand( wallet::cli::execute_subcommand(
ctx.wallet_mut(), ctx.wallet_mut(),
Command::Token(TokenProgramAgnosticSubcommand::Send { Command::Token(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(holder_ata_id)), to: Some(format_public_account_id(holder_ata_id)),
to_label: None,
to_npk: None, to_npk: None,
to_vpk: None, to_vpk: None,
amount: fund_amount, amount: fund_amount,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ use log::info;
use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program}; use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program};
use nssa_core::program::DEFAULT_PROGRAM_ID; use nssa_core::program::DEFAULT_PROGRAM_ID;
use tempfile::tempdir; use tempfile::tempdir;
use wallet::WalletCore;
use wallet_ffi::{ use wallet_ffi::{
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
FfiTransferResult, WalletHandle, error, FfiTransferResult, WalletHandle, error,
@ -211,14 +210,6 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl
}) })
} }
fn new_wallet_rust_with_default_config(password: &str) -> Result<WalletCore> {
let tempdir = tempdir()?;
let config_path = tempdir.path().join("wallet_config.json");
let storage_path = tempdir.path().join("storage.json");
WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned())
}
fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
let config_path = home.join("wallet_config.json"); let config_path = home.join("wallet_config.json");
let storage_path = home.join("storage.json"); let storage_path = home.join("storage.json");
@ -232,19 +223,8 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
fn wallet_ffi_create_public_accounts() -> Result<()> { fn wallet_ffi_create_public_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
let n_accounts = 10; let n_accounts = 10;
// First `n_accounts` public accounts created with Rust wallet
let new_public_account_ids_rust = {
let mut account_ids = Vec::new();
let mut wallet_rust = new_wallet_rust_with_default_config(password)?; // Create `n_accounts` public accounts with wallet FFI
for _ in 0..n_accounts {
let account_id = wallet_rust.create_new_account_public(None).0;
account_ids.push(*account_id.value());
}
account_ids
};
// First `n_accounts` public accounts created with wallet FFI
let new_public_account_ids_ffi = unsafe { let new_public_account_ids_ffi = unsafe {
let mut account_ids = Vec::new(); let mut account_ids = Vec::new();
@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
account_ids account_ids
}; };
assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust); // All returned IDs must be unique and non-zero
assert_eq!(new_public_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_public_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate public account IDs returned"
);
assert!(
new_public_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
);
Ok(()) Ok(())
} }
@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
fn wallet_ffi_create_private_accounts() -> Result<()> { fn wallet_ffi_create_private_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
let n_accounts = 10; let n_accounts = 10;
// First `n_accounts` private accounts created with Rust wallet // Create `n_accounts` private accounts with wallet FFI
let new_private_account_ids_rust = {
let mut account_ids = Vec::new();
let mut wallet_rust = new_wallet_rust_with_default_config(password)?;
for _ in 0..n_accounts {
let account_id = wallet_rust.create_new_account_private(None).0;
account_ids.push(*account_id.value());
}
account_ids
};
// First `n_accounts` private accounts created with wallet FFI
let new_private_account_ids_ffi = unsafe { let new_private_account_ids_ffi = unsafe {
let mut account_ids = Vec::new(); let mut account_ids = Vec::new();
@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
account_ids account_ids
}; };
assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust); // All returned IDs must be unique and non-zero
assert_eq!(new_private_account_ids_ffi.len(), n_accounts);
let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect();
assert_eq!(
unique.len(),
n_accounts,
"Duplicate private account IDs returned"
);
assert!(
new_private_account_ids_ffi
.iter()
.all(|id| *id != [0_u8; 32]),
"Zero account ID returned"
);
Ok(()) Ok(())
} }
@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
fn test_wallet_ffi_list_accounts() -> Result<()> { fn test_wallet_ffi_list_accounts() -> Result<()> {
let password = "password_for_tests"; let password = "password_for_tests";
// Create the wallet FFI // Create the wallet FFI and track which account IDs were created as public/private
let wallet_ffi_handle = unsafe { let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe {
let handle = new_wallet_ffi_with_default_config(password)?; let handle = new_wallet_ffi_with_default_config(password)?;
// Create 5 public accounts and 5 private accounts let mut public_ids: Vec<[u8; 32]> = Vec::new();
let mut private_ids: Vec<[u8; 32]> = Vec::new();
// Create 5 public accounts and 5 private accounts, recording their IDs
for _ in 0..5 { for _ in 0..5 {
let mut out_account_id = FfiBytes32::from_bytes([0; 32]); let mut out_account_id = FfiBytes32::from_bytes([0; 32]);
wallet_ffi_create_account_public(handle, &raw mut out_account_id); wallet_ffi_create_account_public(handle, &raw mut out_account_id);
public_ids.push(out_account_id.data);
wallet_ffi_create_account_private(handle, &raw mut out_account_id); wallet_ffi_create_account_private(handle, &raw mut out_account_id);
private_ids.push(out_account_id.data);
} }
handle (handle, public_ids, private_ids)
};
// Create the wallet Rust
let wallet_rust = {
let mut wallet = new_wallet_rust_with_default_config(password)?;
// Create 5 public accounts and 5 private accounts
for _ in 0..5 {
wallet.create_new_account_public(None);
wallet.create_new_account_private(None);
}
wallet
}; };
// Get the account list with FFI method // Get the account list with FFI method
@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
out_list out_list
}; };
let wallet_rust_account_ids = wallet_rust
.storage()
.user_data
.account_ids()
.collect::<Vec<_>>();
// Assert same number of elements between Rust and FFI result
assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count);
let wallet_ffi_account_list_slice = unsafe { let wallet_ffi_account_list_slice = unsafe {
core::slice::from_raw_parts( core::slice::from_raw_parts(
wallet_ffi_account_list.entries, wallet_ffi_account_list.entries,
@ -396,37 +376,38 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
) )
}; };
// Assert same account ids between Rust and FFI result // All created accounts must appear in the list
assert_eq!( let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
wallet_rust_account_ids .iter()
.iter() .filter(|e| e.is_public)
.map(nssa::AccountId::value) .map(|e| e.account_id.data)
.collect::<HashSet<_>>(), .collect();
wallet_ffi_account_list_slice let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice
.iter() .iter()
.map(|entry| &entry.account_id.data) .filter(|e| !e.is_public)
.collect::<HashSet<_>>() .map(|e| e.account_id.data)
); .collect();
// Assert `is_pub` flag is correct in the FFI result for id in &created_public_ids {
for entry in wallet_ffi_account_list_slice { assert!(
let account_id = AccountId::new(entry.account_id.data); listed_public_ids.contains(id),
let is_pub_default_in_rust_wallet = wallet_rust "Created public account not found in list with is_public=true"
.storage() );
.user_data
.default_pub_account_signing_keys
.contains_key(&account_id);
let is_pub_key_tree_wallet_rust = wallet_rust
.storage()
.user_data
.public_key_tree
.account_id_map
.contains_key(&account_id);
let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust;
assert_eq!(entry.is_public, is_public_in_rust_wallet);
} }
for id in &created_private_ids {
assert!(
listed_private_ids.contains(id),
"Created private account not found in list with is_public=false"
);
}
// Total listed accounts must be at least the number we created
assert!(
wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(),
"Listed account count ({}) is less than the number of created accounts ({})",
wallet_ffi_account_list.count,
created_public_ids.len() + created_private_ids.len()
);
unsafe { unsafe {
wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list); wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list);

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies] [dependencies]
nssa_core = { workspace = true, features = ["host"] } nssa_core = { workspace = true, features = ["host"] }
clock_core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true

View File

@ -17,6 +17,7 @@ pub struct PrivacyPreservingCircuitInput {
/// - `0` - public account /// - `0` - public account
/// - `1` - private account with authentication /// - `1` - private account with authentication
/// - `2` - private account without authentication /// - `2` - private account without authentication
/// - `3` - private PDA account
pub visibility_mask: Vec<u8>, pub visibility_mask: Vec<u8>,
/// Public keys of private accounts. /// Public keys of private accounts.
pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>,

View File

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

View File

@ -6,7 +6,7 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
BlockId, Timestamp, BlockId, NullifierPublicKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata}, account::{Account, AccountId, AccountWithMetadata},
}; };
@ -16,6 +16,8 @@ pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub type ProgramId = [u32; 8]; pub type ProgramId = [u32; 8];
pub type InstructionData = Vec<u32>; pub type InstructionData = Vec<u32>;
pub struct ProgramInput<T> { pub struct ProgramInput<T> {
pub self_program_id: ProgramId,
pub caller_program_id: Option<ProgramId>,
pub pre_states: Vec<AccountWithMetadata>, pub pre_states: Vec<AccountWithMetadata>,
pub instruction: T, pub instruction: T,
} }
@ -25,7 +27,7 @@ pub struct ProgramInput<T> {
/// Each program can derive up to `2^256` unique account IDs by choosing different /// Each program can derive up to `2^256` unique account IDs by choosing different
/// seeds. PDAs allow programs to control namespaced account identifiers without /// seeds. PDAs allow programs to control namespaced account identifiers without
/// collisions between programs. /// collisions between programs.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PdaSeed([u8; 32]); pub struct PdaSeed([u8; 32]);
impl PdaSeed { impl PdaSeed {
@ -35,8 +37,10 @@ impl PdaSeed {
} }
} }
impl From<(&ProgramId, &PdaSeed)> for AccountId { impl AccountId {
fn from(value: (&ProgramId, &PdaSeed)) -> Self { /// Derives an [`AccountId`] for a public PDA from the program ID and seed.
#[must_use]
pub fn for_public_pda(program_id: &ProgramId, seed: &PdaSeed) -> Self {
use risc0_zkvm::sha::{Impl, Sha256 as _}; use risc0_zkvm::sha::{Impl, Sha256 as _};
const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] = const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] =
b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00"; b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00";
@ -44,9 +48,38 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId {
let mut bytes = [0; 96]; let mut bytes = [0; 96];
bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX); bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX);
let program_id_bytes: &[u8] = let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[u8]"); bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes); bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..].copy_from_slice(&value.1.0); bytes[64..].copy_from_slice(&seed.0);
Self::new(
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("Hash output must be exactly 32 bytes long"),
)
}
/// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier
/// public key.
///
/// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the
/// derivation, making the address unique per group of controllers sharing viewing keys.
#[must_use]
pub fn for_private_pda(
program_id: &ProgramId,
seed: &PdaSeed,
npk: &NullifierPublicKey,
) -> Self {
use risc0_zkvm::sha::{Impl, Sha256 as _};
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
let mut bytes = [0_u8; 128];
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
let program_id_bytes: &[u8] =
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
bytes[32..64].copy_from_slice(program_id_bytes);
bytes[64..96].copy_from_slice(&seed.0);
bytes[96..128].copy_from_slice(&npk.to_byte_array());
Self::new( Self::new(
Impl::hash_bytes(&bytes) Impl::hash_bytes(&bytes)
.as_bytes() .as_bytes()
@ -63,6 +96,9 @@ pub struct ChainedCall {
pub pre_states: Vec<AccountWithMetadata>, pub pre_states: Vec<AccountWithMetadata>,
/// The instruction data to pass. /// The instruction data to pass.
pub instruction_data: InstructionData, pub instruction_data: InstructionData,
/// PDA seeds authorized for the callee. For each seed, the callee is authorized to
/// mutate the `AccountId` derived from `(caller_program_id, seed)`, regardless of
/// whether the account is public or private.
pub pda_seeds: Vec<PdaSeed>, pub pda_seeds: Vec<PdaSeed>,
} }
@ -112,7 +148,9 @@ pub enum Claim {
/// This will give no error if program had authorization in pre state and may be useful /// 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. /// if program decides to give up authorization for a chained call.
Authorized, Authorized,
/// The program requests ownership of the account through a PDA. /// 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), Pda(PdaSeed),
} }
@ -281,6 +319,11 @@ pub struct InvalidWindow;
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
#[must_use = "ProgramOutput does nothing unless written"] #[must_use = "ProgramOutput does nothing unless written"]
pub struct ProgramOutput { pub struct ProgramOutput {
/// The program ID of the program that produced this output.
pub self_program_id: ProgramId,
/// The program ID of the caller that invoked this program via a chained call,
/// or `None` if this is a top-level call.
pub caller_program_id: Option<ProgramId>,
/// The instruction data the program received to produce this output. /// The instruction data the program received to produce this output.
pub instruction_data: InstructionData, pub instruction_data: InstructionData,
/// The account pre states the program received to produce this output. /// The account pre states the program received to produce this output.
@ -297,11 +340,15 @@ pub struct ProgramOutput {
impl ProgramOutput { impl ProgramOutput {
pub const fn new( pub const fn new(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
instruction_data: InstructionData, instruction_data: InstructionData,
pre_states: Vec<AccountWithMetadata>, pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>, post_states: Vec<AccountPostState>,
) -> Self { ) -> Self {
Self { Self {
self_program_id,
caller_program_id,
instruction_data, instruction_data,
pre_states, pre_states,
post_states, post_states,
@ -371,8 +418,8 @@ impl ProgramOutput {
} }
/// Representation of a number as `lo + hi * 2^128`. /// Representation of a number as `lo + hi * 2^128`.
#[derive(PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
struct WrappedBalanceSum { pub struct WrappedBalanceSum {
lo: u128, lo: u128,
hi: u128, hi: u128,
} }
@ -382,7 +429,7 @@ impl WrappedBalanceSum {
/// ///
/// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not /// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not
/// expected in practical scenarios. /// expected in practical scenarios.
fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> { pub fn from_balances(balances: impl Iterator<Item = u128>) -> Option<Self> {
let mut wrapped = Self { lo: 0, hi: 0 }; let mut wrapped = Self { lo: 0, hi: 0 };
for balance in balances { for balance in balances {
@ -397,29 +444,107 @@ impl WrappedBalanceSum {
} }
} }
impl std::fmt::Display for WrappedBalanceSum {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.hi == 0 {
write!(f, "{}", self.lo)
} else {
write!(f, "{} * 2^128 + {}", self.hi, self.lo)
}
}
}
impl From<u128> for WrappedBalanceSum {
fn from(value: u128) -> Self {
Self { lo: value, hi: 0 }
}
}
#[derive(thiserror::Error, Debug)]
pub enum ExecutionValidationError {
#[error("Pre-state account IDs are not unique")]
PreStateAccountIdsNotUnique,
#[error(
"Pre-state and post-state lengths do not match: pre-state length {pre_state_length}, post-state length {post_state_length}"
)]
MismatchedPreStatePostStateLength {
pre_state_length: usize,
post_state_length: usize,
},
#[error("Unallowed modification of nonce for account {account_id}")]
ModifiedNonce { account_id: AccountId },
#[error("Unallowed modification of program owner for account {account_id}")]
ModifiedProgramOwner { account_id: AccountId },
#[error(
"Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner"
)]
UnauthorizedBalanceDecrease {
account_id: AccountId,
owner_program_id: ProgramId,
executing_program_id: ProgramId,
},
#[error(
"Unauthorized modification of data for account {account_id} which is not default and not owned by executing program {executing_program_id:?}"
)]
UnauthorizedDataModification {
account_id: AccountId,
executing_program_id: ProgramId,
},
#[error(
"Post-state for account {account_id} has default program owner but pre-state was not default"
)]
NonDefaultAccountWithDefaultOwner { account_id: AccountId },
#[error("Total balance across accounts overflowed 2^256 - 1")]
BalanceSumOverflow,
#[error(
"Total balance across accounts is not preserved: total balance in pre-states {total_balance_pre_states}, total balance in post-states {total_balance_post_states}"
)]
MismatchedTotalBalance {
total_balance_pre_states: WrappedBalanceSum,
total_balance_post_states: WrappedBalanceSum,
},
}
/// Computes the set of public-PDA `AccountId`s the callee is authorized to mutate.
///
/// Returns only public-form derivations, suitable for contexts where all accounts are public
/// (e.g. the public-execution path). The privacy circuit must additionally check each mask-3
/// `pre_state` against [`AccountId::for_private_pda`] with the supplied npk for that
/// `pre_state`.
#[must_use] #[must_use]
pub fn compute_authorized_pdas( pub fn compute_public_authorized_pdas(
caller_program_id: Option<ProgramId>, caller_program_id: Option<ProgramId>,
pda_seeds: &[PdaSeed], pda_seeds: &[PdaSeed],
) -> HashSet<AccountId> { ) -> HashSet<AccountId> {
caller_program_id let Some(caller) = caller_program_id else {
.map(|caller_program_id| { return HashSet::new();
pda_seeds };
.iter() pda_seeds
.map(|pda_seed| AccountId::from((&caller_program_id, pda_seed))) .iter()
.collect() .map(|seed| AccountId::for_public_pda(&caller, seed))
}) .collect()
.unwrap_or_default()
} }
/// Reads the NSSA inputs from the guest environment. /// Reads the NSSA inputs from the guest environment.
#[must_use] #[must_use]
pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) { pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionData) {
let self_program_id: ProgramId = env::read();
let caller_program_id: Option<ProgramId> = env::read();
let pre_states: Vec<AccountWithMetadata> = env::read(); let pre_states: Vec<AccountWithMetadata> = env::read();
let instruction_words: InstructionData = env::read(); let instruction_words: InstructionData = env::read();
let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap();
( (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction, instruction,
}, },
@ -433,31 +558,39 @@ pub fn read_nssa_inputs<T: DeserializeOwned>() -> (ProgramInput<T>, InstructionD
/// - `pre_states`: The list of input accounts, each annotated with authorization metadata. /// - `pre_states`: The list of input accounts, each annotated with authorization metadata.
/// - `post_states`: The list of resulting accounts after executing the program logic. /// - `post_states`: The list of resulting accounts after executing the program logic.
/// - `executing_program_id`: The identifier of the program that was executed. /// - `executing_program_id`: The identifier of the program that was executed.
#[must_use]
pub fn validate_execution( pub fn validate_execution(
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
post_states: &[AccountPostState], post_states: &[AccountPostState],
executing_program_id: ProgramId, executing_program_id: ProgramId,
) -> bool { ) -> Result<(), ExecutionValidationError> {
// 1. Check account ids are all different // 1. Check account ids are all different
if !validate_uniqueness_of_account_ids(pre_states) { if !validate_uniqueness_of_account_ids(pre_states) {
return false; return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
} }
// 2. Lengths must match // 2. Lengths must match
if pre_states.len() != post_states.len() { if pre_states.len() != post_states.len() {
return false; return Err(
ExecutionValidationError::MismatchedPreStatePostStateLength {
pre_state_length: pre_states.len(),
post_state_length: post_states.len(),
},
);
} }
for (pre, post) in pre_states.iter().zip(post_states) { for (pre, post) in pre_states.iter().zip(post_states) {
// 3. Nonce must remain unchanged // 3. Nonce must remain unchanged
if pre.account.nonce != post.account.nonce { if pre.account.nonce != post.account.nonce {
return false; return Err(ExecutionValidationError::ModifiedNonce {
account_id: pre.account_id,
});
} }
// 4. Program ownership changes are not allowed // 4. Program ownership changes are not allowed
if pre.account.program_owner != post.account.program_owner { if pre.account.program_owner != post.account.program_owner {
return false; return Err(ExecutionValidationError::ModifiedProgramOwner {
account_id: pre.account_id,
});
} }
let account_program_owner = pre.account.program_owner; let account_program_owner = pre.account.program_owner;
@ -466,7 +599,11 @@ pub fn validate_execution(
if post.account.balance < pre.account.balance if post.account.balance < pre.account.balance
&& account_program_owner != executing_program_id && account_program_owner != executing_program_id
{ {
return false; return Err(ExecutionValidationError::UnauthorizedBalanceDecrease {
account_id: pre.account_id,
owner_program_id: account_program_owner,
executing_program_id,
});
} }
// 6. Data changes only allowed if owned by executing program or if account pre state has // 6. Data changes only allowed if owned by executing program or if account pre state has
@ -475,13 +612,20 @@ pub fn validate_execution(
&& pre.account != Account::default() && pre.account != Account::default()
&& account_program_owner != executing_program_id && account_program_owner != executing_program_id
{ {
return false; return Err(ExecutionValidationError::UnauthorizedDataModification {
account_id: pre.account_id,
executing_program_id,
});
} }
// 7. If a post state has default program owner, the pre state must have been a default // 7. If a post state has default program owner, the pre state must have been a default
// account // account
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
return false; return Err(
ExecutionValidationError::NonDefaultAccountWithDefaultOwner {
account_id: pre.account_id,
},
);
} }
} }
@ -490,20 +634,23 @@ pub fn validate_execution(
let Some(total_balance_pre_states) = let Some(total_balance_pre_states) =
WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance))
else { else {
return false; return Err(ExecutionValidationError::BalanceSumOverflow);
}; };
let Some(total_balance_post_states) = let Some(total_balance_post_states) =
WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance))
else { else {
return false; return Err(ExecutionValidationError::BalanceSumOverflow);
}; };
if total_balance_pre_states != total_balance_post_states { if total_balance_pre_states != total_balance_post_states {
return false; return Err(ExecutionValidationError::MismatchedTotalBalance {
total_balance_pre_states,
total_balance_post_states,
});
} }
true Ok(())
} }
fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool {
@ -620,7 +767,7 @@ mod tests {
#[test] #[test]
fn program_output_try_with_block_validity_window_range() { fn program_output_try_with_block_validity_window_range() {
let output = ProgramOutput::new(vec![], vec![], vec![]) let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
.try_with_block_validity_window(10_u64..100) .try_with_block_validity_window(10_u64..100)
.unwrap(); .unwrap();
assert_eq!(output.block_validity_window.start(), Some(10)); assert_eq!(output.block_validity_window.start(), Some(10));
@ -629,24 +776,24 @@ mod tests {
#[test] #[test]
fn program_output_with_block_validity_window_range_from() { fn program_output_with_block_validity_window_range_from() {
let output = let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..); .with_block_validity_window(10_u64..);
assert_eq!(output.block_validity_window.start(), Some(10)); assert_eq!(output.block_validity_window.start(), Some(10));
assert_eq!(output.block_validity_window.end(), None); assert_eq!(output.block_validity_window.end(), None);
} }
#[test] #[test]
fn program_output_with_block_validity_window_range_to() { fn program_output_with_block_validity_window_range_to() {
let output = let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(..100_u64); .with_block_validity_window(..100_u64);
assert_eq!(output.block_validity_window.start(), None); assert_eq!(output.block_validity_window.start(), None);
assert_eq!(output.block_validity_window.end(), Some(100)); assert_eq!(output.block_validity_window.end(), Some(100));
} }
#[test] #[test]
fn program_output_try_with_block_validity_window_empty_range_fails() { fn program_output_try_with_block_validity_window_empty_range_fails() {
let result = let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
ProgramOutput::new(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5); .try_with_block_validity_window(5_u64..5);
assert!(result.is_err()); assert!(result.is_err());
} }
@ -694,4 +841,108 @@ mod tests {
assert_eq!(account_post_state.account(), &account); assert_eq!(account_post_state.account(), &account);
assert_eq!(account_post_state.account_mut(), &mut account); assert_eq!(account_post_state.account_mut(), &mut account);
} }
// ---- AccountId::for_private_pda tests ----
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
/// `(program_id, seed, npk)` triple. Any change to `PRIVATE_PDA_PREFIX`, byte ordering,
/// or the underlying hash breaks this test.
#[test]
fn for_private_pda_matches_pinned_value() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let expected = AccountId::new([
132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53,
133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45,
]);
assert_eq!(
AccountId::for_private_pda(&program_id, &seed, &npk),
expected
);
}
/// Two groups with different viewing keys at the same (program, seed) get different addresses.
#[test]
fn for_private_pda_differs_for_different_npk() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk_a = NullifierPublicKey([3; 32]);
let npk_b = NullifierPublicKey([4; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed, &npk_a),
AccountId::for_private_pda(&program_id, &seed, &npk_b),
);
}
/// Different seeds produce different addresses, even with the same program and npk.
#[test]
fn for_private_pda_differs_for_different_seed() {
let program_id: ProgramId = [1; 8];
let seed_a = PdaSeed::new([2; 32]);
let seed_b = PdaSeed::new([5; 32]);
let npk = NullifierPublicKey([3; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id, &seed_a, &npk),
AccountId::for_private_pda(&program_id, &seed_b, &npk),
);
}
/// Different programs produce different addresses, even with the same seed and npk.
#[test]
fn for_private_pda_differs_for_different_program_id() {
let program_id_a: ProgramId = [1; 8];
let program_id_b: ProgramId = [9; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
assert_ne!(
AccountId::for_private_pda(&program_id_a, &seed, &npk),
AccountId::for_private_pda(&program_id_b, &seed, &npk),
);
}
/// A private PDA at the same (program, seed) has a different address than a public PDA,
/// because the private formula uses a different prefix and includes npk.
#[test]
fn for_private_pda_differs_from_public_pda() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let public_id = AccountId::for_public_pda(&program_id, &seed);
assert_ne!(private_id, public_id);
}
/// A private PDA address differs from a standard private account address at the same `npk`,
/// because the private PDA formula includes `program_id` and `seed`.
#[test]
fn for_private_pda_differs_from_standard_private() {
let program_id: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let npk = NullifierPublicKey([3; 32]);
let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk);
let standard_private_id = AccountId::from(&npk);
assert_ne!(private_pda_id, standard_private_id);
}
// ---- compute_public_authorized_pdas tests ----
/// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds.
#[test]
fn compute_public_authorized_pdas_with_seeds() {
let caller: ProgramId = [1; 8];
let seed = PdaSeed::new([2; 32]);
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
let expected = AccountId::for_public_pda(&caller, &seed);
assert!(result.contains(&expected));
assert_eq!(result.len(), 1);
}
/// With no caller (top-level call), the result is always empty.
#[test]
fn compute_public_authorized_pdas_no_caller_returns_empty() {
let seed = PdaSeed::new([2; 32]);
let result = compute_public_authorized_pdas(None, &[seed]);
assert!(result.is_empty());
}
} }

View File

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

View File

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

View File

@ -10,7 +10,7 @@ use nssa_core::{
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
use crate::{ use crate::{
error::NssaError, error::{InvalidProgramBehaviorError, NssaError},
program::Program, program::Program,
program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}, program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID},
state::MAX_NUMBER_CHAINED_CALLS, state::MAX_NUMBER_CHAINED_CALLS,
@ -87,15 +87,16 @@ pub fn execute_and_prove(
pda_seeds: vec![], pda_seeds: vec![],
}; };
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program)]); let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
let mut chain_calls_counter = 0; let mut chain_calls_counter = 0;
while let Some((chained_call, program)) = chained_calls.pop_front() { while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
return Err(NssaError::MaxChainedCallsDepthExceeded); return Err(NssaError::MaxChainedCallsDepthExceeded);
} }
let inner_receipt = execute_and_prove_program( let inner_receipt = execute_and_prove_program(
program, program,
caller_program_id,
&chained_call.pre_states, &chained_call.pre_states,
&chained_call.instruction_data, &chained_call.instruction_data,
)?; )?;
@ -112,10 +113,12 @@ pub fn execute_and_prove(
env_builder.add_assumption(inner_receipt); env_builder.add_assumption(inner_receipt);
for new_call in program_output.chained_calls.into_iter().rev() { for new_call in program_output.chained_calls.into_iter().rev() {
let next_program = dependencies let next_program = dependencies.get(&new_call.program_id).ok_or(
.get(&new_call.program_id) InvalidProgramBehaviorError::UndeclaredProgramDependency {
.ok_or(NssaError::InvalidProgramBehavior)?; program_id: new_call.program_id,
chained_calls.push_front((new_call, next_program)); },
)?;
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
} }
chain_calls_counter = chain_calls_counter chain_calls_counter = chain_calls_counter
@ -153,12 +156,19 @@ pub fn execute_and_prove(
fn execute_and_prove_program( fn execute_and_prove_program(
program: &Program, program: &Program,
caller_program_id: Option<ProgramId>,
pre_states: &[AccountWithMetadata], pre_states: &[AccountWithMetadata],
instruction_data: &InstructionData, instruction_data: &InstructionData,
) -> Result<Receipt, NssaError> { ) -> Result<Receipt, NssaError> {
// Write inputs to the program // Write inputs to the program
let mut env_builder = ExecutorEnv::builder(); let mut env_builder = ExecutorEnv::builder();
Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; Program::write_inputs(
program.id(),
caller_program_id,
pre_states,
instruction_data,
&mut env_builder,
)?;
let env = env_builder.build().unwrap(); let env = env_builder.build().unwrap();
// Prove the program // Prove the program

View File

@ -1,19 +1,10 @@
use std::{ use std::collections::HashSet;
collections::{HashMap, HashSet},
hash::Hash,
};
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use nssa_core::{ use nssa_core::account::AccountId;
BlockId, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountWithMetadata},
};
use sha2::{Digest as _, digest::FixedOutput as _}; use sha2::{Digest as _, digest::FixedOutput as _};
use super::{message::Message, witness_set::WitnessSet}; use super::{message::Message, witness_set::WitnessSet};
use crate::{
AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PrivacyPreservingTransaction { pub struct PrivacyPreservingTransaction {
@ -30,108 +21,6 @@ impl PrivacyPreservingTransaction {
} }
} }
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> 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()));
}
}
// Verify validity window
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
// 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,
)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
// 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
Ok(message
.public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect())
}
#[must_use] #[must_use]
pub const fn message(&self) -> &Message { pub const fn message(&self) -> &Message {
&self.message &self.message
@ -170,36 +59,6 @@ impl PrivacyPreservingTransaction {
} }
} }
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
proof
.is_valid_for(&output)
.then_some(())
.ok_or(NssaError::InvalidPrivacyPreservingProof)
}
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
let set: HashSet<&T> = data.iter().collect();
set.len()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

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

View File

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

View File

@ -1,20 +1,10 @@
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::HashSet;
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use log::debug; use nssa_core::account::AccountId;
use nssa_core::{
BlockId, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution},
};
use sha2::{Digest as _, digest::FixedOutput as _}; use sha2::{Digest as _, digest::FixedOutput as _};
use crate::{ use crate::public_transaction::{Message, WitnessSet};
V03State, ensure,
error::NssaError,
public_transaction::{Message, WitnessSet},
state::MAX_NUMBER_CHAINED_CALLS,
};
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct PublicTransaction { pub struct PublicTransaction {
@ -67,211 +57,6 @@ impl PublicTransaction {
hasher.update(&bytes); hasher.update(&bytes);
hasher.finalize_fixed().into() hasher.finalize_fixed().into()
} }
pub(crate) fn validate_and_produce_public_state_diff(
&self,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> 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,
);
let is_authorized = |account_id: &AccountId| {
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coincide with the values in the public
// 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
ensure!(
pre.is_authorized == is_authorized(&account_id),
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
);
// Verify validity window
ensure!(
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter_mut().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
let account_id = program_output.pre_states[i].account_id;
match claim {
Claim::Authorized => {
// The program can only claim accounts that were authorized by the signer.
ensure!(
is_authorized(&account_id),
NssaError::InvalidProgramBehavior
);
}
Claim::Pda(seed) => {
// The program can only claim accounts that correspond to the PDAs it is
// authorized to claim.
let pda = AccountId::from((&chained_call.program_id, &seed));
ensure!(account_id == pda, NssaError::InvalidProgramBehavior);
}
}
post.account_mut().program_owner = chained_call.program_id;
}
// Update the state diff
for (pre, post) in program_output
.pre_states
.iter()
.zip(program_output.post_states.iter())
{
state_diff.insert(pre.account_id, post.account().clone());
}
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front((new_call, Some(chained_call.program_id)));
}
chain_calls_counter = chain_calls_counter
.checked_add(1)
.expect("we check the max depth at the beginning of the loop");
}
// Check that all modified uninitialized accounts where claimed
for post in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(*account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
return None;
}
if pre == *post {
return None;
}
Some(post)
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
NssaError::InvalidProgramBehavior
);
}
Ok(state_diff)
}
} }
#[cfg(test)] #[cfg(test)]
@ -283,6 +68,7 @@ pub mod tests {
error::NssaError, error::NssaError,
program::Program, program::Program,
public_transaction::{Message, WitnessSet}, public_transaction::{Message, WitnessSet},
validated_state_diff::ValidatedStateDiff,
}; };
fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) { fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) {
@ -296,7 +82,7 @@ pub mod tests {
fn state_for_tests() -> V03State { fn state_for_tests() -> V03State {
let (_, _, addr1, addr2) = keys_for_tests(); let (_, _, addr1, addr2) = keys_for_tests();
let initial_data = [(addr1, 10000), (addr2, 20000)]; let initial_data = [(addr1, 10000), (addr2, 20000)];
V03State::new_with_genesis_accounts(&initial_data, &[]) V03State::new_with_genesis_accounts(&initial_data, vec![], 0)
} }
fn transaction_for_tests() -> PublicTransaction { fn transaction_for_tests() -> PublicTransaction {
@ -391,7 +177,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -411,7 +197,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -432,7 +218,7 @@ pub mod tests {
let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -452,7 +238,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
@ -468,7 +254,7 @@ pub mod tests {
let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
assert!(matches!(result, Err(NssaError::InvalidInput(_)))); assert!(matches!(result, Err(NssaError::InvalidInput(_))));
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,462 @@
use std::{
collections::{HashMap, HashSet, VecDeque},
hash::Hash,
};
use log::debug;
use nssa_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{
ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution,
},
};
use crate::{
V03State, ensure,
error::{InvalidProgramBehaviorError, NssaError},
privacy_preserving_transaction::{
PrivacyPreservingTransaction, circuit::Proof, message::Message,
},
program::Program,
program_deployment_transaction::ProgramDeploymentTransaction,
public_transaction::PublicTransaction,
state::MAX_NUMBER_CHAINED_CALLS,
};
pub struct StateDiff {
pub signer_account_ids: Vec<AccountId>,
pub public_diff: HashMap<AccountId, Account>,
pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<Nullifier>,
pub program: Option<Program>,
}
/// The validated output of executing or verifying a transaction, ready to be applied to the state.
///
/// Can only be constructed by the transaction validation functions inside this crate, ensuring the
/// diff has been checked before any state mutation occurs.
pub struct ValidatedStateDiff(StateDiff);
impl ValidatedStateDiff {
pub fn from_public_transaction(
tx: &PublicTransaction,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, NssaError> {
let message = tx.message();
let witness_set = tx.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 = tx.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(
caller_program_id,
&chained_call.pre_states,
&chained_call.instruction_data,
)?;
debug!(
"Program {:?} output: {:?}",
chained_call.program_id, program_output
);
let authorized_pdas =
compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds);
let is_authorized = |account_id: &AccountId| {
signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id)
};
for pre in &program_output.pre_states {
let account_id = pre.account_id;
// Check that the program output pre_states coincide with the values in the public
// 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,
InvalidProgramBehaviorError::InconsistentAccountPreState {
account_id,
expected: Box::new(expected_pre),
actual: Box::new(pre.account.clone())
}
);
// Check that authorization flags are consistent with the provided ones or
// authorized by program through the PDA mechanism
let expected_is_authorized = is_authorized(&account_id);
ensure!(
pre.is_authorized == expected_is_authorized,
InvalidProgramBehaviorError::InconsistentAccountAuthorization {
account_id,
expected_authorization: expected_is_authorized,
actual_authorization: pre.is_authorized
}
);
}
// Verify that the program output's self_program_id matches the expected program ID.
ensure!(
program_output.self_program_id == chained_call.program_id,
InvalidProgramBehaviorError::MismatchedProgramId {
expected: chained_call.program_id,
actual: program_output.self_program_id
}
);
// Verify that the program output's caller_program_id matches the actual caller.
ensure!(
program_output.caller_program_id == caller_program_id,
InvalidProgramBehaviorError::MismatchedCallerProgramId {
expected: caller_program_id,
actual: program_output.caller_program_id,
}
);
// Verify execution corresponds to a well-behaved program.
// See the # Programs section for the definition of the `validate_execution` method.
validate_execution(
&program_output.pre_states,
&program_output.post_states,
chained_call.program_id,
)
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
// Verify validity window
ensure!(
program_output.block_validity_window.is_valid_for(block_id)
&& program_output
.timestamp_validity_window
.is_valid_for(timestamp),
NssaError::OutOfValidityWindow
);
for (i, post) in program_output.post_states.iter_mut().enumerate() {
let Some(claim) = post.required_claim() else {
continue;
};
let account_id = program_output.pre_states[i].account_id;
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
);
match claim {
Claim::Authorized => {
// The program can only claim accounts that were authorized by the signer.
ensure!(
is_authorized(&account_id),
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
);
}
Claim::Pda(seed) => {
// The program can only claim accounts that correspond to the PDAs it is
// authorized to claim. The public-execution path only sees public
// accounts, so the public-PDA derivation is the correct formula here.
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
ensure!(
account_id == pda,
InvalidProgramBehaviorError::MismatchedPdaClaim {
expected: pda,
actual: account_id
}
);
}
}
post.account_mut().program_owner = chained_call.program_id;
}
// 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 (account_id, 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((*account_id, post))
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
);
}
Ok(Self(StateDiff {
signer_account_ids,
public_diff: state_diff,
new_commitments: vec![],
new_nullifiers: vec![],
program: None,
}))
}
pub fn from_privacy_preserving_transaction(
tx: &PrivacyPreservingTransaction,
state: &V03State,
block_id: BlockId,
timestamp: Timestamp,
) -> Result<Self, NssaError> {
let message = &tx.message;
let witness_set = &tx.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 = tx.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()));
}
}
// Verify validity window
if !message.block_validity_window.is_valid_for(block_id)
|| !message.timestamp_validity_window.is_valid_for(timestamp)
{
return Err(NssaError::OutOfValidityWindow);
}
// 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,
)?;
// 5. Commitment freshness
state.check_commitments_are_new(&message.new_commitments)?;
// 6. Nullifier uniqueness
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
let public_diff = message
.public_account_ids
.iter()
.copied()
.zip(message.public_post_states.clone())
.collect();
let new_nullifiers = message
.new_nullifiers
.iter()
.copied()
.map(|(nullifier, _)| nullifier)
.collect();
Ok(Self(StateDiff {
signer_account_ids,
public_diff,
new_commitments: message.new_commitments.clone(),
new_nullifiers,
program: None,
}))
}
pub fn from_program_deployment_transaction(
tx: &ProgramDeploymentTransaction,
state: &V03State,
) -> Result<Self, NssaError> {
// TODO: remove clone
let program = Program::new(tx.message.bytecode.clone())?;
if state.programs().contains_key(&program.id()) {
return Err(NssaError::ProgramAlreadyExists);
}
Ok(Self(StateDiff {
signer_account_ids: vec![],
public_diff: HashMap::new(),
new_commitments: vec![],
new_nullifiers: vec![],
program: Some(program),
}))
}
/// Returns the public account changes produced by this transaction.
///
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
/// to enforce that system accounts are not modified by user transactions.
#[must_use]
pub fn public_diff(&self) -> HashMap<AccountId, Account> {
self.0.public_diff.clone()
}
pub(crate) fn into_state_diff(self) -> StateDiff {
self.0
}
}
fn check_privacy_preserving_circuit_proof_is_valid(
proof: &Proof,
public_pre_states: &[AccountWithMetadata],
message: &Message,
) -> Result<(), NssaError> {
let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(),
public_post_states: message.public_post_states.clone(),
ciphertexts: message
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window,
timestamp_validity_window: message.timestamp_validity_window,
};
proof
.is_valid_for(&output)
.then_some(())
.ok_or(NssaError::InvalidPrivacyPreservingProof)
}
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
let set: HashSet<&T> = data.iter().collect();
set.len()
}

View File

@ -9,6 +9,7 @@ workspace = true
[dependencies] [dependencies]
nssa_core.workspace = true nssa_core.workspace = true
clock_core.workspace = true
token_core.workspace = true token_core.workspace = true
token_program.workspace = true token_program.workspace = true
amm_core.workspace = true amm_core.workspace = true

View File

@ -14,6 +14,8 @@ use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs};
fn main() { fn main() {
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction, instruction,
}, },
@ -112,15 +114,15 @@ fn main() {
min_amount_to_remove_token_b, min_amount_to_remove_token_b,
) )
} }
Instruction::Swap { Instruction::SwapExactInput {
swap_amount_in, swap_amount_in,
min_amount_out, min_amount_out,
token_definition_id_in, token_definition_id_in,
} => { } => {
let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states
.try_into() .try_into()
.expect("Transfer instruction requires exactly five accounts"); .expect("SwapExactInput instruction requires exactly five accounts");
amm_program::swap::swap( amm_program::swap::swap_exact_input(
pool, pool,
vault_a, vault_a,
vault_b, vault_b,
@ -131,9 +133,34 @@ fn main() {
token_definition_id_in, token_definition_id_in,
) )
} }
Instruction::SwapExactOutput {
exact_amount_out,
max_amount_in,
token_definition_id_in,
} => {
let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states
.try_into()
.expect("SwapExactOutput instruction requires exactly five accounts");
amm_program::swap::swap_exact_output(
pool,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
exact_amount_out,
max_amount_in,
token_definition_id_in,
)
}
}; };
ProgramOutput::new(instruction_words, pre_states_clone, post_states) ProgramOutput::new(
.with_chained_calls(chained_calls) self_program_id,
.write(); caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.with_chained_calls(chained_calls)
.write();
} }

View File

@ -4,6 +4,8 @@ use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs};
fn main() { fn main() {
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction, instruction,
}, },
@ -56,7 +58,13 @@ fn main() {
} }
}; };
ProgramOutput::new(instruction_words, pre_states_clone, post_states) ProgramOutput::new(
.with_chained_calls(chained_calls) self_program_id,
.write(); caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.with_chained_calls(chained_calls)
.write();
} }

View File

@ -67,6 +67,8 @@ fn main() {
// Read input accounts. // Read input accounts.
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: balance_to_move, instruction: balance_to_move,
}, },
@ -84,5 +86,12 @@ fn main() {
_ => panic!("invalid params"), _ => panic!("invalid params"),
}; };
ProgramOutput::new(instruction_words, pre_states, post_states).write(); ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states,
post_states,
)
.write();
} }

View File

@ -0,0 +1,94 @@
//! Clock Program.
//!
//! A system program that records the current block ID and timestamp into dedicated clock accounts.
//! Three accounts are maintained, updated at different block intervals (every 1, 10, and 50
//! blocks), allowing programs to read recent timestamps at various granularities.
//!
//! This program can only be invoked exclusively by the sequencer as the last transaction in every
//! block. Clock accounts are assigned to the clock program at genesis, so no claiming is required
//! here.
use clock_core::{
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID,
ClockAccountData, Instruction,
};
use nssa_core::{
account::AccountWithMetadata,
program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs},
};
fn update_if_multiple(
pre: AccountWithMetadata,
divisor: u64,
current_block_id: u64,
updated_data: &[u8],
) -> (AccountWithMetadata, AccountPostState) {
if current_block_id.is_multiple_of(divisor) {
let mut post_account = pre.account.clone();
post_account.data = updated_data
.to_vec()
.try_into()
.expect("Clock account data should fit in account data");
(pre, AccountPostState::new(post_account))
} else {
let post = AccountPostState::new(pre.account.clone());
(pre, post)
}
}
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction: timestamp,
},
instruction_words,
) = read_nssa_inputs::<Instruction>();
let Ok([pre_01, pre_10, pre_50]) = <[_; 3]>::try_from(pre_states) else {
panic!("Invalid number of input accounts");
};
// Verify pre-states correspond to the expected clock account IDs.
if pre_01.account_id != CLOCK_01_PROGRAM_ACCOUNT_ID
|| pre_10.account_id != CLOCK_10_PROGRAM_ACCOUNT_ID
|| pre_50.account_id != CLOCK_50_PROGRAM_ACCOUNT_ID
{
panic!("Invalid input accounts");
}
// Verify all clock accounts are owned by this program (assigned at genesis).
if pre_01.account.program_owner != self_program_id
|| pre_10.account.program_owner != self_program_id
|| pre_50.account.program_owner != self_program_id
{
panic!("Clock accounts must be owned by the clock program");
}
let prev_data = ClockAccountData::from_bytes(&pre_01.account.data.clone().into_inner());
let current_block_id = prev_data
.block_id
.checked_add(1)
.expect("Next block id should be within u64 boundaries");
let updated_data = ClockAccountData {
block_id: current_block_id,
timestamp,
}
.to_bytes();
let (pre_01, post_01) = update_if_multiple(pre_01, 1, current_block_id, &updated_data);
let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, &updated_data);
let (pre_50, post_50) = update_if_multiple(pre_50, 50, current_block_id, &updated_data);
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![pre_01, pre_10, pre_50],
vec![post_01, post_10, post_50],
)
.write();
}

View File

@ -46,6 +46,8 @@ fn main() {
// It is expected to receive only two accounts: [pinata_account, winner_account] // It is expected to receive only two accounts: [pinata_account, winner_account]
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: solution, instruction: solution,
}, },
@ -79,6 +81,8 @@ fn main() {
.expect("Overflow when adding prize to winner"); .expect("Overflow when adding prize to winner");
ProgramOutput::new( ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words, instruction_words,
vec![pinata, winner], vec![pinata, winner],
vec![ vec![

View File

@ -52,6 +52,8 @@ fn main() {
// winner_token_holding] // winner_token_holding]
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction: solution, instruction: solution,
}, },
@ -97,6 +99,8 @@ fn main() {
.with_pda_seeds(vec![PdaSeed::new([0; 32])]); .with_pda_seeds(vec![PdaSeed::new([0; 32])]);
ProgramOutput::new( ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words, instruction_words,
vec![ vec![
pinata_definition, pinata_definition,

View File

@ -11,7 +11,7 @@ use nssa_core::{
compute_digest_for_path, compute_digest_for_path,
program::{ program::{
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID, AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow, MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
validate_execution, validate_execution,
}, },
}; };
@ -23,15 +23,62 @@ struct ExecutionState {
post_states: HashMap<AccountId, Account>, post_states: HashMap<AccountId, Account>,
block_validity_window: BlockValidityWindow, block_validity_window: BlockValidityWindow,
timestamp_validity_window: TimestampValidityWindow, timestamp_validity_window: TimestampValidityWindow,
/// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)`
/// check.
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
/// under the private derivation. Binding is an idempotent property, not an event: the same
/// position can legitimately be bound through both paths in the same tx (e.g. a program
/// claims a private PDA and then delegates it to a callee), and the set uses `contains`,
/// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this
/// set; otherwise the npk is unbound and the circuit rejects.
private_pda_bound_positions: HashSet<usize>,
/// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one
/// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and
/// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in
/// a chained call could authorize multiple family members at once (different npks under the
/// same seed) and let a callee mix balances across them. Every claim and every
/// caller-authorization resolution is recorded here, either as a new `(program, seed)` →
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
/// `(program, seed)` → one account per tx.
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
/// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for
/// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking
/// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and
/// caller-seeds authorization paths.
private_pda_npk_by_position: HashMap<usize, NullifierPublicKey>,
} }
impl ExecutionState { impl ExecutionState {
/// Validate program outputs and derive the overall execution state. /// Validate program outputs and derive the overall execution state.
pub fn derive_from_outputs( pub fn derive_from_outputs(
visibility_mask: &[u8], visibility_mask: &[u8],
private_account_keys: &[(NullifierPublicKey, SharedSecretKey)],
program_id: ProgramId, program_id: ProgramId,
program_outputs: Vec<ProgramOutput>, program_outputs: Vec<ProgramOutput>,
) -> Self { ) -> Self {
// Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in
// pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The
// downstream `compute_circuit_output` also consumes the same iterator and its trailing
// assertions catch an over-supply of keys; under-supply surfaces here.
let mut private_pda_npk_by_position: HashMap<usize, NullifierPublicKey> = HashMap::new();
{
let mut keys_iter = private_account_keys.iter();
for (pos, &mask) in visibility_mask.iter().enumerate() {
if matches!(mask, 1..=3) {
let (npk, _) = keys_iter.next().unwrap_or_else(|| {
panic!(
"private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})"
)
});
if mask == 3 {
private_pda_npk_by_position.insert(pos, *npk);
}
}
}
}
let block_valid_from = program_outputs let block_valid_from = program_outputs
.iter() .iter()
.filter_map(|output| output.block_validity_window.start()) .filter_map(|output| output.block_validity_window.start())
@ -66,6 +113,9 @@ impl ExecutionState {
post_states: HashMap::new(), post_states: HashMap::new(),
block_validity_window, block_validity_window,
timestamp_validity_window, timestamp_validity_window,
private_pda_bound_positions: HashSet::new(),
pda_family_binding: HashMap::new(),
private_pda_npk_by_position,
}; };
let Some(first_output) = program_outputs.first() else { let Some(first_output) = program_outputs.first() else {
@ -107,27 +157,45 @@ impl ExecutionState {
|_: Infallible| unreachable!("Infallible error is never constructed"), |_: Infallible| unreachable!("Infallible error is never constructed"),
); );
// Verify that the program output's self_program_id matches the expected program ID.
// This ensures the proof commits to which program produced the output.
assert_eq!(
program_output.self_program_id, chained_call.program_id,
"Program output self_program_id does not match chained call program_id"
);
// Verify that the program output's caller_program_id matches the actual caller.
// This prevents a malicious user from privately executing an internal function
// by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id
// to bypass access control checks).
assert_eq!(
program_output.caller_program_id, caller_program_id,
"Program output caller_program_id does not match actual caller"
);
// Check that the program is well behaved. // Check that the program is well behaved.
// See the # Programs section for the definition of the `validate_execution` method. // See the # Programs section for the definition of the `validate_execution` method.
let execution_valid = validate_execution( let validated_execution = validate_execution(
&program_output.pre_states, &program_output.pre_states,
&program_output.post_states, &program_output.post_states,
chained_call.program_id, chained_call.program_id,
); );
assert!(execution_valid, "Bad behaved program"); if let Err(err) = validated_execution {
panic!(
"Invalid program behavior in program {:?}: {err}",
chained_call.program_id
);
}
for next_call in program_output.chained_calls.iter().rev() { for next_call in program_output.chained_calls.iter().rev() {
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
} }
let authorized_pdas = nssa_core::program::compute_authorized_pdas(
caller_program_id,
&chained_call.pda_seeds,
);
execution_state.validate_and_sync_states( execution_state.validate_and_sync_states(
visibility_mask, visibility_mask,
chained_call.program_id, chained_call.program_id,
&authorized_pdas, caller_program_id,
&chained_call.pda_seeds,
program_output.pre_states, program_output.pre_states,
program_output.post_states, program_output.post_states,
); );
@ -141,6 +209,19 @@ impl ExecutionState {
"Inner call without a chained call found", "Inner call without a chained call found",
); );
// Every mask-3 pre_state must have had its npk bound to its account_id, either via a
// `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching
// the private derivation. An unbound mask-3 pre_state has no cryptographic link between
// the supplied npk and the account_id, and must be rejected.
for (pos, &mask) in visibility_mask.iter().enumerate() {
if mask == 3 {
assert!(
execution_state.private_pda_bound_positions.contains(&pos),
"private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds"
);
}
}
// Check that all modified uninitialized accounts were claimed // Check that all modified uninitialized accounts were claimed
for (account_id, post) in execution_state for (account_id, post) in execution_state
.pre_states .pre_states
@ -170,7 +251,8 @@ impl ExecutionState {
&mut self, &mut self,
visibility_mask: &[u8], visibility_mask: &[u8],
program_id: ProgramId, program_id: ProgramId,
authorized_pdas: &HashSet<AccountId>, caller_program_id: Option<ProgramId>,
caller_pda_seeds: &[PdaSeed],
pre_states: Vec<AccountWithMetadata>, pre_states: Vec<AccountWithMetadata>,
post_states: Vec<AccountPostState>, post_states: Vec<AccountPostState>,
) { ) {
@ -197,19 +279,28 @@ impl ExecutionState {
"Inconsistent pre state for account {pre_account_id}", "Inconsistent pre state for account {pre_account_id}",
); );
let previous_is_authorized = self let (previous_is_authorized, pre_state_position) = self
.pre_states .pre_states
.iter() .iter()
.find(|acc| acc.account_id == pre_account_id) .enumerate()
.find(|(_, acc)| acc.account_id == pre_account_id)
.map_or_else( .map_or_else(
|| panic!( || panic!(
"Pre state must exist in execution state for account {pre_account_id}", "Pre state must exist in execution state for account {pre_account_id}",
), ),
|acc| acc.is_authorized |(pos, acc)| (acc.is_authorized, pos)
); );
let is_authorized = let is_authorized = resolve_authorization_and_record_bindings(
previous_is_authorized || authorized_pdas.contains(&pre_account_id); &mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_npk_by_position,
pre_account_id,
pre_state_position,
caller_program_id,
caller_pda_seeds,
previous_is_authorized,
);
assert_eq!( assert_eq!(
pre_is_authorized, is_authorized, pre_is_authorized, is_authorized,
@ -236,9 +327,9 @@ impl ExecutionState {
.position(|acc| acc.account_id == pre_account_id) .position(|acc| acc.account_id == pre_account_id)
.expect("Pre state must exist at this point"); .expect("Pre state must exist at this point");
let is_public_account = visibility_mask[pre_state_position] == 0; let mask = visibility_mask[pre_state_position];
if is_public_account { match mask {
match claim { 0 => match claim {
Claim::Authorized => { Claim::Authorized => {
// Note: no need to check authorized pdas because we have already // Note: no need to check authorized pdas because we have already
// checked consistency of authorization above. // checked consistency of authorization above.
@ -248,18 +339,52 @@ impl ExecutionState {
); );
} }
Claim::Pda(seed) => { Claim::Pda(seed) => {
let pda = AccountId::from((&program_id, &seed)); let pda = AccountId::for_public_pda(&program_id, &seed);
assert_eq!( assert_eq!(
pre_account_id, pda, pre_account_id, pda,
"Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}"
); );
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
},
3 => {
match claim {
Claim::Authorized => {
assert!(
pre_is_authorized,
"Cannot claim unauthorized private PDA {pre_account_id}"
);
}
Claim::Pda(seed) => {
let npk = self
.private_pda_npk_by_position
.get(&pre_state_position)
.expect("private PDA pre_state must have an npk in the position map");
let pda = AccountId::for_private_pda(&program_id, &seed, npk);
assert_eq!(
pre_account_id, pda,
"Invalid private PDA claim for account {pre_account_id}"
);
self.private_pda_bound_positions.insert(pre_state_position);
assert_family_binding(
&mut self.pda_family_binding,
program_id,
seed,
pre_account_id,
);
}
} }
} }
} else { _ => {
// We don't care about the exact claim mechanism for private accounts. // Mask 1/2: standard private accounts don't enforce the claim semantics.
// This is because the main reason to have it is to protect against PDA griefing // Unauthorized private claiming is intentionally allowed since operating
// attacks in public execution, while private PDA doesn't make much sense // these accounts requires the npk/nsk keypair anyway.
// anyway. }
} }
post.account_mut().program_owner = program_id; post.account_mut().program_owner = program_id;
@ -283,6 +408,82 @@ impl ExecutionState {
} }
} }
/// Record or re-verify the `(program_id, seed) → account_id` family binding for the
/// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under
/// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a
/// single `pda_seeds: [seed]` entry could authorize multiple private-PDA family members at
/// once (different npks under the same seed) and let a callee mix balances across them. Free
/// function so callers can pass `&mut self.pda_family_binding` without holding a borrow on
/// the surrounding struct's other fields.
fn assert_family_binding(
bindings: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
program_id: ProgramId,
seed: PdaSeed,
account_id: AccountId,
) {
match bindings.entry((program_id, seed)) {
Entry::Vacant(e) => {
e.insert(account_id);
}
Entry::Occupied(e) => {
assert_eq!(
*e.get(),
account_id,
"Two different accounts resolved under the same (program, seed) in one transaction: existing {}, new {account_id}",
e.get()
);
}
}
}
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
/// previously-seen authorization or a matching caller seed (under the public or private
/// derivation). When a caller seed matches, also records the `(caller, seed) → account_id`
/// family binding and, for the private form, marks the position in
/// `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`,
/// top-level flows have no caller-emitted seeds, so binding at top level must come from the
/// claim path. Free function so callers can pass individual `&mut self.*` field borrows
/// without holding a borrow on the surrounding struct's other fields.
#[expect(
clippy::too_many_arguments,
reason = "breaking out a context struct does not buy us anything here"
)]
fn resolve_authorization_and_record_bindings(
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
private_pda_bound_positions: &mut HashSet<usize>,
private_pda_npk_by_position: &HashMap<usize, NullifierPublicKey>,
pre_account_id: AccountId,
pre_state_position: usize,
caller_program_id: Option<ProgramId>,
caller_pda_seeds: &[PdaSeed],
previous_is_authorized: bool,
) -> bool {
let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> =
caller_program_id.and_then(|caller| {
caller_pda_seeds.iter().find_map(|seed| {
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
return Some((*seed, false, caller));
}
if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position)
&& AccountId::for_private_pda(&caller, seed, npk) == pre_account_id
{
return Some((*seed, true, caller));
}
None
})
});
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
if is_private_form {
private_pda_bound_positions.insert(pre_state_position);
}
}
previous_is_authorized || matched_caller_seed.is_some()
}
fn compute_circuit_output( fn compute_circuit_output(
execution_state: ExecutionState, execution_state: ExecutionState,
visibility_mask: &[u8], visibility_mask: &[u8],
@ -418,6 +619,88 @@ fn compute_circuit_output(
.checked_add(1) .checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); .unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
} }
3 => {
// Private PDA account. The supplied npk has already been bound to
// `pre_state.account_id` upstream in `validate_and_sync_states`, either via a
// `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which
// assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The
// post-loop assertion in `derive_from_outputs` (see the
// `private_pda_bound_positions` check) guarantees that every mask-3
// position has been through at least one such binding, so this
// branch can safely use the wallet npk without re-verifying.
let Some((npk, shared_secret)) = private_keys_iter.next() else {
panic!("Missing private account key");
};
let (new_nullifier, new_nonce) = if pre_state.is_authorized {
// Existing private PDA with authentication (like mask 1)
let Some(nsk) = private_nsks_iter.next() else {
panic!("Missing private account nullifier secret key");
};
assert_eq!(
npk,
&NullifierPublicKey::from(nsk),
"Nullifier public key mismatch"
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
let new_nullifier = compute_nullifier_and_set_digest(
membership_proof_opt.as_ref(),
&pre_state.account,
npk,
nsk,
);
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
(new_nullifier, new_nonce)
} else {
// New private PDA (like mask 2). The default + unauthorized requirement
// here rules out use cases like a fully-private multisig, which would need
// a non-default, non-authorized private PDA input account.
// TODO(private-pdas-pr-2/3): relax this once the wallet can supply a
// `(seed, owner)` side input so the npk-to-account_id binding can be
// re-verified for an existing private PDA without a `Claim::Pda` or caller
// `pda_seeds` match.
assert_eq!(
pre_state.account,
Account::default(),
"New private PDA must be default"
);
let Some(membership_proof_opt) = private_membership_proofs_iter.next() else {
panic!("Missing membership proof");
};
assert!(
membership_proof_opt.is_none(),
"Membership proof must be None for new accounts"
);
let nullifier = Nullifier::for_account_initialization(npk);
let new_nonce = Nonce::private_account_nonce_init(npk);
((nullifier, DUMMY_COMMITMENT_HASH), new_nonce)
};
output.new_nullifiers.push(new_nullifier);
let mut post_with_updated_nonce = post_state;
post_with_updated_nonce.nonce = new_nonce;
let commitment_post = Commitment::new(npk, &post_with_updated_nonce);
let encrypted_account = EncryptionScheme::encrypt(
&post_with_updated_nonce,
shared_secret,
&commitment_post,
output_index,
);
output.new_commitments.push(commitment_post);
output.ciphertexts.push(encrypted_account);
output_index = output_index
.checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
}
_ => panic!("Invalid visibility mask value"), _ => panic!("Invalid visibility mask value"),
} }
} }
@ -480,8 +763,12 @@ fn main() {
program_id, program_id,
} = env::read(); } = env::read();
let execution_state = let execution_state = ExecutionState::derive_from_outputs(
ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs); &visibility_mask,
&private_account_keys,
program_id,
program_outputs,
);
let output = compute_circuit_output( let output = compute_circuit_output(
execution_state, execution_state,

View File

@ -12,6 +12,8 @@ use token_program::core::Instruction;
fn main() { fn main() {
let ( let (
ProgramInput { ProgramInput {
self_program_id,
caller_program_id,
pre_states, pre_states,
instruction, instruction,
}, },
@ -81,5 +83,12 @@ fn main() {
} }
}; };
ProgramOutput::new(instruction_words, pre_states_clone, post_states).write(); ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
pre_states_clone,
post_states,
)
.write();
} }

View File

@ -68,11 +68,27 @@ pub enum Instruction {
/// - User Holding Account for Token A /// - User Holding Account for Token A
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
/// authorized. /// authorized.
Swap { SwapExactInput {
swap_amount_in: u128, swap_amount_in: u128,
min_amount_out: u128, min_amount_out: u128,
token_definition_id_in: AccountId, token_definition_id_in: AccountId,
}, },
/// Swap tokens specifying the exact desired output amount,
/// while maintaining the Pool constant product.
///
/// Required accounts:
/// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A
/// - User Holding Account for Token B Either User Holding Account for Token A or Token B is
/// authorized.
SwapExactOutput {
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
},
} }
#[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
@ -119,10 +135,10 @@ pub fn compute_pool_pda(
definition_token_a_id: AccountId, definition_token_a_id: AccountId,
definition_token_b_id: AccountId, definition_token_b_id: AccountId,
) -> AccountId { ) -> AccountId {
AccountId::from(( AccountId::for_public_pda(
&amm_program_id, &amm_program_id,
&compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id),
)) )
} }
#[must_use] #[must_use]
@ -159,10 +175,10 @@ pub fn compute_vault_pda(
pool_id: AccountId, pool_id: AccountId,
definition_token_id: AccountId, definition_token_id: AccountId,
) -> AccountId { ) -> AccountId {
AccountId::from(( AccountId::for_public_pda(
&amm_program_id, &amm_program_id,
&compute_vault_pda_seed(pool_id, definition_token_id), &compute_vault_pda_seed(pool_id, definition_token_id),
)) )
} }
#[must_use] #[must_use]
@ -183,7 +199,7 @@ pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId
#[must_use] #[must_use]
pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId {
AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) AccountId::for_public_pda(&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))
} }
#[must_use] #[must_use]

View File

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

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