mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-16 10:49:32 +00:00
Merge branch 'dev' into Pravdyvy/pda-account-id-generation
This commit is contained in:
commit
e4fd9b2397
@ -60,6 +60,7 @@ allow-git = [
|
||||
"https://github.com/logos-blockchain/logos-blockchain.git",
|
||||
"https://github.com/logos-blockchain/logos-blockchain-circuits.git",
|
||||
"https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git",
|
||||
"https://github.com/logos-blockchain/sponges",
|
||||
"https://github.com/arkworks-rs/spongefish.git",
|
||||
]
|
||||
unknown-git = "deny"
|
||||
|
||||
4
.github/workflows/bench-regression.yml
vendored
4
.github/workflows/bench-regression.yml
vendored
@ -28,10 +28,6 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- uses: ./.github/actions/install-logos-blockchain-circuits
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
@ -135,7 +135,37 @@ jobs:
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests --all-features
|
||||
run: cargo nextest run --workspace --exclude integration_tests --exclude test_fixtures --all-features
|
||||
|
||||
test-fixtures-tests:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
- name: Run test_fixtures tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run -p test_fixtures
|
||||
|
||||
integration-tests-prebuild:
|
||||
runs-on: ubuntu-latest
|
||||
@ -193,7 +223,7 @@ jobs:
|
||||
echo "Discovered integration targets: $targets_json"
|
||||
|
||||
integration-tests:
|
||||
needs: integration-tests-prebuild
|
||||
needs: [test-fixtures-tests, integration-tests-prebuild]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
|
||||
197
Cargo.lock
generated
197
Cargo.lock
generated
@ -1154,6 +1154,25 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bridge_lock_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bridge_lock_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bridge_lock_core",
|
||||
"cross_zone_outbox_core",
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bridge_program"
|
||||
version = "0.1.0"
|
||||
@ -1833,6 +1852,76 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bridge_lock_core",
|
||||
"cross_zone_inbox_core",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"risc0-zkvm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.9",
|
||||
"common",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
"env_logger",
|
||||
"lee",
|
||||
"log",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"risc0-zkvm",
|
||||
"sequencer_service_rpc",
|
||||
"serde",
|
||||
"test_fixtures",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_inbox_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_inbox_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cross_zone_inbox_core",
|
||||
"lee_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_outbox_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"borsh",
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cross_zone_outbox_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cross_zone_outbox_core",
|
||||
"lee_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@ -1914,7 +2003,6 @@ dependencies = [
|
||||
"criterion",
|
||||
"key_protocol",
|
||||
"lee_core",
|
||||
"rand 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2077,7 +2165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3850,21 +3938,27 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-stream",
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"common",
|
||||
"cross_zone",
|
||||
"cross_zone_inbox_core",
|
||||
"futures",
|
||||
"hex",
|
||||
"humantime-serde",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"log",
|
||||
"logos-blockchain-core",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"storage",
|
||||
"tempfile",
|
||||
"testnet_initial_state",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
@ -3912,6 +4006,7 @@ dependencies = [
|
||||
"base64",
|
||||
"common",
|
||||
"hex",
|
||||
"indexer_core",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"schemars 1.2.1",
|
||||
@ -4012,8 +4107,11 @@ dependencies = [
|
||||
"authenticated_transfer_core",
|
||||
"borsh",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"bytesize",
|
||||
"common",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
"faucet_core",
|
||||
"futures",
|
||||
"hex",
|
||||
@ -4029,8 +4127,10 @@ dependencies = [
|
||||
"logos-blockchain-key-management-system-service",
|
||||
"logos-blockchain-zone-sdk",
|
||||
"num-bigint 0.4.6",
|
||||
"ping_core",
|
||||
"programs",
|
||||
"reqwest",
|
||||
"risc0-zkvm",
|
||||
"sequencer_core",
|
||||
"sequencer_service_rpc",
|
||||
"serde_json",
|
||||
@ -4043,6 +4143,7 @@ dependencies = [
|
||||
"vault_core",
|
||||
"wallet",
|
||||
"wallet-ffi",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4441,11 +4542,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
|
||||
source = "git+https://github.com/logos-blockchain/sponges?rev=3a56e99771beedf04946eab21a4a62adc2951377#3a56e99771beedf04946eab21a4a62adc2951377"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"risc0-zkvm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4553,7 +4654,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"borsh",
|
||||
"build_utils",
|
||||
"env_logger",
|
||||
"hex",
|
||||
"hex-literal 1.1.0",
|
||||
"k256",
|
||||
@ -4568,7 +4668,6 @@ dependencies = [
|
||||
"test-case",
|
||||
"test_methods",
|
||||
"thiserror 2.0.18",
|
||||
"token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7177,6 +7276,31 @@ dependencies = [
|
||||
"token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ping_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ping_receiver_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"ping_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ping_sender_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cross_zone_outbox_core",
|
||||
"lee_core",
|
||||
"ping_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
@ -7449,15 +7573,20 @@ dependencies = [
|
||||
"associated_token_account_program",
|
||||
"authenticated_transfer_core",
|
||||
"bridge_core",
|
||||
"bridge_lock_core",
|
||||
"build_utils",
|
||||
"clock_core",
|
||||
"cross_zone_inbox_core",
|
||||
"cross_zone_outbox_core",
|
||||
"faucet_core",
|
||||
"lee",
|
||||
"lee_core",
|
||||
"ping_core",
|
||||
"risc0-zkvm",
|
||||
"token_core",
|
||||
"token_program",
|
||||
"vault_core",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8896,10 +9025,13 @@ dependencies = [
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"common",
|
||||
"cross_zone",
|
||||
"cross_zone_inbox_core",
|
||||
"faucet_core",
|
||||
"futures",
|
||||
"hex",
|
||||
"humantime-serde",
|
||||
"itertools 0.14.0",
|
||||
"key_protocol",
|
||||
"lee",
|
||||
"lee_core",
|
||||
@ -9461,11 +9593,13 @@ dependencies = [
|
||||
"borsh",
|
||||
"common",
|
||||
"lee",
|
||||
"log",
|
||||
"programs",
|
||||
"rocksdb",
|
||||
"system_accounts",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9739,6 +9873,7 @@ name = "test_fixtures"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bip39",
|
||||
"bytesize",
|
||||
"common",
|
||||
"env_logger",
|
||||
@ -9758,6 +9893,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"testcontainers",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"wallet",
|
||||
]
|
||||
@ -9840,6 +9976,7 @@ dependencies = [
|
||||
"programs",
|
||||
"serde",
|
||||
"system_accounts",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -10890,7 +11027,6 @@ dependencies = [
|
||||
"risc0-zkvm",
|
||||
"sequencer_service_rpc",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"vault_core",
|
||||
"wallet",
|
||||
@ -11550,6 +11686,23 @@ dependencies = [
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapped_token_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"risc0-zkvm",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapped_token_program"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lee_core",
|
||||
"wrapped_token_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
@ -11802,3 +11955,31 @@ dependencies = [
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@ -43,6 +43,13 @@ members = [
|
||||
"lez/programs/pinata_token",
|
||||
"lez/programs/token",
|
||||
"lez/programs/vault",
|
||||
"lez/programs/cross_zone_inbox",
|
||||
"lez/programs/cross_zone_outbox",
|
||||
"lez/programs/bridge_lock",
|
||||
"lez/programs/wrapped_token",
|
||||
"lez/programs/ping_sender",
|
||||
"lez/programs/ping_receiver",
|
||||
"lez/cross_zone",
|
||||
|
||||
"test_programs",
|
||||
"test_programs/guest",
|
||||
@ -58,6 +65,7 @@ members = [
|
||||
"tools/cycle_bench",
|
||||
"tools/crypto_primitives_bench",
|
||||
"tools/integration_bench",
|
||||
"tools/cross_zone_chat",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@ -94,6 +102,12 @@ authenticated_transfer_core = { path = "lez/programs/authenticated_transfer/core
|
||||
faucet_core = { path = "lez/programs/faucet/core" }
|
||||
bridge_core = { path = "lez/programs/bridge/core" }
|
||||
vault_core = { path = "lez/programs/vault/core" }
|
||||
cross_zone_inbox_core = { path = "lez/programs/cross_zone_inbox/core" }
|
||||
cross_zone_outbox_core = { path = "lez/programs/cross_zone_outbox/core" }
|
||||
bridge_lock_core = { path = "lez/programs/bridge_lock/core" }
|
||||
wrapped_token_core = { path = "lez/programs/wrapped_token/core" }
|
||||
ping_core = { path = "lez/programs/ping_core" }
|
||||
cross_zone = { path = "lez/cross_zone" }
|
||||
build_utils = { path = "build_utils" }
|
||||
test_programs = { path = "test_programs" }
|
||||
testnet_initial_state = { path = "lez/testnet_initial_state" }
|
||||
@ -147,6 +161,7 @@ bip39 = "2.2.0"
|
||||
hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
zstd = "0.13"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
num-bigint = "0.4.6"
|
||||
@ -181,6 +196,7 @@ elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
|
||||
actix-web = { version = "4.13.0", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
axum = "0.8.4"
|
||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
|
||||
pyo3 = { version = "0.29", features = ["auto-initialize"] }
|
||||
@ -194,6 +210,10 @@ opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# Keccak speedup for in-guest ML KEM
|
||||
[patch.crates-io]
|
||||
keccak = { git = "https://github.com/logos-blockchain/sponges", rev = "3a56e99771beedf04946eab21a4a62adc2951377" }
|
||||
|
||||
# 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]
|
||||
|
||||
30
Justfile
30
Justfile
@ -6,6 +6,11 @@ default:
|
||||
# ---- Configuration ----
|
||||
ARTIFACTS := "artifacts"
|
||||
|
||||
# On macOS the integration-test binary links pyo3 against the CommandLineTools
|
||||
# Python framework with no embedded rpath, so it needs this to launch. Empty on
|
||||
# Linux/CI, which is unaffected.
|
||||
DEMO_ENV := if os() == "macos" { "DYLD_FALLBACK_FRAMEWORK_PATH=/Library/Developer/CommandLineTools/Library/Frameworks" } else { "" }
|
||||
|
||||
# Build risc0 program artifacts.
|
||||
build-artifacts:
|
||||
@echo "🔨 Building artifacts"
|
||||
@ -35,6 +40,11 @@ test:
|
||||
@echo "🧪 Running tests"
|
||||
RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast
|
||||
|
||||
# Regenerate the prebuilt sequencer db dump for fast TestContext::new() (needs Docker; commit the dump).
|
||||
regenerate-test-fixture:
|
||||
@echo "🧪 Regenerating test fixtures"
|
||||
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||
|
||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||
bench:
|
||||
@echo "📊 Running criterion benches"
|
||||
@ -94,6 +104,26 @@ wallet-import-test-accounts:
|
||||
|
||||
just run-wallet account list
|
||||
|
||||
# Demo: cross-zone ping. Boots two zones on one Bedrock and sends a message from
|
||||
# zone A to zone B, where the indexer re-derives and verifies it (Option B)
|
||||
# before ping_receiver records it. Dev mode, no proving.
|
||||
demo-cross-zone-ping:
|
||||
@echo "📡 Cross-zone ping demo (message A → B, indexer-verified)"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo test -p integration_tests --release --test cross_zone_verified -- --nocapture
|
||||
|
||||
# Demo: cross-zone wrapped-token bridge. Locks a balance on zone A and mints the
|
||||
# wrapped token to a recipient on zone B over the same verified spine.
|
||||
demo-cross-zone-bridge:
|
||||
@echo "🌉 Cross-zone bridge demo (lock on A, mint on B)"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo test -p integration_tests --release --test cross_zone_bridge -- --nocapture
|
||||
|
||||
# Demo: interactive cross-zone chat. Boots two zones on one Bedrock and serves a
|
||||
# local two-column web UI; type in one zone and watch the message cross into the
|
||||
# other. Two people can chat across the zones. Dev mode, no proving.
|
||||
cross-zone-chat:
|
||||
@echo "💬 Cross-zone chat demo — open the printed localhost URL"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo run -p cross_zone_chat --release
|
||||
|
||||
# Clean runtime data
|
||||
clean:
|
||||
@echo "🧹 Cleaning run artifacts"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/bridge_lock.bin
Normal file
BIN
artifacts/lez/programs/bridge_lock.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/cross_zone_inbox.bin
Normal file
BIN
artifacts/lez/programs/cross_zone_inbox.bin
Normal file
Binary file not shown.
BIN
artifacts/lez/programs/cross_zone_outbox.bin
Normal file
BIN
artifacts/lez/programs/cross_zone_outbox.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/ping_receiver.bin
Normal file
BIN
artifacts/lez/programs/ping_receiver.bin
Normal file
Binary file not shown.
BIN
artifacts/lez/programs/ping_sender.bin
Normal file
BIN
artifacts/lez/programs/ping_sender.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/wrapped_token.bin
Normal file
BIN
artifacts/lez/programs/wrapped_token.bin
Normal file
Binary file not shown.
@ -22,6 +22,12 @@ associated_token_account_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
faucet_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
cross_zone_outbox_core.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
indexer_service_rpc = { workspace = true, features = ["client"] }
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
wallet-ffi.workspace = true
|
||||
|
||||
@ -6,11 +6,168 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee::AccountId;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
pub use test_fixtures::*;
|
||||
use wallet::{
|
||||
cli::{
|
||||
CliAccountMention, Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
programs::{
|
||||
native_token_transfer::AuthTransferSubcommand, token::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
},
|
||||
storage::key_chain::FoundPrivateAccount,
|
||||
};
|
||||
|
||||
/// Maximum time to wait for the indexer to catch up to the sequencer.
|
||||
pub const L2_TO_L1_TIMEOUT: Duration = Duration::from_mins(7);
|
||||
pub const L2_TO_L1_TIMEOUT: Duration = Duration::from_mins(6);
|
||||
|
||||
/// Create a private or public account at the given chain index and return its ID.
|
||||
/// Pass `cci: None` to use the wallet's next available chain index.
|
||||
pub async fn new_account(
|
||||
ctx: &mut TestContext,
|
||||
private: bool,
|
||||
cci: Option<ChainIndex>,
|
||||
) -> Result<AccountId> {
|
||||
let subcommand = if private {
|
||||
NewSubcommand::Private { cci, label: None }
|
||||
} else {
|
||||
NewSubcommand::Public { cci, label: None }
|
||||
};
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(subcommand)),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
Ok(account_id)
|
||||
}
|
||||
|
||||
/// Send `amount` from `from` to `to` via an authenticated transfer (identifier 0).
|
||||
pub async fn send(
|
||||
ctx: &mut TestContext,
|
||||
from: CliAccountMention,
|
||||
to: CliAccountMention,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from,
|
||||
to: Some(to),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a token (New) and wait for the block to be included.
|
||||
pub async fn create_token(
|
||||
ctx: &mut TestContext,
|
||||
definition_account_id: CliAccountMention,
|
||||
supply_account_id: CliAccountMention,
|
||||
name: impl Into<String>,
|
||||
total_supply: u128,
|
||||
) -> Result<()> {
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id,
|
||||
supply_account_id,
|
||||
name: name.into(),
|
||||
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;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send tokens and wait for the block to be included.
|
||||
pub async fn token_send(
|
||||
ctx: &mut TestContext,
|
||||
from: CliAccountMention,
|
||||
to: CliAccountMention,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from,
|
||||
to: Some(to),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
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;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve the native token balance for `account_id`.
|
||||
pub async fn account_balance(ctx: &TestContext, account_id: AccountId) -> Result<u128> {
|
||||
Ok(ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(account_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Fetch the full account state for `account_id` from the sequencer.
|
||||
pub async fn get_account(ctx: &TestContext, account_id: AccountId) -> Result<lee::Account> {
|
||||
Ok(ctx.sequencer_client().get_account(account_id).await?)
|
||||
}
|
||||
|
||||
/// Fetch the current commitment for `account_id` and assert it is present in the sequencer state.
|
||||
pub async fn assert_private_commitment_in_state(
|
||||
ctx: &TestContext,
|
||||
account_id: AccountId,
|
||||
label: &str,
|
||||
) -> Result<()> {
|
||||
let commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(account_id)
|
||||
.with_context(|| format!("Failed to get commitment for {label}"))?;
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync the wallet's private accounts.
|
||||
pub async fn sync_private(ctx: &mut TestContext) -> Result<()> {
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look up a restored private account for `account_id`, panicking with `label` if absent.
|
||||
pub fn restored_private_account<'ctx>(
|
||||
ctx: &'ctx TestContext,
|
||||
account_id: AccountId,
|
||||
label: &str,
|
||||
) -> FoundPrivateAccount<'ctx> {
|
||||
ctx.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.private_account(account_id)
|
||||
.unwrap_or_else(|| panic!("{label} should be restored"))
|
||||
}
|
||||
|
||||
/// Assert that a restored public account's signing key exists, panicking with `label` if absent.
|
||||
pub fn assert_public_account_restored(ctx: &TestContext, account_id: AccountId, label: &str) {
|
||||
ctx.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(account_id)
|
||||
.unwrap_or_else(|| panic!("{label} should be restored"));
|
||||
}
|
||||
|
||||
/// Poll the indexer until its last finalized block id reaches the sequencer's
|
||||
/// current last block id or until [`L2_TO_L1_TIMEOUT`] elapses.
|
||||
|
||||
@ -4,12 +4,11 @@
|
||||
)]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{TestContext, private_mention};
|
||||
use integration_tests::{TestContext, get_account, new_account, private_mention};
|
||||
use key_protocol::key_management::KeyChain;
|
||||
use lee::Data;
|
||||
use lee_core::account::Nonce;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
|
||||
@ -24,10 +23,7 @@ use wallet::{
|
||||
async fn get_existing_account() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let account = ctx
|
||||
.sequencer_client()
|
||||
.get_account(ctx.existing_public_accounts()[0])
|
||||
.await?;
|
||||
let account = get_account(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
|
||||
assert_eq!(
|
||||
account.program_owner,
|
||||
@ -95,18 +91,7 @@ async fn add_label_to_existing_account() -> Result<()> {
|
||||
async fn new_public_account_without_label() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
|
||||
let result = execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Extract the account_id from the result
|
||||
|
||||
let wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
panic!("Expected RegisterAccount return value")
|
||||
};
|
||||
let account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Verify no label was stored for the account id
|
||||
assert!(
|
||||
@ -156,7 +141,11 @@ async fn import_private_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
|
||||
let account_id = lee::AccountId::from((
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
0,
|
||||
));
|
||||
let account = lee::Account {
|
||||
program_owner: programs::authenticated_transfer().id(),
|
||||
balance: 777,
|
||||
@ -213,7 +202,11 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
|
||||
let account_id = lee::AccountId::from((
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
0,
|
||||
));
|
||||
let key_chain_json =
|
||||
serde_json::to_string(&key_chain).context("Failed to serialize key chain")?;
|
||||
|
||||
|
||||
@ -7,16 +7,18 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, create_token, get_account, new_account,
|
||||
public_mention, token_send,
|
||||
};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
account::Label,
|
||||
cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand},
|
||||
programs::amm::AmmProgramAgnosticSubcommand,
|
||||
},
|
||||
};
|
||||
|
||||
@ -25,148 +27,60 @@ async fn amm_public() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create new account for the token definition
|
||||
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 definition_account_id_1 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for the token supply holder
|
||||
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");
|
||||
};
|
||||
let supply_account_id_1 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_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 recipient_account_id_1 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for the token definition
|
||||
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 definition_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for the token supply holder
|
||||
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");
|
||||
};
|
||||
let supply_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for receiving a token transaction
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_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 recipient_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new token
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id_1),
|
||||
supply_account_id: public_mention(supply_account_id_1),
|
||||
name: "A NAM1".to_owned(),
|
||||
|
||||
total_supply: 37,
|
||||
};
|
||||
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;
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_1),
|
||||
public_mention(supply_account_id_1),
|
||||
"A NAM1".to_owned(),
|
||||
37,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id_1),
|
||||
to: Some(public_mention(recipient_account_id_1)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
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;
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id_1),
|
||||
public_mention(recipient_account_id_1),
|
||||
7,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create new token
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id_2),
|
||||
supply_account_id: public_mention(supply_account_id_2),
|
||||
name: "A NAM2".to_owned(),
|
||||
|
||||
total_supply: 37,
|
||||
};
|
||||
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;
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_2),
|
||||
public_mention(supply_account_id_2),
|
||||
"A NAM2".to_owned(),
|
||||
37,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id_2),
|
||||
to: Some(public_mention(recipient_account_id_2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 7,
|
||||
};
|
||||
|
||||
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;
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id_2),
|
||||
public_mention(recipient_account_id_2),
|
||||
7,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("=================== SETUP FINISHED ===============");
|
||||
|
||||
@ -174,19 +88,7 @@ async fn amm_public() -> Result<()> {
|
||||
|
||||
// Setup accounts
|
||||
// Create new account for the user holding lp
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: user_holding_lp,
|
||||
} = 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 user_holding_lp = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Send creation tx
|
||||
let subcommand = AmmProgramAgnosticSubcommand::New {
|
||||
@ -201,17 +103,11 @@ async fn amm_public() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let user_holding_a_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_1)
|
||||
.await?;
|
||||
let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?;
|
||||
|
||||
let user_holding_b_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_2)
|
||||
.await?;
|
||||
let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?;
|
||||
|
||||
let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?;
|
||||
let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?;
|
||||
|
||||
assert_eq!(
|
||||
u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()),
|
||||
@ -244,17 +140,11 @@ async fn amm_public() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let user_holding_a_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_1)
|
||||
.await?;
|
||||
let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?;
|
||||
|
||||
let user_holding_b_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_2)
|
||||
.await?;
|
||||
let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?;
|
||||
|
||||
let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?;
|
||||
let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?;
|
||||
|
||||
assert_eq!(
|
||||
u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()),
|
||||
@ -287,17 +177,11 @@ async fn amm_public() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let user_holding_a_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_1)
|
||||
.await?;
|
||||
let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?;
|
||||
|
||||
let user_holding_b_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_2)
|
||||
.await?;
|
||||
let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?;
|
||||
|
||||
let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?;
|
||||
let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?;
|
||||
|
||||
assert_eq!(
|
||||
u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()),
|
||||
@ -331,17 +215,11 @@ async fn amm_public() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let user_holding_a_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_1)
|
||||
.await?;
|
||||
let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?;
|
||||
|
||||
let user_holding_b_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_2)
|
||||
.await?;
|
||||
let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?;
|
||||
|
||||
let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?;
|
||||
let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?;
|
||||
|
||||
assert_eq!(
|
||||
u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()),
|
||||
@ -375,17 +253,11 @@ async fn amm_public() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let user_holding_a_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_1)
|
||||
.await?;
|
||||
let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?;
|
||||
|
||||
let user_holding_b_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_2)
|
||||
.await?;
|
||||
let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?;
|
||||
|
||||
let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?;
|
||||
let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?;
|
||||
|
||||
assert_eq!(
|
||||
u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()),
|
||||
@ -412,33 +284,9 @@ 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 definition_account_id_1 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
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");
|
||||
};
|
||||
let supply_account_id_1 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create holding_a with a label
|
||||
let holding_a_label = Label::new("amm-holding-a-label");
|
||||
@ -457,33 +305,9 @@ async fn amm_new_pool_using_labels() -> Result<()> {
|
||||
};
|
||||
|
||||
// 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 definition_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
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");
|
||||
};
|
||||
let supply_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create holding_b with a label
|
||||
let holding_b_label = Label::new("amm-holding-b-label");
|
||||
@ -518,48 +342,40 @@ async fn amm_new_pool_using_labels() -> Result<()> {
|
||||
};
|
||||
|
||||
// Create token 1 and distribute to holding_a
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id_1),
|
||||
supply_account_id: public_mention(supply_account_id_1),
|
||||
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;
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_1),
|
||||
public_mention(supply_account_id_1),
|
||||
"TOKEN1".to_owned(),
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id_1),
|
||||
to: Some(public_mention(holding_a_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
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;
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id_1),
|
||||
public_mention(holding_a_id),
|
||||
5,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create token 2 and distribute to holding_b
|
||||
let subcommand = TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id_2),
|
||||
supply_account_id: public_mention(supply_account_id_2),
|
||||
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;
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_2),
|
||||
public_mention(supply_account_id_2),
|
||||
"TOKEN2".to_owned(),
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id_2),
|
||||
to: Some(public_mention(holding_b_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
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;
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id_2),
|
||||
public_mention(holding_b_id),
|
||||
5,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create AMM pool using account labels instead of IDs
|
||||
let subcommand = AmmProgramAgnosticSubcommand::New {
|
||||
@ -572,7 +388,7 @@ async fn amm_new_pool_using_labels() -> Result<()> {
|
||||
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?;
|
||||
let holding_lp_acc = get_account(&ctx, holding_lp_id).await?;
|
||||
|
||||
// LP balance should be 3 (geometric mean of 3, 3)
|
||||
assert_eq!(
|
||||
|
||||
@ -9,75 +9,34 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result};
|
||||
use associated_token_account_core::{compute_ata_seed, get_associated_token_account_id};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
|
||||
verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, create_token, get_account, new_account,
|
||||
private_mention, public_mention, token_send, verify_commitment_is_in_state,
|
||||
};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
programs::{ata::AtaSubcommand, token::TokenProgramAgnosticSubcommand},
|
||||
};
|
||||
|
||||
/// Create a public account and return its ID.
|
||||
async fn new_public_account(ctx: &mut TestContext) -> Result<lee::AccountId> {
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
Ok(account_id)
|
||||
}
|
||||
|
||||
/// Create a private account and return its ID.
|
||||
async fn new_private_account(ctx: &mut TestContext) -> Result<lee::AccountId> {
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
Ok(account_id)
|
||||
}
|
||||
use wallet::cli::{Command, programs::ata::AtaSubcommand};
|
||||
|
||||
#[test]
|
||||
async fn create_ata_initializes_holding_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let owner_account_id = new_public_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let owner_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create a fungible token
|
||||
let total_supply = 100_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
total_supply,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Create the ATA for owner + definition
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -121,25 +80,20 @@ async fn create_ata_initializes_holding_account() -> Result<()> {
|
||||
async fn create_ata_is_idempotent() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let owner_account_id = new_public_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let owner_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create a fungible token
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply: 100,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
100,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Create the ATA once
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -196,28 +150,23 @@ async fn create_ata_is_idempotent() -> Result<()> {
|
||||
async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let sender_account_id = new_public_account(&mut ctx).await?;
|
||||
let recipient_account_id = new_public_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let sender_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let recipient_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
let total_supply = 1000_u128;
|
||||
|
||||
// Create a fungible token, supply goes to supply_account_id
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
total_supply,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Derive ATA addresses
|
||||
let ata_program_id = programs::ata().id();
|
||||
let sender_ata_id = get_associated_token_account_id(
|
||||
@ -252,23 +201,14 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
|
||||
// Fund sender's ATA from the supply account (direct token transfer)
|
||||
let fund_amount = 200_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id),
|
||||
to: Some(public_mention(sender_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id),
|
||||
public_mention(sender_ata_id),
|
||||
fund_amount,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Transfer from sender's ATA to recipient's ATA via the ATA program
|
||||
let transfer_amount = 50_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
@ -286,7 +226,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify sender ATA balance decreased
|
||||
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
|
||||
let sender_ata_acc = get_account(&ctx, sender_ata_id).await?;
|
||||
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
sender_holding,
|
||||
@ -297,7 +237,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify recipient ATA balance increased
|
||||
let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?;
|
||||
let recipient_ata_acc = get_account(&ctx, recipient_ata_id).await?;
|
||||
let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
recipient_holding,
|
||||
@ -323,7 +263,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify sender ATA balance after burn
|
||||
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
|
||||
let sender_ata_acc = get_account(&ctx, sender_ata_id).await?;
|
||||
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
sender_holding,
|
||||
@ -334,10 +274,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify the token definition total_supply decreased by burn_amount
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
assert_eq!(
|
||||
token_definition,
|
||||
@ -355,25 +292,20 @@ async fn transfer_and_burn_via_ata() -> Result<()> {
|
||||
async fn create_ata_with_private_owner() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let owner_account_id = new_private_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let owner_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create a fungible token
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply: 100,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
100,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Create the ATA for the private owner + definition
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -424,28 +356,23 @@ async fn create_ata_with_private_owner() -> Result<()> {
|
||||
async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let sender_account_id = new_private_account(&mut ctx).await?;
|
||||
let recipient_account_id = new_public_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let sender_account_id = new_account(&mut ctx, true, None).await?;
|
||||
let recipient_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
let total_supply = 1000_u128;
|
||||
|
||||
// Create a fungible token
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
total_supply,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Derive ATA addresses
|
||||
let ata_program_id = programs::ata().id();
|
||||
let sender_ata_id = get_associated_token_account_id(
|
||||
@ -480,23 +407,14 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
|
||||
// Fund sender's ATA from the supply account (direct token transfer)
|
||||
let fund_amount = 200_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id),
|
||||
to: Some(public_mention(sender_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id),
|
||||
public_mention(sender_ata_id),
|
||||
fund_amount,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Transfer from sender's ATA (private owner) to recipient's ATA
|
||||
let transfer_amount = 50_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
@ -514,7 +432,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify sender ATA balance decreased
|
||||
let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?;
|
||||
let sender_ata_acc = get_account(&ctx, sender_ata_id).await?;
|
||||
let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
sender_holding,
|
||||
@ -525,7 +443,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify recipient ATA balance increased
|
||||
let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?;
|
||||
let recipient_ata_acc = get_account(&ctx, recipient_ata_id).await?;
|
||||
let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
recipient_holding,
|
||||
@ -549,27 +467,22 @@ async fn transfer_via_ata_private_owner() -> Result<()> {
|
||||
async fn burn_via_ata_private_owner() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let definition_account_id = new_public_account(&mut ctx).await?;
|
||||
let supply_account_id = new_public_account(&mut ctx).await?;
|
||||
let holder_account_id = new_private_account(&mut ctx).await?;
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
let holder_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
let total_supply = 500_u128;
|
||||
|
||||
// Create a fungible token
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::New {
|
||||
definition_account_id: public_mention(definition_account_id),
|
||||
supply_account_id: public_mention(supply_account_id),
|
||||
name: "TEST".to_owned(),
|
||||
total_supply,
|
||||
}),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id),
|
||||
public_mention(supply_account_id),
|
||||
"TEST".to_owned(),
|
||||
total_supply,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Derive holder's ATA address
|
||||
let ata_program_id = programs::ata().id();
|
||||
let holder_ata_id = get_associated_token_account_id(
|
||||
@ -592,23 +505,14 @@ async fn burn_via_ata_private_owner() -> Result<()> {
|
||||
|
||||
// Fund holder's ATA from the supply account
|
||||
let fund_amount = 300_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Token(TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id),
|
||||
to: Some(public_mention(holder_ata_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: fund_amount,
|
||||
}),
|
||||
token_send(
|
||||
&mut ctx,
|
||||
public_mention(supply_account_id),
|
||||
public_mention(holder_ata_id),
|
||||
fund_amount,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Burn from holder's ATA (private owner)
|
||||
let burn_amount = 100_u128;
|
||||
wallet::cli::execute_subcommand(
|
||||
@ -625,7 +529,7 @@ async fn burn_via_ata_private_owner() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify holder ATA balance after burn
|
||||
let holder_ata_acc = ctx.sequencer_client().get_account(holder_ata_id).await?;
|
||||
let holder_ata_acc = get_account(&ctx, holder_ata_id).await?;
|
||||
let holder_holding = TokenHolding::try_from(&holder_ata_acc.data)?;
|
||||
assert_eq!(
|
||||
holder_holding,
|
||||
@ -636,10 +540,7 @@ async fn burn_via_ata_private_owner() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify the token definition total_supply decreased by burn_amount
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
assert_eq!(
|
||||
token_definition,
|
||||
|
||||
@ -3,17 +3,18 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
|
||||
public_mention, verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance,
|
||||
assert_private_commitment_in_state, fetch_privacy_preserving_tx, get_account, new_account,
|
||||
private_mention, public_mention, send, sync_private, verify_commitment_is_in_state,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, SharedSecretKey, execute_and_prove,
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
@ -34,32 +35,13 @@ async fn private_transfer_to_owned_account() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
let to: AccountId = ctx.existing_private_accounts()[1];
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(&mut ctx, private_mention(from), private_mention(to), 100).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);
|
||||
assert_private_commitment_in_state(&ctx, from, "sender").await?;
|
||||
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
|
||||
|
||||
info!("Successfully transferred privately to owned account");
|
||||
|
||||
@ -86,8 +68,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
});
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = result else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
@ -125,17 +107,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
.context("Failed to get sender's private account")?;
|
||||
assert_eq!(from_acc.balance, 10000);
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(public_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(&mut ctx, private_mention(from), public_mention(to), 100).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -144,13 +116,9 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
.wallet()
|
||||
.get_account_private(from)
|
||||
.context("Failed to get sender's private account")?;
|
||||
let new_commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(from)
|
||||
.context("Failed to get private account commitment")?;
|
||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||
assert_private_commitment_in_state(&ctx, from, "sender").await?;
|
||||
|
||||
let acc_2_balance = ctx.sequencer_client().get_account_balance(to).await?;
|
||||
let acc_2_balance = account_balance(&ctx, to).await?;
|
||||
|
||||
assert_eq!(from_acc.balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
@ -167,18 +135,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
|
||||
// Create a new private account
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id,
|
||||
} = sub_ret
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let to_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Get the keys for the newly created account
|
||||
let to = ctx
|
||||
@ -200,21 +157,20 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
|
||||
// Sync the wallet to claim the new account
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
let new_commitment1 = ctx
|
||||
let sender_commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(from)
|
||||
.context("Failed to get private account commitment for sender")?;
|
||||
assert_eq!(tx.message.new_commitments[0], new_commitment1);
|
||||
assert_eq!(tx.message.new_commitments[0], sender_commitment);
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 2);
|
||||
for commitment in tx.message.new_commitments {
|
||||
@ -239,17 +195,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_public_accounts()[0];
|
||||
let to: AccountId = ctx.existing_private_accounts()[1];
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(from),
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(&mut ctx, public_mention(from), private_mention(to), 100).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
@ -258,13 +204,9 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
|
||||
.wallet()
|
||||
.get_account_private(to)
|
||||
.context("Failed to get receiver's private account")?;
|
||||
let new_commitment = ctx
|
||||
.wallet()
|
||||
.get_private_account_commitment(to)
|
||||
.context("Failed to get receiver's commitment")?;
|
||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
|
||||
|
||||
let acc_from_balance = ctx.sequencer_client().get_account_balance(from).await?;
|
||||
let acc_from_balance = account_balance(&ctx, from).await?;
|
||||
|
||||
assert_eq!(acc_from_balance, 9900);
|
||||
assert_eq!(acc_to.balance, 20100);
|
||||
@ -294,8 +236,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
});
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = result else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
@ -303,7 +245,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
|
||||
let acc_1_balance = ctx.sequencer_client().get_account_balance(from).await?;
|
||||
let acc_1_balance = account_balance(&ctx, from).await?;
|
||||
|
||||
assert!(
|
||||
verify_commitment_is_in_state(
|
||||
@ -332,18 +274,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
|
||||
// Create a new private account
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id,
|
||||
} = sub_ret
|
||||
else {
|
||||
anyhow::bail!("Failed to register account");
|
||||
};
|
||||
let to_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Get the newly created account's keys
|
||||
let to = ctx
|
||||
@ -365,7 +296,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Failed to send transaction");
|
||||
};
|
||||
|
||||
@ -396,14 +327,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
async fn initialize_private_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
account_id: private_mention(account_id),
|
||||
@ -413,14 +337,9 @@ async fn initialize_private_account() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
info!("Syncing private accounts");
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).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);
|
||||
assert_private_commitment_in_state(&ctx, account_id, "account").await?;
|
||||
|
||||
let account = ctx
|
||||
.wallet()
|
||||
@ -455,32 +374,19 @@ async fn private_transfer_using_from_label() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Send using the label instead of account ID
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: CliAccountMention::Label(label),
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
CliAccountMention::Label(label),
|
||||
private_mention(to),
|
||||
100,
|
||||
)
|
||||
.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);
|
||||
assert_private_commitment_in_state(&ctx, from, "sender").await?;
|
||||
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
|
||||
|
||||
info!("Successfully transferred privately using from_label");
|
||||
|
||||
@ -510,14 +416,9 @@ async fn initialize_private_account_using_label() -> Result<()> {
|
||||
|
||||
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?;
|
||||
sync_private(&mut ctx).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);
|
||||
assert_private_commitment_in_state(&ctx, account_id, "account").await?;
|
||||
|
||||
let account = ctx
|
||||
.wallet()
|
||||
@ -593,21 +494,17 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// Both accounts must be discovered with the correct balances.
|
||||
let account_id_1 = AccountId::for_regular_private_account(&npk, identifier_1);
|
||||
let account_id_1 = AccountId::for_regular_private_account(&npk, &vpk, identifier_1);
|
||||
let acc_1 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_1)
|
||||
.context("account for identifier 1 not found after sync")?;
|
||||
assert_eq!(acc_1.balance, 100);
|
||||
|
||||
let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2);
|
||||
let account_id_2 = AccountId::for_regular_private_account(&npk, &vpk, identifier_2);
|
||||
let acc_2 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_2)
|
||||
@ -663,25 +560,19 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let nsk: lee_core::NullifierSecretKey = [3; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let ssk = SharedSecretKey([55_u8; 32]);
|
||||
let epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
let attacker_vault_id = {
|
||||
let seed = vault_core::compute_vault_seed(attacker_id);
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, &vpk, 1337)
|
||||
};
|
||||
let amount: u128 = 1;
|
||||
|
||||
let faucet_pre = AccountWithMetadata::new(
|
||||
ctx.sequencer_client()
|
||||
.get_account(faucet_account_id)
|
||||
.await?,
|
||||
get_account(&ctx, faucet_account_id).await?,
|
||||
false,
|
||||
faucet_account_id,
|
||||
);
|
||||
let vault_pda_pre = AccountWithMetadata::new(
|
||||
ctx.sequencer_client()
|
||||
.get_account(attacker_vault_id)
|
||||
.await?,
|
||||
get_account(&ctx, attacker_vault_id).await?,
|
||||
false,
|
||||
attacker_vault_id,
|
||||
);
|
||||
@ -705,11 +596,11 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
vpk,
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
ssk,
|
||||
identifier: 1337,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
},
|
||||
],
|
||||
@ -720,3 +611,89 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prove_init_with_commitment_root(
|
||||
ctx: &TestContext,
|
||||
commitment_root: lee_core::CommitmentSetDigest,
|
||||
) -> Result<lee_core::PrivacyPreservingCircuitOutput> {
|
||||
let program = programs::authenticated_transfer();
|
||||
let sender_id = ctx.existing_public_accounts()[0];
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
ctx.sequencer_client().get_account(sender_id).await?,
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
let nsk: lee_core::NullifierSecretKey = [7; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender_pre, recipient],
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
|
||||
amount: 1,
|
||||
})?,
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk,
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
identifier: 0,
|
||||
commitment_root,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?;
|
||||
|
||||
let nsk: lee_core::NullifierSecretKey = [7; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
|
||||
let output = prove_init_with_commitment_root(&ctx, expected_digest).await?;
|
||||
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
let (nullifier, digest) = &output.new_nullifiers[0];
|
||||
assert_eq!(
|
||||
*nullifier,
|
||||
Nullifier::for_account_initialization(&recipient_account_id)
|
||||
);
|
||||
assert_eq!(*digest, expected_digest);
|
||||
assert_ne!(*digest, DUMMY_COMMITMENT_HASH);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?;
|
||||
|
||||
let output_with_root = prove_init_with_commitment_root(&ctx, expected_digest).await?;
|
||||
let output_without_root = prove_init_with_commitment_root(&ctx, DUMMY_COMMITMENT_HASH).await?;
|
||||
|
||||
assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest);
|
||||
assert_eq!(
|
||||
output_without_root.new_nullifiers[0].1,
|
||||
DUMMY_COMMITMENT_HASH
|
||||
);
|
||||
assert_ne!(
|
||||
output_with_root.new_nullifiers[0].1,
|
||||
output_without_root.new_nullifiers[0].1,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -2,7 +2,10 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, new_account,
|
||||
public_mention, send,
|
||||
};
|
||||
use lee::public_transaction;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
@ -10,8 +13,7 @@ use tokio::test;
|
||||
use wallet::{
|
||||
account::Label,
|
||||
cli::{
|
||||
CliAccountMention, Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
CliAccountMention, Command, account::AccountSubcommand,
|
||||
programs::native_token_transfer::AuthTransferSubcommand,
|
||||
},
|
||||
};
|
||||
@ -20,30 +22,22 @@ use wallet::{
|
||||
async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let receiver = ctx.existing_public_accounts()[1];
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(sender),
|
||||
public_mention(receiver),
|
||||
100,
|
||||
)
|
||||
.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?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, receiver).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -58,51 +52,23 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
pub async fn successful_transfer_to_new_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
let new_persistent_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_persistent_account_id = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.public_account_ids()
|
||||
.map(|(account_id, _)| account_id)
|
||||
.find(|acc_id| {
|
||||
*acc_id != ctx.existing_public_accounts()[0]
|
||||
&& *acc_id != ctx.existing_public_accounts()[1]
|
||||
})
|
||||
.expect("Failed to find newly created account in the wallet storage");
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(new_persistent_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(sender),
|
||||
public_mention(new_persistent_account_id),
|
||||
100,
|
||||
)
|
||||
.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(new_persistent_account_id)
|
||||
.await?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, new_persistent_account_id).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -134,14 +100,8 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
info!("Checking balances unchanged");
|
||||
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?;
|
||||
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -156,31 +116,24 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
|
||||
async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// First transfer
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let receiver = ctx.existing_public_accounts()[1];
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
// First transfer
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(sender),
|
||||
public_mention(receiver),
|
||||
100,
|
||||
)
|
||||
.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 after first transfer");
|
||||
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?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, receiver).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -191,30 +144,20 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
info!("First TX Success!");
|
||||
|
||||
// Second transfer
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(sender),
|
||||
public_mention(receiver),
|
||||
100,
|
||||
)
|
||||
.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 after second transfer");
|
||||
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?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, receiver).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -231,14 +174,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
|
||||
async fn initialize_public_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
account_id: public_mention(account_id),
|
||||
@ -246,7 +182,7 @@ async fn initialize_public_account() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
info!("Checking correct execution");
|
||||
let account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
let account = get_account(&ctx, account_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
account.program_owner,
|
||||
@ -274,30 +210,22 @@ async fn successful_transfer_using_from_label() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Send using the label instead of account ID
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: CliAccountMention::Label(label),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let receiver = ctx.existing_public_accounts()[1];
|
||||
send(
|
||||
&mut ctx,
|
||||
CliAccountMention::Label(label),
|
||||
public_mention(receiver),
|
||||
100,
|
||||
)
|
||||
.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?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, receiver).await?;
|
||||
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
@ -320,30 +248,22 @@ async fn successful_transfer_using_to_label() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Send using the label for the recipient
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(CliAccountMention::Label(label)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let receiver = ctx.existing_public_accounts()[1];
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(sender),
|
||||
CliAccountMention::Label(label),
|
||||
100,
|
||||
)
|
||||
.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?;
|
||||
let acc_1_balance = account_balance(&ctx, sender).await?;
|
||||
let acc_2_balance = account_balance(&ctx, receiver).await?;
|
||||
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
@ -359,14 +279,8 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
|
||||
let faucet_account_id = system_accounts::faucet_account_id();
|
||||
|
||||
let recipient = ctx.existing_public_accounts()[0];
|
||||
let recipient_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let recipient_balance_before = account_balance(&ctx, recipient).await?;
|
||||
let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?;
|
||||
|
||||
let amount = 1_u128;
|
||||
let message = public_transaction::Message::try_new(
|
||||
@ -387,14 +301,8 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let recipient_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let recipient_balance_after = account_balance(&ctx, recipient).await?;
|
||||
let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(recipient_balance_after, recipient_balance_before);
|
||||
@ -413,14 +321,8 @@ async fn cannot_execute_faucet_program() -> Result<()> {
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient);
|
||||
|
||||
let recipient_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let recipient_balance_before = account_balance(&ctx, recipient).await?;
|
||||
let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?;
|
||||
|
||||
let amount = 1_u128;
|
||||
let message = public_transaction::Message::try_new(
|
||||
@ -445,14 +347,8 @@ async fn cannot_execute_faucet_program() -> Result<()> {
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let recipient_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let recipient_balance_after = account_balance(&ctx, recipient).await?;
|
||||
let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(recipient_balance_after, recipient_balance_before);
|
||||
@ -493,28 +389,16 @@ async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
let faucet_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(attacker_vault_id)
|
||||
.await?;
|
||||
let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?;
|
||||
let vault_balance_before = account_balance(&ctx, attacker_vault_id).await?;
|
||||
|
||||
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let faucet_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(faucet_account_id)
|
||||
.await?;
|
||||
let vault_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(attacker_vault_id)
|
||||
.await?;
|
||||
let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?;
|
||||
let vault_balance_after = account_balance(&ctx, attacker_vault_id).await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(faucet_balance_after, faucet_balance_before);
|
||||
|
||||
@ -11,7 +11,8 @@ use borsh::BorshSerialize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use futures::StreamExt as _;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, wait_for_indexer_to_catch_up,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account,
|
||||
wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
|
||||
@ -65,27 +66,15 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
let bridge_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(bridge_account_id)
|
||||
.await?;
|
||||
let vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let bridge_balance_before = account_balance(&ctx, bridge_account_id).await?;
|
||||
let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
|
||||
|
||||
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let bridge_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(bridge_account_id)
|
||||
.await?;
|
||||
let vault_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let bridge_balance_after = account_balance(&ctx, bridge_account_id).await?;
|
||||
let vault_balance_after = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(bridge_balance_after, bridge_balance_before);
|
||||
@ -169,16 +158,12 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
|
||||
// Get pre-state of bridge and vault accounts
|
||||
let bridge_pre = AccountWithMetadata::new(
|
||||
ctx.sequencer_client()
|
||||
.get_account(bridge_account_id)
|
||||
.await?,
|
||||
get_account(&ctx, bridge_account_id).await?,
|
||||
false,
|
||||
bridge_account_id,
|
||||
);
|
||||
let vault_pre = AccountWithMetadata::new(
|
||||
ctx.sequencer_client()
|
||||
.get_account(recipient_vault_id)
|
||||
.await?,
|
||||
get_account(&ctx, recipient_vault_id).await?,
|
||||
false,
|
||||
recipient_vault_id,
|
||||
);
|
||||
@ -229,27 +214,15 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
witness_set,
|
||||
));
|
||||
|
||||
let bridge_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(bridge_account_id)
|
||||
.await?;
|
||||
let vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let bridge_balance_before = account_balance(&ctx, bridge_account_id).await?;
|
||||
let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
|
||||
|
||||
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let bridge_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(bridge_account_id)
|
||||
.await?;
|
||||
let vault_balance_after = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let bridge_balance_after = account_balance(&ctx, bridge_account_id).await?;
|
||||
let vault_balance_after = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?;
|
||||
|
||||
assert_eq!(bridge_balance_after, bridge_balance_before);
|
||||
@ -421,7 +394,7 @@ async fn wait_for_vault_balance(
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
let balance = ctx.sequencer_client().get_account_balance(vault_id).await?;
|
||||
let balance = account_balance(ctx, vault_id).await?;
|
||||
if balance == expected_balance {
|
||||
return Ok(());
|
||||
}
|
||||
@ -449,14 +422,8 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res
|
||||
let vault_program_id = programs::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
|
||||
let vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let recipient_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_id)
|
||||
.await?;
|
||||
let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
|
||||
|
||||
// Submit deposit to Bedrock
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
@ -507,14 +474,8 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?;
|
||||
let vault_balance_after_claim = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
let recipient_balance_after_claim = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_id)
|
||||
.await?;
|
||||
let vault_balance_after_claim = account_balance(&ctx, recipient_vault_id).await?;
|
||||
let recipient_balance_after_claim = account_balance(&ctx, recipient_id).await?;
|
||||
|
||||
assert!(
|
||||
claim_on_chain.is_some(),
|
||||
@ -543,7 +504,7 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res
|
||||
account_id.into(),
|
||||
)
|
||||
.await?;
|
||||
let sequencer_account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
let sequencer_account = get_account(&ctx, account_id).await?;
|
||||
assert_eq!(
|
||||
indexer_account,
|
||||
sequencer_account.into(),
|
||||
|
||||
197
integration_tests/tests/cross_zone_bridge.rs
Normal file
197
integration_tests/tests/cross_zone_bridge.rs
Normal file
@ -0,0 +1,197 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Demo 2: a wrapped-token bridge over the cross-zone spine. A holder locks part
|
||||
//! of their bridgeable balance on zone A; the watcher carries the emitted mint to
|
||||
//! zone B, where the indexer re-derives and verifies it (Option B) before the
|
||||
//! wrapped token is minted to the recipient. Reuses the M3/M4 spine unchanged;
|
||||
//! only the source caller (`bridge_lock`) and target (`wrapped_token`) are new.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{setup_bedrock_node, setup_indexer, setup_sequencer},
|
||||
};
|
||||
use lee::{
|
||||
AccountId, PrivateKey, PublicKey, PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, GenesisAction};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
const INITIAL_BALANCE: u128 = 100;
|
||||
const LOCK_AMOUNT: u128 = 30;
|
||||
const RECIPIENT: [u8; 32] = [9; 32];
|
||||
|
||||
#[test]
|
||||
async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let holder_key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let holder_id = AccountId::from(&PublicKey::new_from_private_key(&holder_key));
|
||||
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: *channel_a.as_ref(),
|
||||
allowed_targets: vec![wrapped_token_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// Zone A seeds the holder's bridgeable balance. Zone B runs the watcher on its
|
||||
// sequencer and the verifier on its indexer.
|
||||
let genesis_a = vec![GenesisAction::SupplyBridgeLockHolding {
|
||||
holder: holder_id,
|
||||
amount: INITIAL_BALANCE,
|
||||
}];
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, genesis_a, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_seq_b, _seq_b_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
vec![],
|
||||
channel_b,
|
||||
Some(cross_zone.clone()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Lock LOCK_AMOUNT on zone A, addressed to the recipient on zone B.
|
||||
let lock = build_lock_tx(&holder_key, holder_id, zone_b);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(lock)
|
||||
.await
|
||||
.context("Failed to submit lock on zone A")?;
|
||||
|
||||
// Wait until zone B's indexer reflects the verified mint.
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, idx_b.addr())
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let minted = wait_for_mint(&indexer, holding_id).await?;
|
||||
assert_eq!(
|
||||
minted, LOCK_AMOUNT,
|
||||
"zone B must mint exactly the locked amount"
|
||||
);
|
||||
|
||||
// Conservation: the mint on B must be backed by an equal lock on A. The lock
|
||||
// has already landed (it preceded delivery), so zone A reflects the debit and
|
||||
// escrow now.
|
||||
let seq_a_client = sequencer_client(seq_a.addr())?;
|
||||
let escrow_id = bridge_lock_core::escrow_account_id(programs::bridge_lock().id());
|
||||
let escrowed = bridge_lock_core::read_balance(
|
||||
&seq_a_client.get_account(escrow_id).await?.data.into_inner(),
|
||||
);
|
||||
assert_eq!(
|
||||
escrowed, LOCK_AMOUNT,
|
||||
"zone A escrow must hold the locked amount"
|
||||
);
|
||||
let remaining = bridge_lock_core::read_balance(
|
||||
&seq_a_client.get_account(holder_id).await?.data.into_inner(),
|
||||
);
|
||||
assert_eq!(
|
||||
remaining,
|
||||
INITIAL_BALANCE - LOCK_AMOUNT,
|
||||
"zone A holder must be debited by the locked amount"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a signed `bridge_lock` Lock that forwards a wrapped-token Mint of the
|
||||
/// locked amount to the recipient on the target zone.
|
||||
fn build_lock_tx(
|
||||
holder_key: &PrivateKey,
|
||||
holder_id: AccountId,
|
||||
target_zone: [u8; 32],
|
||||
) -> LeeTransaction {
|
||||
let bridge_lock_id = programs::bridge_lock().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let mint = wrapped_token_core::Instruction::Mint {
|
||||
recipient: RECIPIENT,
|
||||
amount: LOCK_AMOUNT,
|
||||
};
|
||||
let words = risc0_zkvm::serde::to_vec(&mint).expect("serialize mint");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let target_accounts = vec![
|
||||
wrapped_token_core::config_account_id(wrapped_token_id).into_value(),
|
||||
wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT).into_value(),
|
||||
];
|
||||
let lock = bridge_lock_core::Instruction::Lock {
|
||||
amount: LOCK_AMOUNT,
|
||||
target_zone,
|
||||
target_program_id: wrapped_token_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
outbox_program_id: outbox_id,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let accounts = vec![
|
||||
holder_id,
|
||||
bridge_lock_core::escrow_account_id(bridge_lock_id),
|
||||
outbox_pda(outbox_id, &target_zone, ordinal),
|
||||
];
|
||||
// One nonce per signature: the holder signs, at its genesis nonce 0.
|
||||
let message = Message::try_new(bridge_lock_id, accounts, vec![0_u128.into()], lock)
|
||||
.expect("build lock message");
|
||||
let witness = WitnessSet::for_message(&message, &[holder_key]);
|
||||
LeeTransaction::Public(PublicTransaction::new(message, witness))
|
||||
}
|
||||
|
||||
fn sequencer_client(addr: SocketAddr) -> Result<SequencerClient> {
|
||||
let url = config::addr_to_url(config::UrlProtocol::Http, addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
SequencerClientBuilder::default()
|
||||
.build(url)
|
||||
.context("Failed to build sequencer client")
|
||||
}
|
||||
|
||||
/// Polls zone B's indexer until the recipient's wrapped holding is non-zero.
|
||||
async fn wait_for_mint(indexer: &IndexerClient, holding_id: AccountId) -> Result<u128> {
|
||||
let account_id = indexer_service_protocol::AccountId {
|
||||
value: holding_id.into_value(),
|
||||
};
|
||||
let wait = async {
|
||||
loop {
|
||||
let account =
|
||||
indexer_service_rpc::RpcClient::get_account(&**indexer, account_id).await?;
|
||||
let balance = wrapped_token_core::read_balance(&account.data.0);
|
||||
if balance != 0 {
|
||||
return Ok::<u128, anyhow::Error>(balance);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B's indexer did not mint the wrapped token in time")?
|
||||
}
|
||||
80
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
80
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
@ -0,0 +1,80 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
//! M6 ingress guard: the cross-zone inbox is sequencer-only. Only the watcher
|
||||
//! injects inbox dispatches; a user must not be able to invoke the inbox through
|
||||
//! the public RPC, or anyone could forge an inbound cross-zone delivery. The
|
||||
//! inbox guest's caller-is-none assertion passes for a top-level user tx, so the
|
||||
//! sequencer ingress guard is the only thing that stops this.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, Instruction, inbox_config_account_id, inbox_seen_shard_account_id,
|
||||
};
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{setup_bedrock_node, setup_sequencer},
|
||||
};
|
||||
use lee::{
|
||||
PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
#[test]
|
||||
async fn user_origin_inbox_call_rejected() -> Result<()> {
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up Bedrock node")?;
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel = config::bedrock_channel_id();
|
||||
let (seq, _seq_home) = setup_sequencer(partial, bedrock_addr, vec![], channel, None)
|
||||
.await
|
||||
.context("Failed to set up sequencer")?;
|
||||
|
||||
// A user hand-builds a top-level inbox Dispatch and submits it via RPC.
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone: [2; 32],
|
||||
src_block_id: 1,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9; 8],
|
||||
target_program_id: programs::ping_receiver().id(),
|
||||
payload: vec![],
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &msg.src_zone, msg.src_block_id);
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![inbox_config_account_id(inbox_id), seen_id],
|
||||
vec![],
|
||||
Instruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
let result = sequencer_client(seq.addr())?.send_transaction(tx).await;
|
||||
let err = result.expect_err("the sequencer must reject a user-origin inbox call");
|
||||
assert!(
|
||||
err.to_string().contains("sequencer-only"),
|
||||
"rejection should cite the sequencer-only guard, got: {err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sequencer_client(addr: SocketAddr) -> Result<SequencerClient> {
|
||||
let url = config::addr_to_url(config::UrlProtocol::Http, addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
SequencerClientBuilder::default()
|
||||
.build(url)
|
||||
.context("Failed to build sequencer client")
|
||||
}
|
||||
143
integration_tests/tests/cross_zone_ping.rs
Normal file
143
integration_tests/tests/cross_zone_ping.rs
Normal file
@ -0,0 +1,143 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! End-to-end cross-zone round trip: a ping submitted on zone A is delivered by
|
||||
//! zone B's watcher to `ping_receiver` on zone B, which records the payload.
|
||||
//!
|
||||
//! Two sequencers share one Bedrock node (no indexers): zone A publishes the
|
||||
//! ping to Bedrock, zone B's watcher reads zone A's finalized blocks, injects the
|
||||
//! inbox dispatch, and zone B's sequencer delivers it. This is the M3 milestone,
|
||||
//! sequencer-trusted, with no indexer re-derivation (that is M4).
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{setup_bedrock_node, setup_sequencer},
|
||||
};
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
|
||||
|
||||
#[test]
|
||||
async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
// Zone B watches zone A and allows delivery only to ping_receiver.
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (seq_b, _seq_b_home) =
|
||||
setup_sequencer(partial, bedrock_addr, vec![], channel_b, Some(cross_zone))
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
|
||||
// Submit the ping on zone A, addressed to ping_receiver on zone B.
|
||||
let ping = build_ping_tx(zone_b, receiver_id);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(ping)
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
|
||||
// Wait until zone B's sequencer records the delivered payload.
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let delivered = wait_for_delivery(sequencer_client(seq_b.addr())?, record_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B must record the payload delivered from zone A"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a top-level `ping_sender` transaction that chains into the outbox to emit
|
||||
/// a message carrying a `ping_receiver::Record` instruction for the target zone.
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
// The payload is the ping_receiver instruction, serialized as risc0 words in
|
||||
// little-endian bytes (the contract the inbox reverses when forwarding).
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
fn sequencer_client(addr: SocketAddr) -> Result<SequencerClient> {
|
||||
let url = config::addr_to_url(config::UrlProtocol::Http, addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
SequencerClientBuilder::default()
|
||||
.build(url)
|
||||
.context("Failed to build sequencer client")
|
||||
}
|
||||
|
||||
/// Polls zone B's sequencer until the ping record PDA holds a payload.
|
||||
async fn wait_for_delivery(client: SequencerClient, record_id: AccountId) -> Result<Vec<u8>> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let account = client.get_account(record_id).await?;
|
||||
let data = account.data.into_inner();
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B did not record the cross-zone payload in time")?
|
||||
}
|
||||
373
integration_tests/tests/cross_zone_state_machine.rs
Normal file
373
integration_tests/tests/cross_zone_state_machine.rs
Normal file
@ -0,0 +1,373 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Single-zone state-machine tests for cross-zone delivery (ping demo) and the
|
||||
//! wrapped-token bridge (Demo 2). They drive the guests in isolation, no watcher
|
||||
//! or Bedrock: a hand-built `cross_zone_inbox::Dispatch` (as the watcher would
|
||||
//! inject) and the source `bridge_lock::Lock` (which escrows and chains
|
||||
//! `outbox::Emit`). Fast, so they pin guest logic before the e2e exercises the
|
||||
//! plumbing. Run with `RISC0_DEV_MODE=1`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, InboxConfig, Instruction as InboxInstruction, SeenShard,
|
||||
inbox_config_account_id, inbox_seen_shard_account_id, message_key,
|
||||
};
|
||||
use cross_zone_outbox_core::{OutboxRecord, outbox_pda};
|
||||
use lee::{
|
||||
AccountId, PrivateKey, PublicKey, PublicTransaction, V03State, ValidatedStateDiff,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use lee_core::account::Account;
|
||||
use ping_core::{ReceiverInstruction, ping_record_pda};
|
||||
|
||||
const INITIAL_BALANCE: u128 = 100;
|
||||
const LOCK_AMOUNT: u128 = 30;
|
||||
const RECIPIENT: [u8; 32] = [9; 32];
|
||||
|
||||
/// State registering the cross-zone builtins these tests exercise.
|
||||
fn base_state() -> V03State {
|
||||
V03State::new().with_programs([
|
||||
programs::cross_zone_inbox(),
|
||||
programs::cross_zone_outbox(),
|
||||
programs::ping_receiver(),
|
||||
programs::bridge_lock(),
|
||||
programs::wrapped_token(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Seeds an inbox config (inbox-owned) allowing `src_zone -> target`.
|
||||
fn seed_inbox_config(
|
||||
state: &mut V03State,
|
||||
self_zone: [u8; 32],
|
||||
src_zone: [u8; 32],
|
||||
target: lee_core::program::ProgramId,
|
||||
) {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
allowed_targets.insert(src_zone, vec![target]);
|
||||
let config = InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
};
|
||||
state.insert_genesis_account(
|
||||
inbox_config_account_id(inbox_id),
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: config
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("config fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Seeds the wrapped-token config account pinning the inbox as authorized minter,
|
||||
/// matching what genesis seeds for a real zone.
|
||||
fn seed_wrapped_config(state: &mut V03State) {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
state.insert_genesis_account(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
data: wrapped_token_core::minter_bytes(programs::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data"),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The wrapped-token `Mint` the bridge forwards, serialized as the cross-zone
|
||||
/// payload (risc0 words, little-endian bytes).
|
||||
fn mint_payload() -> Vec<u8> {
|
||||
let mint = wrapped_token_core::Instruction::Mint {
|
||||
recipient: RECIPIENT,
|
||||
amount: LOCK_AMOUNT,
|
||||
};
|
||||
let words = risc0_zkvm::serde::to_vec(&mint).expect("serialize mint");
|
||||
words.iter().flat_map(|word| word.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Drives `cross_zone_inbox::Dispatch` directly through the state machine
|
||||
/// (no watcher) and asserts the message is delivered to `ping_receiver`, which
|
||||
/// records the payload into its own PDA.
|
||||
#[test]
|
||||
fn inbox_dispatch_delivers_payload_to_ping_receiver() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, receiver_id);
|
||||
|
||||
// The payload is the ping_receiver instruction, serialized as risc0 words in
|
||||
// little-endian bytes (the contract the inbox reverses when forwarding).
|
||||
let inner = b"hello-cross-zone".to_vec();
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: inner.clone(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: receiver_id,
|
||||
payload,
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![inbox_config_account_id(inbox_id), seen_id, record_id],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("dispatch must validate and execute");
|
||||
let record = diff
|
||||
.public_diff()
|
||||
.get(&record_id)
|
||||
.expect("ping record account must change")
|
||||
.clone();
|
||||
assert_eq!(
|
||||
record.data.into_inner(),
|
||||
inner,
|
||||
"ping_receiver must record the delivered payload"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drives `bridge_lock::Lock` and asserts it debits the holder, credits the
|
||||
/// escrow, and records the forwarded mint in the outbox PDA.
|
||||
#[test]
|
||||
fn lock_escrows_balance_and_emits_to_outbox() {
|
||||
let bridge_lock_id = programs::bridge_lock().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let zone_b = [2_u8; 32];
|
||||
let ordinal = 0;
|
||||
|
||||
let mut state = base_state();
|
||||
|
||||
let holder_key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let holder_id = AccountId::from(&PublicKey::new_from_private_key(&holder_key));
|
||||
state.insert_genesis_account(
|
||||
holder_id,
|
||||
Account {
|
||||
program_owner: bridge_lock_id,
|
||||
balance: 0,
|
||||
data: bridge_lock_core::balance_bytes(INITIAL_BALANCE)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("balance fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
|
||||
let payload = mint_payload();
|
||||
let target_accounts = vec![
|
||||
wrapped_token_core::config_account_id(wrapped_token_id).into_value(),
|
||||
wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT).into_value(),
|
||||
];
|
||||
let lock = bridge_lock_core::Instruction::Lock {
|
||||
amount: LOCK_AMOUNT,
|
||||
target_zone: zone_b,
|
||||
target_program_id: wrapped_token_id,
|
||||
target_accounts,
|
||||
payload: payload.clone(),
|
||||
outbox_program_id: outbox_id,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let escrow_id = bridge_lock_core::escrow_account_id(bridge_lock_id);
|
||||
let outbox_record_id = outbox_pda(outbox_id, &zone_b, ordinal);
|
||||
let message = Message::try_new(
|
||||
bridge_lock_id,
|
||||
vec![holder_id, escrow_id, outbox_record_id],
|
||||
vec![0_u128.into()],
|
||||
lock,
|
||||
)
|
||||
.expect("build lock message");
|
||||
let witness = WitnessSet::for_message(&message, &[&holder_key]);
|
||||
let tx = PublicTransaction::new(message, witness);
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("lock must validate and execute");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
let holder_after =
|
||||
bridge_lock_core::read_balance(&public_diff[&holder_id].data.clone().into_inner());
|
||||
assert_eq!(
|
||||
holder_after,
|
||||
INITIAL_BALANCE - LOCK_AMOUNT,
|
||||
"holder debited"
|
||||
);
|
||||
|
||||
let escrow_after =
|
||||
bridge_lock_core::read_balance(&public_diff[&escrow_id].data.clone().into_inner());
|
||||
assert_eq!(escrow_after, LOCK_AMOUNT, "escrow credited");
|
||||
|
||||
let record =
|
||||
OutboxRecord::from_bytes(&public_diff[&outbox_record_id].data.clone().into_inner())
|
||||
.expect("outbox PDA holds an OutboxRecord");
|
||||
assert_eq!(record.target_zone, zone_b);
|
||||
assert_eq!(record.target_program_id, wrapped_token_id);
|
||||
assert_eq!(
|
||||
record.payload, payload,
|
||||
"emitted payload is the wrapped mint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drives a hand-built `cross_zone_inbox::Dispatch` (as the watcher would inject)
|
||||
/// and asserts it chains into `wrapped_token::Mint`, crediting the recipient.
|
||||
#[test]
|
||||
fn inbox_dispatch_mints_wrapped_token() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("dispatch must validate and execute");
|
||||
let minted = wrapped_token_core::read_balance(
|
||||
&diff.public_diff()[&holding_id].data.clone().into_inner(),
|
||||
);
|
||||
assert_eq!(
|
||||
minted, LOCK_AMOUNT,
|
||||
"recipient holding minted the locked amount"
|
||||
);
|
||||
}
|
||||
|
||||
/// A dispatch whose message key is already in the seen-shard is an idempotent
|
||||
/// no-op: the inbox makes no chained call, so the wrapped token is not minted a
|
||||
/// second time. This is the bridge's replay defense.
|
||||
#[test]
|
||||
fn mint_replay_rejected() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
let src_tx_index = 0;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
// Seed the seen-shard as already containing this message's key, so the inbox
|
||||
// takes the replay no-op branch. The shard is inbox-owned (claimed on a prior
|
||||
// delivery), so the guest leaves it untouched.
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let mut shard = SeenShard::default();
|
||||
shard.insert(message_key(&src_zone, src_block_id, src_tx_index));
|
||||
state.insert_genesis_account(
|
||||
seen_id,
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: shard
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("shard fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("a replayed dispatch is a valid no-op, not an error");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// No mint: the holding is never credited on replay.
|
||||
let minted = public_diff.get(&holding_id).map_or(0, |account| {
|
||||
wrapped_token_core::read_balance(&account.data.clone().into_inner())
|
||||
});
|
||||
assert_eq!(minted, 0, "a replayed message must not mint again");
|
||||
|
||||
// The seen-shard is untouched by the no-op.
|
||||
if let Some(seen) = public_diff.get(&seen_id) {
|
||||
let shard_after =
|
||||
SeenShard::from_bytes(&seen.data.clone().into_inner()).expect("seen shard decodes");
|
||||
assert_eq!(shard_after, shard, "replay must not modify the seen-shard");
|
||||
}
|
||||
}
|
||||
162
integration_tests/tests/cross_zone_verified.rs
Normal file
162
integration_tests/tests/cross_zone_verified.rs
Normal file
@ -0,0 +1,162 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Cross-zone round trip with the indexer in the loop (Option B). A ping on zone
|
||||
//! A is delivered to zone B, and zone B's indexer independently re-derives the
|
||||
//! injected dispatch from zone A's finalized blocks before applying it. The
|
||||
//! payload landing in the indexer's state proves verification passed; a forgery
|
||||
//! would have halted the indexer instead.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{setup_bedrock_node, setup_indexer, setup_sequencer},
|
||||
};
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-verified-zone";
|
||||
|
||||
#[test]
|
||||
async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// Zone A: source. Zone B: destination, with the watcher on its sequencer and
|
||||
// the verifier on its indexer.
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (_seq_b, _seq_b_home) = setup_sequencer(
|
||||
partial,
|
||||
bedrock_addr,
|
||||
vec![],
|
||||
channel_b,
|
||||
Some(cross_zone.clone()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Submit the ping on zone A, addressed to ping_receiver on zone B.
|
||||
let ping = build_ping_tx(zone_b, receiver_id);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(ping)
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
|
||||
// Wait until zone B's indexer records the delivered payload. The indexer only
|
||||
// applies the dispatch after re-deriving and verifying it.
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, idx_b.addr())
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let delivered = wait_for_indexer_delivery(&indexer, record_id).await?;
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B's indexer must record the verified cross-zone payload"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
fn sequencer_client(addr: SocketAddr) -> Result<SequencerClient> {
|
||||
let url = config::addr_to_url(config::UrlProtocol::Http, addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
SequencerClientBuilder::default()
|
||||
.build(url)
|
||||
.context("Failed to build sequencer client")
|
||||
}
|
||||
|
||||
/// Polls zone B's indexer until the ping record PDA holds a payload.
|
||||
async fn wait_for_indexer_delivery(
|
||||
indexer: &IndexerClient,
|
||||
record_id: AccountId,
|
||||
) -> Result<Vec<u8>> {
|
||||
let account_id = indexer_service_protocol::AccountId {
|
||||
value: record_id.into_value(),
|
||||
};
|
||||
let wait = async {
|
||||
loop {
|
||||
let account =
|
||||
indexer_service_rpc::RpcClient::get_account(&**indexer, account_id).await?;
|
||||
let data = account.data.0;
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B's indexer did not record the payload in time")?
|
||||
}
|
||||
@ -50,8 +50,12 @@ pub fn setup_indexer_ffi(bedrock_addr: SocketAddr) -> Result<(IndexerServiceFFI,
|
||||
temp_indexer_dir.path().display()
|
||||
);
|
||||
|
||||
let indexer_config = integration_tests::config::indexer_config(bedrock_addr)
|
||||
.context("Failed to create Indexer config")?;
|
||||
let indexer_config = integration_tests::config::indexer_config(
|
||||
bedrock_addr,
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
None,
|
||||
)
|
||||
.context("Failed to create Indexer config")?;
|
||||
|
||||
let config_json = serde_json::to_vec(&indexer_config)?;
|
||||
let config_path = temp_indexer_dir.path().join("indexer_config.json");
|
||||
|
||||
54
integration_tests/tests/indexer_stall.rs
Normal file
54
integration_tests/tests/indexer_stall.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexer_service_protocol::IndexerSyncState;
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{TestContext, wait_for_indexer_to_catch_up};
|
||||
use log::info;
|
||||
|
||||
const CAUGHT_UP_STATUS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Test that the indexer status RPC reports caught-up with no stall after a clean run.
|
||||
///
|
||||
/// The sequencer keeps producing blocks while we assert, so the status is polled until a
|
||||
/// `CaughtUp` snapshot is observed and the indexed tip is checked as a lower bound.
|
||||
///
|
||||
/// TODO: Integration-level park testing (publishing a bad block to force a stall) is a follow-up
|
||||
/// needing fault injection support in the test harness.
|
||||
#[tokio::test]
|
||||
async fn indexer_status_rpc_reports_caught_up_with_no_stall() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let indexer_tip = wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
|
||||
let status = tokio::time::timeout(CAUGHT_UP_STATUS_TIMEOUT, async {
|
||||
loop {
|
||||
let status = ctx.indexer_client().get_status().await?;
|
||||
if status.state == IndexerSyncState::CaughtUp {
|
||||
return anyhow::Ok(status);
|
||||
}
|
||||
info!("Waiting for caught-up indexer status, got {status:?}");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Timed out waiting for indexer status to report caught-up")??;
|
||||
|
||||
assert!(
|
||||
status.stall_reason.is_none(),
|
||||
"indexer should have no stall reason after a clean run, got {status:?}"
|
||||
);
|
||||
// test for >= here because the sequencer keeps producing blocks while we assert,
|
||||
// so the indexed tip may be ahead of the tip we observed when we waited for caught-up.
|
||||
assert!(
|
||||
status.indexed_block_id >= Some(indexer_tip),
|
||||
"status indexed_block_id should be at least the caught-up tip {indexer_tip}, got {status:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,51 +1,36 @@
|
||||
#![expect(
|
||||
clippy::shadow_unrelated,
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::Result;
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
|
||||
verify_commitment_is_in_state, wait_for_indexer_to_catch_up,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance,
|
||||
assert_private_commitment_in_state, get_account, private_mention, public_mention, send,
|
||||
wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use lee::AccountId;
|
||||
use log::info;
|
||||
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
|
||||
|
||||
#[tokio::test]
|
||||
async fn indexer_state_consistency() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let (acc0, acc1) = (
|
||||
ctx.existing_public_accounts()[0],
|
||||
ctx.existing_public_accounts()[1],
|
||||
);
|
||||
send(&mut ctx, public_mention(acc0), public_mention(acc1), 100).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 = 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?;
|
||||
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?;
|
||||
|
||||
info!("Balance of sender: {acc_1_balance:#?}");
|
||||
info!("Balance of receiver: {acc_2_balance:#?}");
|
||||
@ -56,32 +41,13 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
let to: AccountId = ctx.existing_private_accounts()[1];
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(private_mention(to)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(&mut ctx, private_mention(from), private_mention(to), 100).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);
|
||||
assert_private_commitment_in_state(&ctx, from, "sender").await?;
|
||||
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
|
||||
|
||||
info!("Successfully transferred privately to owned account");
|
||||
|
||||
@ -100,16 +66,8 @@ async fn indexer_state_consistency() -> Result<()> {
|
||||
.unwrap();
|
||||
|
||||
info!("Checking correct state transition");
|
||||
let acc1_seq_state = sequencer_service_rpc::RpcClient::get_account(
|
||||
ctx.sequencer_client(),
|
||||
ctx.existing_public_accounts()[0],
|
||||
)
|
||||
.await?;
|
||||
let acc2_seq_state = sequencer_service_rpc::RpcClient::get_account(
|
||||
ctx.sequencer_client(),
|
||||
ctx.existing_public_accounts()[1],
|
||||
)
|
||||
.await?;
|
||||
let acc1_seq_state = get_account(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
let acc2_seq_state = get_account(&ctx, ctx.existing_public_accounts()[1]).await?;
|
||||
|
||||
assert_eq!(acc1_ind_state, acc1_seq_state.into());
|
||||
assert_eq!(acc2_ind_state, acc2_seq_state.into());
|
||||
|
||||
@ -9,12 +9,13 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention, wait_for_indexer_to_catch_up,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, public_mention,
|
||||
send, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use log::info;
|
||||
use wallet::{
|
||||
account::Label,
|
||||
cli::{CliAccountMention, Command, programs::native_token_transfer::AuthTransferSubcommand},
|
||||
cli::{CliAccountMention, Command},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@ -38,31 +39,19 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?;
|
||||
|
||||
// Send using labels instead of account IDs
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: CliAccountMention::Label(from_label),
|
||||
to: Some(CliAccountMention::Label(to_label)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
CliAccountMention::Label(from_label),
|
||||
CliAccountMention::Label(to_label),
|
||||
100,
|
||||
)
|
||||
.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?;
|
||||
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?;
|
||||
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
@ -75,11 +64,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
|
||||
.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?;
|
||||
let acc1_seq_state = get_account(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
|
||||
assert_eq!(acc1_ind_state, acc1_seq_state.into());
|
||||
|
||||
|
||||
@ -8,8 +8,9 @@ use std::{str::FromStr as _, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention,
|
||||
public_mention, verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, assert_public_account_restored,
|
||||
fetch_privacy_preserving_tx, new_account, private_mention, public_mention,
|
||||
restored_private_account, send, verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee::AccountId;
|
||||
@ -17,8 +18,7 @@ use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
Command, SubcommandReturnValue, account::AccountSubcommand,
|
||||
programs::native_token_transfer::AuthTransferSubcommand,
|
||||
};
|
||||
|
||||
@ -28,35 +28,12 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
|
||||
// Create a new private account
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
}));
|
||||
|
||||
// Key Tree shift — create 3 accounts to advance the key index
|
||||
for _ in 0..3 {
|
||||
// Key Tree shift
|
||||
// This way we have account with child index > 0.
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
new_account(&mut ctx, true, None).await?;
|
||||
}
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id,
|
||||
} = sub_ret
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let to_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Get the keys for the newly created account
|
||||
let to_account = ctx
|
||||
@ -80,8 +57,8 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
@ -118,107 +95,47 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
|
||||
// Create first private account at root
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: Some(ChainIndex::root()),
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id1,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
// Create private accounts at root and /0
|
||||
let to_account_id1 = new_account(&mut ctx, true, Some(ChainIndex::root())).await?;
|
||||
let to_account_id2 = new_account(&mut ctx, true, Some(ChainIndex::from_str("/0")?)).await?;
|
||||
|
||||
// Create second private account at /0
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: Some(ChainIndex::from_str("/0")?),
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id2,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
|
||||
// Send to first private account
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(private_mention(to_account_id1)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 100,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Send to second private account
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(private_mention(to_account_id2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 101,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
// Send to both private accounts
|
||||
send(
|
||||
&mut ctx,
|
||||
private_mention(from),
|
||||
private_mention(to_account_id1),
|
||||
100,
|
||||
)
|
||||
.await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
private_mention(from),
|
||||
private_mention(to_account_id2),
|
||||
101,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let from: AccountId = ctx.existing_public_accounts()[0];
|
||||
|
||||
// Create first public account at root
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: Some(ChainIndex::root()),
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id3,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
// Create public accounts at root and /0
|
||||
let to_account_id3 = new_account(&mut ctx, false, Some(ChainIndex::root())).await?;
|
||||
let to_account_id4 = new_account(&mut ctx, false, Some(ChainIndex::from_str("/0")?)).await?;
|
||||
|
||||
// Create second public account at /0
|
||||
let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: Some(ChainIndex::from_str("/0")?),
|
||||
label: None,
|
||||
}));
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: to_account_id4,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
|
||||
// Send to first public account
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(from),
|
||||
to: Some(public_mention(to_account_id3)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 102,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
// Send to second public account
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(from),
|
||||
to: Some(public_mention(to_account_id4)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 103,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
// Send to both public accounts
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(from),
|
||||
public_mention(to_account_id3),
|
||||
102,
|
||||
)
|
||||
.await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(from),
|
||||
public_mention(to_account_id4),
|
||||
103,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!("Preparation complete, performing keys restoration");
|
||||
|
||||
@ -226,34 +143,12 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
wallet::cli::execute_keys_restoration(ctx.wallet_mut(), 10).await?;
|
||||
|
||||
// Verify restored private accounts
|
||||
let acc1 = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.private_account(to_account_id1)
|
||||
.expect("Acc 1 should be restored");
|
||||
|
||||
let acc2 = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.private_account(to_account_id2)
|
||||
.expect("Acc 2 should be restored");
|
||||
let acc1 = restored_private_account(&ctx, to_account_id1, "Acc 1");
|
||||
let acc2 = restored_private_account(&ctx, to_account_id2, "Acc 2");
|
||||
|
||||
// Verify restored public accounts
|
||||
let _acc3 = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(to_account_id3)
|
||||
.expect("Acc 3 should be restored");
|
||||
|
||||
let _acc4 = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(to_account_id4)
|
||||
.expect("Acc 4 should be restored");
|
||||
assert_public_account_restored(&ctx, to_account_id3, "Acc 3");
|
||||
assert_public_account_restored(&ctx, to_account_id4, "Acc 4");
|
||||
|
||||
assert_eq!(
|
||||
acc1.account.program_owner,
|
||||
@ -270,27 +165,20 @@ async fn restore_keys_from_seed() -> Result<()> {
|
||||
info!("Tree checks passed, testing restored accounts can transact");
|
||||
|
||||
// Test that restored accounts can send transactions
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(to_account_id1),
|
||||
to: Some(private_mention(to_account_id2)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 10,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(to_account_id3),
|
||||
to: Some(public_mention(to_account_id4)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: 11,
|
||||
});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
private_mention(to_account_id1),
|
||||
private_mention(to_account_id2),
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
send(
|
||||
&mut ctx,
|
||||
public_mention(to_account_id3),
|
||||
public_mention(to_account_id4),
|
||||
11,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
|
||||
@ -8,15 +8,13 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
|
||||
verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, new_account, private_mention,
|
||||
public_mention, sync_private, verify_commitment_is_in_state, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
programs::{
|
||||
native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand,
|
||||
},
|
||||
@ -26,25 +24,9 @@ use wallet::cli::{
|
||||
async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
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: winner_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let winner_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
let pinata_balance_pre = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -64,10 +46,7 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
|
||||
"Expected init guidance, got: {err}",
|
||||
);
|
||||
|
||||
let pinata_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre);
|
||||
|
||||
@ -78,25 +57,9 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()>
|
||||
async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: winner_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let winner_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
let pinata_balance_pre = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -116,10 +79,7 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()
|
||||
"Expected init guidance, got: {err}",
|
||||
);
|
||||
|
||||
let pinata_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre);
|
||||
|
||||
@ -135,10 +95,7 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
|
||||
to: public_mention(ctx.existing_public_accounts()[0]),
|
||||
});
|
||||
|
||||
let pinata_balance_pre = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
@ -146,15 +103,9 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
info!("Checking correct balance move");
|
||||
let pinata_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let winner_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(ctx.existing_public_accounts()[0])
|
||||
.await?;
|
||||
let winner_balance_post = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize);
|
||||
assert_eq!(winner_balance_post, 10000 + pinata_prize);
|
||||
@ -164,6 +115,41 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn claim_pinata_indexer_keeps_up() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to: public_mention(ctx.existing_public_accounts()[0]),
|
||||
});
|
||||
|
||||
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!("Waiting for indexer to parse blocks");
|
||||
wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
|
||||
let winner_ind_state = indexer_service_rpc::RpcClient::get_account(
|
||||
&**ctx.indexer_client(),
|
||||
ctx.existing_public_accounts()[0].into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let winner_seq_state = sequencer_service_rpc::RpcClient::get_account(
|
||||
ctx.sequencer_client(),
|
||||
ctx.existing_public_accounts()[0],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(winner_ind_state, winner_seq_state.into());
|
||||
|
||||
info!("Indexer correctly indexed the pinata claim");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
@ -173,22 +159,18 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
||||
to: private_mention(ctx.existing_private_accounts()[0]),
|
||||
});
|
||||
|
||||
let pinata_balance_pre = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = result else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash: _ } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
info!("Syncing private accounts");
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
let new_commitment = ctx
|
||||
.wallet()
|
||||
@ -196,10 +178,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
||||
.context("Failed to get private account commitment")?;
|
||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||
|
||||
let pinata_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize);
|
||||
|
||||
@ -215,20 +194,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
||||
let pinata_prize = 150;
|
||||
|
||||
// Create new private account
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: winner_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let winner_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Initialize account under auth transfer program
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
@ -250,10 +216,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
||||
to: private_mention(winner_account_id),
|
||||
});
|
||||
|
||||
let pinata_balance_pre = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
@ -266,10 +229,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> {
|
||||
.context("Failed to get private account commitment")?;
|
||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||
|
||||
let pinata_balance_post = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize);
|
||||
|
||||
|
||||
@ -9,9 +9,8 @@ use anyhow::{Context as _, Result};
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, sync_private, verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use lee::{
|
||||
AccountId, PrivacyPreservingTransaction, ProgramId,
|
||||
privacy_preserving_transaction::{
|
||||
@ -22,7 +21,7 @@ use lee::{
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::PdaSeed,
|
||||
@ -30,10 +29,7 @@ use lee_core::{
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
AccountIdentity, WalletCore,
|
||||
cli::{Command, account::AccountSubcommand},
|
||||
};
|
||||
use wallet::{AccountIdentity, WalletCore};
|
||||
|
||||
/// Funds a private PDA by calling `auth_transfer` directly.
|
||||
#[expect(
|
||||
@ -51,7 +47,8 @@ async fn fund_private_pda(
|
||||
amount: u128,
|
||||
auth_transfer: &ProgramWithDependencies,
|
||||
) -> Result<()> {
|
||||
let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
|
||||
let pda_account_id =
|
||||
AccountId::for_private_pda(&authority_program_id, &seed, &npk, &vpk, identifier);
|
||||
let sender_account = wallet
|
||||
.get_account_public(sender)
|
||||
.await
|
||||
@ -63,21 +60,17 @@ async fn fund_private_pda(
|
||||
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&vpk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender();
|
||||
let epk = eph_holder.ephemeral_public_key().clone();
|
||||
|
||||
let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.context("failed to serialize auth_transfer instruction")?;
|
||||
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
vpk,
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
},
|
||||
];
|
||||
@ -175,8 +168,8 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
let spend_program =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 1);
|
||||
|
||||
// Use two different public senders to avoid nonce conflicts between the back-to-back txs.
|
||||
let senders = ctx.existing_public_accounts();
|
||||
@ -217,11 +210,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync so alice's wallet discovers and stores both PDAs.
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// Both PDAs must be discoverable and have the correct balance.
|
||||
let pda_0_account = ctx
|
||||
@ -300,11 +289,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
info!("Waiting for block");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::SyncPrivate {}),
|
||||
)
|
||||
.await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// After spending, PDAs should have the remaining balance.
|
||||
let pda_0_spent = ctx
|
||||
|
||||
@ -7,14 +7,11 @@ use std::{io::Write as _, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext};
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
};
|
||||
use wallet::{cli::Command, config::WalletConfigOverrides};
|
||||
|
||||
#[test]
|
||||
async fn deploy_and_execute_program() -> Result<()> {
|
||||
@ -32,20 +29,7 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
|
||||
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 SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
panic!("Expected RegisterAccount return value");
|
||||
};
|
||||
let account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?;
|
||||
let private_key = ctx
|
||||
@ -66,7 +50,7 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
// block
|
||||
tokio::time::sleep(Duration::from_secs(2 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let post_state_account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
let post_state_account = get_account(&ctx, account_id).await?;
|
||||
|
||||
let expected_data: &[u8] = &[];
|
||||
assert_eq!(post_state_account.program_owner, claimer.id());
|
||||
@ -78,3 +62,37 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn deploy_invalid_program_fails() -> Result<()> {
|
||||
// An invalid program bytecode is rejected by the sequencer during block production, so the
|
||||
// deployment transaction is never included in a block. Shrink the wallet's polling window so
|
||||
// the command gives up quickly instead of waiting for the full default timeout.
|
||||
let mut ctx = TestContext::builder()
|
||||
.with_wallet_config_overrides(WalletConfigOverrides {
|
||||
seq_poll_timeout: Some(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)),
|
||||
seq_tx_poll_max_blocks: Some(5),
|
||||
seq_poll_max_retries: Some(2),
|
||||
..WalletConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut tempfile = tempfile::NamedTempFile::new()?;
|
||||
tempfile.write_all(b"this is not a valid program binary")?;
|
||||
|
||||
let command = Command::DeployProgram {
|
||||
binary_filepath: tempfile.path().to_owned(),
|
||||
};
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Deploying an invalid program should fail, but got: {result:?}"
|
||||
);
|
||||
|
||||
info!("Deploying an invalid program failed as expected");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, sync_private,
|
||||
};
|
||||
use log::info;
|
||||
use tokio::test;
|
||||
@ -197,8 +197,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync private accounts
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate);
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// Fund from a public account
|
||||
let from_public = ctx.existing_public_accounts()[0];
|
||||
@ -216,8 +215,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync private accounts
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate);
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// Verify the shared account was updated
|
||||
let entry = ctx
|
||||
|
||||
@ -8,12 +8,11 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention,
|
||||
verify_commitment_is_in_state,
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account, private_mention,
|
||||
public_mention, sync_private, verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -30,52 +29,13 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create new account for the token definition
|
||||
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");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for the token supply holder
|
||||
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: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for receiving a token transaction
|
||||
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");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new token
|
||||
let name = "A NAME".to_owned();
|
||||
@ -92,10 +52,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the status of the token definition account
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(definition_acc.program_owner, programs::token().id());
|
||||
@ -109,10 +66,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
);
|
||||
|
||||
// Check the status of the token holding account with the total supply
|
||||
let supply_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(supply_account_id)
|
||||
.await?;
|
||||
let supply_acc = get_account(&ctx, supply_account_id).await?;
|
||||
|
||||
// The account must be owned by the token program
|
||||
assert_eq!(supply_acc.program_owner, programs::token().id());
|
||||
@ -143,10 +97,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the status of the supply account after transfer
|
||||
let supply_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(supply_account_id)
|
||||
.await?;
|
||||
let supply_acc = get_account(&ctx, supply_account_id).await?;
|
||||
assert_eq!(supply_acc.program_owner, programs::token().id());
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
assert_eq!(
|
||||
@ -158,10 +109,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
);
|
||||
|
||||
// Check the status of the recipient account after transfer
|
||||
let recipient_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id)
|
||||
.await?;
|
||||
let recipient_acc = get_account(&ctx, recipient_account_id).await?;
|
||||
assert_eq!(recipient_acc.program_owner, programs::token().id());
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
assert_eq!(
|
||||
@ -186,10 +134,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the status of the token definition account after burn
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -202,10 +147,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
);
|
||||
|
||||
// Check the status of the recipient account after burn
|
||||
let recipient_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id)
|
||||
.await?;
|
||||
let recipient_acc = get_account(&ctx, recipient_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -234,10 +176,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the status of the token definition account after mint
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -250,10 +189,7 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
);
|
||||
|
||||
// Check the status of the recipient account after mint
|
||||
let recipient_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id)
|
||||
.await?;
|
||||
let recipient_acc = get_account(&ctx, recipient_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -274,52 +210,13 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create new account for the token definition (public)
|
||||
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");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new account for the token supply holder (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create new account for receiving a token transaction (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create new token
|
||||
let name = "A NAME".to_owned();
|
||||
@ -337,10 +234,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the status of the token definition account
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(definition_acc.program_owner, programs::token().id());
|
||||
@ -402,10 +296,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Check the token definition account after burn
|
||||
let definition_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(definition_account_id)
|
||||
.await?;
|
||||
let definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -448,36 +339,10 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create token definition account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: Some(ChainIndex::root()),
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, true, Some(ChainIndex::root())).await?;
|
||||
|
||||
// Create supply account (public)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Public {
|
||||
cci: Some(ChainIndex::root()),
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, false, Some(ChainIndex::root())).await?;
|
||||
|
||||
// Create token with private definition
|
||||
let name = "A NAME".to_owned();
|
||||
@ -502,10 +367,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await);
|
||||
|
||||
// Verify supply account
|
||||
let supply_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(supply_account_id)
|
||||
.await?;
|
||||
let supply_acc = get_account(&ctx, supply_account_id).await?;
|
||||
|
||||
assert_eq!(supply_acc.program_owner, programs::token().id());
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
@ -518,36 +380,10 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
);
|
||||
|
||||
// Create private recipient account
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id_private,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id_private = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create public 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_public,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id_public = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Mint to public account
|
||||
let mint_amount_public = 10;
|
||||
@ -583,10 +419,7 @@ async fn create_token_with_private_definition() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify public recipient received tokens
|
||||
let recipient_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id_public)
|
||||
.await?;
|
||||
let recipient_acc = get_account(&ctx, recipient_account_id_public).await?;
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
|
||||
assert_eq!(
|
||||
@ -646,36 +479,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create token definition account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create supply account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create token with both private definition and supply
|
||||
let name = "A NAME".to_owned();
|
||||
@ -722,20 +529,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> {
|
||||
);
|
||||
|
||||
// Create recipient account
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Transfer tokens
|
||||
let transfer_amount = 7;
|
||||
@ -804,52 +598,13 @@ async fn shielded_token_transfer() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create token definition account (public)
|
||||
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");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create supply account (public)
|
||||
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: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create recipient account (private) for shielded transfer
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create token
|
||||
let name = "A NAME".to_owned();
|
||||
@ -884,10 +639,7 @@ async fn shielded_token_transfer() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify supply account balance
|
||||
let supply_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(supply_account_id)
|
||||
.await?;
|
||||
let supply_acc = get_account(&ctx, supply_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
assert_eq!(
|
||||
token_holding,
|
||||
@ -928,52 +680,13 @@ async fn deshielded_token_transfer() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create token definition account (public)
|
||||
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");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create supply account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create recipient account (public) for deshielded transfer
|
||||
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");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create token with private supply
|
||||
let name = "A NAME".to_owned();
|
||||
@ -1029,10 +742,7 @@ async fn deshielded_token_transfer() -> Result<()> {
|
||||
);
|
||||
|
||||
// Verify recipient balance
|
||||
let recipient_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(recipient_account_id)
|
||||
.await?;
|
||||
let recipient_acc = get_account(&ctx, recipient_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
assert_eq!(
|
||||
token_holding,
|
||||
@ -1052,36 +762,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
// Create token definition account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: definition_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create supply account (private)
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: supply_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let supply_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Create token
|
||||
let name = "A NAME".to_owned();
|
||||
@ -1099,20 +783,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Create new private account for claiming path
|
||||
let result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Account(AccountSubcommand::New(NewSubcommand::Private {
|
||||
cci: None,
|
||||
label: None,
|
||||
})),
|
||||
)
|
||||
.await?;
|
||||
let SubcommandReturnValue::RegisterAccount {
|
||||
account_id: recipient_account_id,
|
||||
} = result
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
// Get keys for foreign mint (claiming path)
|
||||
let holder = ctx
|
||||
@ -1143,8 +814,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Sync to claim the account
|
||||
let command = Command::Account(AccountSubcommand::SyncPrivate {});
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
sync_private(&mut ctx).await?;
|
||||
|
||||
// Verify commitment exists
|
||||
let recipient_commitment = ctx
|
||||
@ -1224,10 +894,7 @@ async fn create_token_using_labels() -> Result<()> {
|
||||
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 definition_acc = get_account(&ctx, definition_account_id).await?;
|
||||
let token_definition = TokenDefinition::try_from(&definition_acc.data)?;
|
||||
|
||||
assert_eq!(definition_acc.program_owner, programs::token().id());
|
||||
@ -1240,10 +907,7 @@ async fn create_token_using_labels() -> Result<()> {
|
||||
}
|
||||
);
|
||||
|
||||
let supply_acc = ctx
|
||||
.sequencer_client()
|
||||
.get_account(supply_account_id)
|
||||
.await?;
|
||||
let supply_acc = get_account(&ctx, supply_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
assert_eq!(
|
||||
token_holding,
|
||||
@ -1263,20 +927,7 @@ 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");
|
||||
};
|
||||
let definition_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create supply account with a label
|
||||
let supply_label = Label::new("token-supply-sender");
|
||||
@ -1296,20 +947,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
|
||||
};
|
||||
|
||||
// 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");
|
||||
};
|
||||
let recipient_account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create token
|
||||
let total_supply = 50;
|
||||
@ -1340,10 +978,7 @@ async fn transfer_token_using_from_label() -> Result<()> {
|
||||
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 recipient_acc = get_account(&ctx, recipient_account_id).await?;
|
||||
let token_holding = TokenHolding::try_from(&recipient_acc.data)?;
|
||||
assert_eq!(
|
||||
token_holding,
|
||||
|
||||
@ -15,7 +15,6 @@ use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TestContext, config::SequencerPartialConfig};
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use lee::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
|
||||
privacy_preserving_transaction::{self as pptx, circuit},
|
||||
@ -23,7 +22,7 @@ use lee::{
|
||||
public_transaction as putx,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
account::{AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
@ -266,7 +265,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_npk, 0),
|
||||
AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0),
|
||||
);
|
||||
let recipient_nsk = [2; 32];
|
||||
let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]);
|
||||
@ -274,17 +273,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&recipient_npk, 0),
|
||||
AccountId::for_regular_private_account(&recipient_npk, &recipient_vpk, 0),
|
||||
);
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk);
|
||||
let sender_ss = eph_holder_from.calculate_shared_secret_sender();
|
||||
let sender_epk = eph_holder_from.ephemeral_public_key().clone();
|
||||
|
||||
let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk);
|
||||
let recipient_ss = eph_holder_to.calculate_shared_secret_sender();
|
||||
let recipient_epk = eph_holder_to.ephemeral_public_key().clone();
|
||||
|
||||
let balance_to_move: u128 = 1;
|
||||
let proof: MembershipProof = (
|
||||
1,
|
||||
@ -301,19 +292,18 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: sender_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
|
||||
ssk: sender_ss,
|
||||
vpk: sender_vpk,
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_nsk,
|
||||
membership_proof: proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: recipient_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
||||
vpk: recipient_vpk,
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_npk,
|
||||
ssk: recipient_ss,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
|
||||
110
integration_tests/tests/two_zone.rs
Normal file
110
integration_tests/tests/two_zone.rs
Normal file
@ -0,0 +1,110 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Two zones (sequencer + indexer each, on separate channels) sharing one
|
||||
//! Bedrock node, each producing and finalizing blocks independently.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{setup_bedrock_node, setup_indexer, setup_sequencer},
|
||||
};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
const ZONE_LIVE_TIMEOUT: Duration = Duration::from_secs(360);
|
||||
|
||||
// Genesis is block 1, so reaching 2 means a block was produced past it.
|
||||
const MIN_BLOCK_ID: u64 = 2;
|
||||
|
||||
#[test]
|
||||
async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
|
||||
// Empty genesis is enough: the clock transaction drives block production.
|
||||
let (seq_a, _seq_a_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (seq_b, _seq_b_home) = setup_sequencer(partial, bedrock_addr, vec![], channel_b, None)
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None)
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
let (height_a, height_b) = tokio::try_join!(
|
||||
wait_until_zone_live("A", seq_a.addr(), idx_a.addr()),
|
||||
wait_until_zone_live("B", seq_b.addr(), idx_b.addr()),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
height_a >= MIN_BLOCK_ID,
|
||||
"Zone A indexer only reached block {height_a}, expected >= {MIN_BLOCK_ID}"
|
||||
);
|
||||
assert!(
|
||||
height_b >= MIN_BLOCK_ID,
|
||||
"Zone B indexer only reached block {height_b}, expected >= {MIN_BLOCK_ID}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for the sequencer to produce past genesis and the indexer to finalize up
|
||||
/// to it. Returns the indexer's finalized block id.
|
||||
async fn wait_until_zone_live(
|
||||
label: &str,
|
||||
sequencer_addr: SocketAddr,
|
||||
indexer_addr: SocketAddr,
|
||||
) -> Result<u64> {
|
||||
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
let sequencer = SequencerClientBuilder::default()
|
||||
.build(sequencer_url)
|
||||
.context("Failed to build sequencer client")?;
|
||||
|
||||
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_addr)
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let wait = async {
|
||||
loop {
|
||||
if sequencer.get_last_block_id().await? >= MIN_BLOCK_ID {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
let target = sequencer.get_last_block_id().await?;
|
||||
loop {
|
||||
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
|
||||
if finalized >= target {
|
||||
log::info!(
|
||||
"Zone {label} live: sequencer at {target}, indexer finalized {finalized}"
|
||||
);
|
||||
return Ok::<u64, anyhow::Error>(finalized);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::time::timeout(ZONE_LIVE_TIMEOUT, wait)
|
||||
.await
|
||||
.with_context(|| format!("Zone {label} did not become live within {ZONE_LIVE_TIMEOUT:?}"))?
|
||||
}
|
||||
@ -30,7 +30,7 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
|
||||
let transfer_result = wallet::cli::execute_subcommand(
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Transfer {
|
||||
from: public_mention(sender),
|
||||
@ -39,10 +39,6 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(transfer_result, SubcommandReturnValue::Empty),
|
||||
"Expected Empty return value for public vault transfer"
|
||||
);
|
||||
|
||||
let sender_balance_after_transfer = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let recipient_balance_after_transfer = ctx
|
||||
@ -64,7 +60,7 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
recipient_vault_balance_before + amount
|
||||
);
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Claim {
|
||||
account_id: public_mention(recipient),
|
||||
@ -72,10 +68,6 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(claim_result, SubcommandReturnValue::Empty),
|
||||
"Expected Empty return value for public vault claim"
|
||||
);
|
||||
|
||||
let sender_balance_after_claim = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let recipient_balance_after_claim = ctx
|
||||
@ -138,9 +130,9 @@ async fn private_transfer_and_private_claim() -> Result<()> {
|
||||
assert!(
|
||||
matches!(
|
||||
transfer_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
SubcommandReturnValue::TransactionExecuted { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault transfer"
|
||||
"Expected TransactionExecuted return value for private vault transfer"
|
||||
);
|
||||
|
||||
let sender_balance_after_transfer = ctx
|
||||
@ -179,9 +171,9 @@ async fn private_transfer_and_private_claim() -> Result<()> {
|
||||
assert!(
|
||||
matches!(
|
||||
claim_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
SubcommandReturnValue::TransactionExecuted { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault claim"
|
||||
"Expected TransactionExecuted return value for private vault claim"
|
||||
);
|
||||
|
||||
let sender_balance_after_claim = ctx
|
||||
|
||||
@ -1012,7 +1012,11 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
|
||||
let (to, to_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
|
||||
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&out_keys.npk(),
|
||||
&out_keys.vpk().unwrap(),
|
||||
0_u128,
|
||||
);
|
||||
let to: FfiBytes32 = account_id.into();
|
||||
(to, out_keys)
|
||||
};
|
||||
@ -1155,7 +1159,11 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
|
||||
let (to, to_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
|
||||
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&out_keys.npk(),
|
||||
&out_keys.vpk().unwrap(),
|
||||
0_u128,
|
||||
);
|
||||
let to: FfiBytes32 = account_id.into();
|
||||
(to, out_keys)
|
||||
};
|
||||
@ -1236,7 +1244,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
let (private_account_id_1, private_account_1_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
|
||||
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&out_keys.npk(),
|
||||
&out_keys.vpk().unwrap(),
|
||||
0_u128,
|
||||
);
|
||||
let to: FfiBytes32 = account_id.into();
|
||||
(to, out_keys)
|
||||
};
|
||||
@ -1244,7 +1256,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
let (private_account_id_2, private_account_2_keys) = unsafe {
|
||||
let mut out_keys = FfiPrivateAccountKeys::default();
|
||||
wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap();
|
||||
let account_id = lee::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128);
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&out_keys.npk(),
|
||||
&out_keys.vpk().unwrap(),
|
||||
0_u128,
|
||||
);
|
||||
let to: FfiBytes32 = account_id.into();
|
||||
(to, out_keys)
|
||||
};
|
||||
|
||||
@ -48,14 +48,3 @@ impl EphemeralKeyHolder {
|
||||
self.shared_secret
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates a fresh shared secret toward `vpk` and returns `(shared_secret, ciphertext)`.
|
||||
///
|
||||
/// Used when the local side is acting as an "ephemeral receiver" — i.e. generating a
|
||||
/// one-sided encryption that only the holder of the VSK can decrypt.
|
||||
#[must_use]
|
||||
pub fn produce_one_sided_shared_secret_receiver(
|
||||
vpk: &ViewingPublicKey,
|
||||
) -> (SharedSecretKey, EphemeralPublicKey) {
|
||||
SharedSecretKey::encapsulate(vpk)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use lee_core::{
|
||||
SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
Identifier, SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
@ -146,11 +146,28 @@ impl GroupKeyHolder {
|
||||
SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Derive keys for a shared regular account from its `identifier`.
|
||||
///
|
||||
/// Computes the derivation seed via the `SharedAccountTag` domain separator, then delegates
|
||||
/// to [`Self::derive_keys_for_shared_account`].
|
||||
#[must_use]
|
||||
pub fn derive_regular_shared_account_keys_from_identifier(
|
||||
&self,
|
||||
identifier: Identifier,
|
||||
) -> PrivateKeyHolder {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(identifier.to_le_bytes());
|
||||
let derivation_seed: [u8; 32] = hasher.finalize().into();
|
||||
self.derive_keys_for_shared_account(&derivation_seed)
|
||||
}
|
||||
|
||||
/// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`].
|
||||
///
|
||||
/// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt
|
||||
/// the payload. The returned bytes are
|
||||
/// `kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48)` = 1148 bytes.
|
||||
/// `kem_ciphertext (ML_KEM_768_CIPHERTEXT_LEN) || nonce (12) || ciphertext+tag (48)`.
|
||||
///
|
||||
/// Each call generates a fresh KEM encapsulation, so two seals of the same holder produce
|
||||
/// different ciphertexts.
|
||||
@ -170,7 +187,7 @@ impl GroupKeyHolder {
|
||||
.encrypt(&nonce, self.gms.as_ref())
|
||||
.expect("AES-GCM encryption should not fail with valid key/nonce");
|
||||
|
||||
let capacity = 1088_usize
|
||||
let capacity = ML_KEM_768_CIPHERTEXT_LEN
|
||||
.checked_add(12)
|
||||
.and_then(|n| n.checked_add(ciphertext.len()))
|
||||
.expect("seal capacity overflow");
|
||||
@ -186,21 +203,21 @@ impl GroupKeyHolder {
|
||||
/// Returns `Err` if the ciphertext is too short or the AES-GCM authentication tag
|
||||
/// doesn't verify (wrong key or tampered data).
|
||||
pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result<Self, SealError> {
|
||||
// kem_ciphertext (1088) + nonce (12) = header, then AES-GCM tag (16) minimum.
|
||||
const KEM_CT_LEN: usize = 1088;
|
||||
const HEADER_LEN: usize = KEM_CT_LEN + 12;
|
||||
// kem_ciphertext (ML_KEM_768_CIPHERTEXT_LEN) + nonce (12) = header, then AES-GCM tag (16)
|
||||
// minimum.
|
||||
const HEADER_LEN: usize = ML_KEM_768_CIPHERTEXT_LEN + 12;
|
||||
const MIN_LEN: usize = HEADER_LEN + 16;
|
||||
|
||||
if sealed.len() < MIN_LEN {
|
||||
return Err(SealError::TooShort);
|
||||
}
|
||||
|
||||
let kem_ct = EphemeralPublicKey(sealed[..KEM_CT_LEN].to_vec());
|
||||
let nonce = aes_gcm::Nonce::from_slice(&sealed[KEM_CT_LEN..HEADER_LEN]);
|
||||
let kem_ct = EphemeralPublicKey(sealed[..ML_KEM_768_CIPHERTEXT_LEN].to_vec());
|
||||
let nonce = aes_gcm::Nonce::from_slice(&sealed[ML_KEM_768_CIPHERTEXT_LEN..HEADER_LEN]);
|
||||
let ciphertext = &sealed[HEADER_LEN..];
|
||||
|
||||
let shared = SharedSecretKey::decapsulate(&kem_ct, &own_key.d, &own_key.z)
|
||||
.expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: KEM_CT_LEN guarantees exactly 1088 bytes");
|
||||
.expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: ML_KEM_768_CIPHERTEXT_LEN guarantees exactly 1088 bytes");
|
||||
let aes_key = Self::seal_kdf(&shared);
|
||||
let cipher = Aes256Gcm::new(&aes_key.into());
|
||||
|
||||
@ -334,10 +351,10 @@ mod tests {
|
||||
let program_id: ProgramId = [9; 8];
|
||||
|
||||
let holder = GroupKeyHolder::from_gms(gms);
|
||||
let npk = holder
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
let keys = holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
|
||||
|
||||
let expected_npk = NullifierPublicKey([
|
||||
136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
|
||||
@ -346,7 +363,7 @@ mod tests {
|
||||
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
|
||||
// We verify npk is pinned, and AccountId is deterministically derived from it.
|
||||
let expected_account_id =
|
||||
AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX);
|
||||
AccountId::for_private_pda(&program_id, &seed, &expected_npk, &vpk, u128::MAX);
|
||||
|
||||
assert_eq!(npk, expected_npk);
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
@ -543,13 +560,16 @@ mod tests {
|
||||
let bob_holder =
|
||||
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
|
||||
|
||||
let bob_npk = bob_holder
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed)
|
||||
.generate_nullifier_public_key();
|
||||
let bob_group_keys = bob_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed);
|
||||
let bob_npk = bob_group_keys.generate_nullifier_public_key();
|
||||
assert_eq!(alice_npk, bob_npk);
|
||||
|
||||
let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0);
|
||||
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0);
|
||||
let alice_vpk = alice_keys.generate_viewing_public_key();
|
||||
let bob_group_vpk = bob_group_keys.generate_viewing_public_key();
|
||||
let alice_account_id =
|
||||
AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, &alice_vpk, 0);
|
||||
let bob_account_id =
|
||||
AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, &bob_group_vpk, 0);
|
||||
assert_eq!(alice_account_id, bob_account_id);
|
||||
}
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ impl ChainIndex {
|
||||
.map(|item| Self(item.into_iter().copied().collect()))
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = Self> {
|
||||
fn collect_chain_ids_at_depth(depth: usize) -> Vec<Self> {
|
||||
let mut stack = vec![Self(vec![0; depth])];
|
||||
let mut cumulative_stack = vec![Self(vec![0; depth])];
|
||||
|
||||
@ -152,23 +152,18 @@ impl ChainIndex {
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_stack.into_iter().unique()
|
||||
cumulative_stack
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = Self> {
|
||||
Self::collect_chain_ids_at_depth(depth).into_iter().unique()
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator<Item = Self> {
|
||||
let mut stack = vec![Self(vec![0; depth])];
|
||||
let mut cumulative_stack = vec![Self(vec![0; depth])];
|
||||
|
||||
while let Some(top_id) = stack.pop() {
|
||||
if let Some(collapsed_id) = top_id.collapse_back() {
|
||||
for id in collapsed_id.shuffle_iter() {
|
||||
stack.push(id.clone());
|
||||
cumulative_stack.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cumulative_stack.into_iter().rev().unique()
|
||||
Self::collect_chain_ids_at_depth(depth)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.unique()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ use sha2::Digest as _;
|
||||
|
||||
use crate::key_management::{
|
||||
KeyChain,
|
||||
key_tree::traits::KeyTreeNode,
|
||||
key_tree::{split_hash, traits::KeyTreeNode},
|
||||
secret_holders::{PrivateKeyHolder, SecretSpendingKey},
|
||||
};
|
||||
|
||||
@ -23,38 +23,11 @@ impl ChildKeysPrivate {
|
||||
#[must_use]
|
||||
pub fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, b"LEE_master_priv");
|
||||
let (first, ccc) = split_hash(&hash_value);
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let ssk = SecretSpendingKey(first);
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key(None);
|
||||
let vsk = ssk.generate_viewing_secret_seed_key(None);
|
||||
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from(&vsk);
|
||||
|
||||
Self {
|
||||
value: (
|
||||
KeyChain {
|
||||
secret_spending_key: ssk,
|
||||
nullifier_public_key: npk,
|
||||
viewing_public_key: vpk,
|
||||
private_key_holder: PrivateKeyHolder {
|
||||
nullifier_secret_key: nsk,
|
||||
viewing_secret_key: vsk,
|
||||
},
|
||||
},
|
||||
BTreeMap::from_iter([(PrivateAccountKind::Regular(0), lee::Account::default())]),
|
||||
),
|
||||
ccc,
|
||||
cci: None,
|
||||
}
|
||||
Self::from_ssk_and_ccc(ssk, ccc, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -77,18 +50,16 @@ impl ChildKeysPrivate {
|
||||
input.extend_from_slice(&cci.to_be_bytes());
|
||||
|
||||
let hash_value = hmac_sha512::HMAC::mac(input, self.ccc);
|
||||
let (first, ccc) = split_hash(&hash_value);
|
||||
|
||||
let ssk = SecretSpendingKey(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
);
|
||||
let ccc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let ssk = SecretSpendingKey(first);
|
||||
|
||||
let nsk = ssk.generate_nullifier_secret_key(Some(cci));
|
||||
let vsk = ssk.generate_viewing_secret_seed_key(Some(cci));
|
||||
Self::from_ssk_and_ccc(ssk, ccc, Some(cci))
|
||||
}
|
||||
|
||||
fn from_ssk_and_ccc(ssk: SecretSpendingKey, ccc: [u8; 32], cci: Option<u32>) -> Self {
|
||||
let nsk = ssk.generate_nullifier_secret_key(cci);
|
||||
let vsk = ssk.generate_viewing_secret_seed_key(cci);
|
||||
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from(&vsk);
|
||||
@ -107,7 +78,7 @@ impl ChildKeysPrivate {
|
||||
BTreeMap::from_iter([(PrivateAccountKind::Regular(0), lee::Account::default())]),
|
||||
),
|
||||
ccc,
|
||||
cci: Some(cci),
|
||||
cci,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,10 +94,11 @@ impl KeyTreeNode for ChildKeysPrivate {
|
||||
|
||||
fn account_ids(&self) -> impl Iterator<Item = lee::AccountId> {
|
||||
let npk = self.value.0.nullifier_public_key;
|
||||
let vpk = self.value.0.viewing_public_key.clone();
|
||||
self.value
|
||||
.1
|
||||
.keys()
|
||||
.map(move |kind| lee::AccountId::for_private_account(&npk, kind))
|
||||
.map(move |kind| lee::AccountId::for_private_account(&npk, &vpk, kind))
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,16 +109,16 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::key_management::{self, secret_holders::ViewingSecretKey};
|
||||
|
||||
const SEED: [u8; 64] = [
|
||||
252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255, 49,
|
||||
43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18, 129, 223,
|
||||
176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56, 114, 39, 38,
|
||||
118, 197, 205, 225,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn master_key_generation() {
|
||||
let seed: [u8; 64] = [
|
||||
252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255,
|
||||
49, 43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18,
|
||||
129, 223, 176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56,
|
||||
114, 39, 38, 118, 197, 205, 225,
|
||||
];
|
||||
|
||||
let keys = ChildKeysPrivate::root(seed);
|
||||
let keys = ChildKeysPrivate::root(SEED);
|
||||
|
||||
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
|
||||
246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139,
|
||||
@ -179,6 +151,7 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
// Length matches MlKem768EncapsulationKey::LEN.
|
||||
let expected_vpk: [u8; 1184] = [
|
||||
127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220,
|
||||
150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181,
|
||||
@ -253,14 +226,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn child_keys_generation() {
|
||||
let seed: [u8; 64] = [
|
||||
252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255,
|
||||
49, 43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18,
|
||||
129, 223, 176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56,
|
||||
114, 39, 38, 118, 197, 205, 225,
|
||||
];
|
||||
|
||||
let root_node = ChildKeysPrivate::root(seed);
|
||||
let root_node = ChildKeysPrivate::root(SEED);
|
||||
let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32);
|
||||
|
||||
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
|
||||
@ -293,6 +259,7 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
// Length matches MlKem768EncapsulationKey::LEN.
|
||||
let expected_vpk: [u8; 1184] = [
|
||||
215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84,
|
||||
232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use k256::elliptic_curve::PrimeField as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyTreeNode;
|
||||
use crate::key_management::key_tree::{split_hash, traits::KeyTreeNode};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
|
||||
@ -21,52 +21,32 @@ impl ChildKeysPublic {
|
||||
#[must_use]
|
||||
pub fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
|
||||
let (first, cc) = split_hash(&hash_value);
|
||||
|
||||
let sk = lee::PrivateKey::try_new(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.expect("Expect a valid Private Key");
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`");
|
||||
let sk = lee::PrivateKey::try_new(first).expect("Expect a valid Private Key");
|
||||
|
||||
let cc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
sk,
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: None,
|
||||
}
|
||||
Self::from_sk_and_cc(sk, cc, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn nth_child(&self, cci: u32) -> Self {
|
||||
let hash_value = self.compute_hash_value(cci);
|
||||
let (first, cc) = split_hash(&hash_value);
|
||||
|
||||
let lhs = k256::Scalar::from_repr(
|
||||
(*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"))
|
||||
.into(),
|
||||
)
|
||||
.expect("Expect a valid k256 scalar");
|
||||
let lhs = k256::Scalar::from_repr(first.into()).expect("Expect a valid k256 scalar");
|
||||
let rhs =
|
||||
k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar");
|
||||
|
||||
let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into())
|
||||
.expect("Expect a valid private key");
|
||||
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`");
|
||||
|
||||
let cc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
Self::from_sk_and_cc(sk, cc, Some(cci))
|
||||
}
|
||||
|
||||
fn from_sk_and_cc(sk: lee::PrivateKey, cc: [u8; 32], cci: Option<u32>) -> Self {
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect(
|
||||
"`key_protocol::key_management::keys_public::ChildKeysPublic`: Invalid private key produced from `tweak`",
|
||||
);
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
@ -74,7 +54,7 @@ impl ChildKeysPublic {
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: Some(cci),
|
||||
cci,
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,15 +108,16 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
const SEED: [u8; 64] = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, 22,
|
||||
227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, 187,
|
||||
148, 92, 44, 253, 210, 37,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn master_keys_generation() {
|
||||
let seed = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||
187, 148, 92, 44, 253, 210, 37,
|
||||
];
|
||||
let keys = ChildKeysPublic::root(seed);
|
||||
let keys = ChildKeysPublic::root(SEED);
|
||||
|
||||
let expected_cc = [
|
||||
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
|
||||
@ -169,13 +150,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn child_keys_generation() {
|
||||
let seed = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||
187, 148, 92, 44, 253, 210, 37,
|
||||
];
|
||||
let root_keys = ChildKeysPublic::root(seed);
|
||||
let root_keys = ChildKeysPublic::root(SEED);
|
||||
let cci = (2_u32).pow(31) + 13;
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
|
||||
@ -39,16 +39,7 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
.try_into()
|
||||
.expect("SeedHolder seed is 64 bytes long");
|
||||
|
||||
let root_keys = N::from_seed(seed_fit);
|
||||
let account_id_map = root_keys
|
||||
.account_ids()
|
||||
.map(|id| (id, ChainIndex::root()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
key_map: BTreeMap::from_iter([(ChainIndex::root(), root_keys)]),
|
||||
account_id_map,
|
||||
}
|
||||
Self::new_from_root(N::from_seed(seed_fit))
|
||||
}
|
||||
|
||||
pub fn new_from_root(root: N) -> Self {
|
||||
@ -63,6 +54,15 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_child(&mut self, child_keys: N, chain_index: ChainIndex) -> ChainIndex {
|
||||
for account_id in child_keys.account_ids() {
|
||||
self.account_id_map.insert(account_id, chain_index.clone());
|
||||
}
|
||||
self.key_map.insert(chain_index.clone(), child_keys);
|
||||
|
||||
chain_index
|
||||
}
|
||||
|
||||
pub fn generate_new_node(&mut self, parent_cci: &ChainIndex) -> Option<ChainIndex> {
|
||||
let parent_keys = self.key_map.get(parent_cci)?;
|
||||
let next_child_id = self
|
||||
@ -71,14 +71,8 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
let next_cci = parent_cci.nth_child(next_child_id);
|
||||
|
||||
let child_keys = parent_keys.derive_child(next_child_id);
|
||||
let account_ids = child_keys.account_ids();
|
||||
|
||||
for account_id in account_ids {
|
||||
self.account_id_map.insert(account_id, next_cci.clone());
|
||||
}
|
||||
self.key_map.insert(next_cci.clone(), child_keys);
|
||||
|
||||
Some(next_cci)
|
||||
Some(self.insert_child(child_keys, next_cci))
|
||||
}
|
||||
|
||||
pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option<ChainIndex> {
|
||||
@ -86,14 +80,8 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
let child_id = *chain_index.chain().last()?;
|
||||
|
||||
let child_keys = parent_keys.derive_child(child_id);
|
||||
let account_ids = child_keys.account_ids();
|
||||
|
||||
for account_id in account_ids {
|
||||
self.account_id_map.insert(account_id, chain_index.clone());
|
||||
}
|
||||
self.key_map.insert(chain_index.clone(), child_keys);
|
||||
|
||||
Some(chain_index.clone())
|
||||
Some(self.insert_child(child_keys, chain_index.clone()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -200,24 +188,27 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
}
|
||||
|
||||
impl KeyTree<ChildKeysPublic> {
|
||||
/// Pairs `cci` with the account ID of the node stored at it.
|
||||
fn account_id_for_cci(&self, cci: ChainIndex) -> Option<(lee::AccountId, ChainIndex)> {
|
||||
let node = self.key_map.get(&cci)?;
|
||||
let account_id = node.account_ids().next()?;
|
||||
Some((account_id, cci))
|
||||
}
|
||||
|
||||
/// Generate a new public key node, returning the account ID and chain index.
|
||||
pub fn generate_new_public_node(
|
||||
&mut self,
|
||||
parent_cci: &ChainIndex,
|
||||
) -> Option<(lee::AccountId, ChainIndex)> {
|
||||
let cci = self.generate_new_node(parent_cci)?;
|
||||
let node = self.key_map.get(&cci)?;
|
||||
let account_id = node.account_ids().next()?;
|
||||
Some((account_id, cci))
|
||||
self.account_id_for_cci(cci)
|
||||
}
|
||||
|
||||
/// Generate a new public key node using layered placement, returning the account ID and chain
|
||||
/// index.
|
||||
pub fn generate_new_public_node_layered(&mut self) -> Option<(lee::AccountId, ChainIndex)> {
|
||||
let cci = self.generate_new_node_layered()?;
|
||||
let node = self.key_map.get(&cci)?;
|
||||
let account_id = node.account_ids().next()?;
|
||||
Some((account_id, cci))
|
||||
self.account_id_for_cci(cci)
|
||||
}
|
||||
|
||||
/// Cleanup of non-initialized accounts in a public tree.
|
||||
@ -277,6 +268,7 @@ impl KeyTree<ChildKeysPrivate> {
|
||||
let node = self.key_map.get(cci)?;
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&node.value.0.nullifier_public_key,
|
||||
&node.value.0.viewing_public_key,
|
||||
identifier,
|
||||
);
|
||||
if self.account_id_map.contains_key(&account_id) {
|
||||
@ -322,6 +314,16 @@ impl KeyTree<ChildKeysPrivate> {
|
||||
}
|
||||
}
|
||||
|
||||
const fn split_hash(hash_value: &[u8; 64]) -> ([u8; 32], [u8; 32]) {
|
||||
let first = *hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32");
|
||||
let last = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
(first, last)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about this in tests")]
|
||||
@ -339,6 +341,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_hash_splits_into_first_and_last_32_bytes() {
|
||||
let mut hash_value = [0_u8; 64];
|
||||
hash_value[..32].fill(0xAA);
|
||||
hash_value[32..].fill(0xBB);
|
||||
|
||||
let (first, last) = split_hash(&hash_value);
|
||||
|
||||
assert_eq!(first, [0xAA; 32]);
|
||||
assert_eq!(last, [0xBB; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_key_tree() {
|
||||
let seed_holder = seed_holder_for_tests();
|
||||
|
||||
@ -25,8 +25,22 @@ impl KeyChain {
|
||||
#[must_use]
|
||||
pub fn new_os_random() -> Self {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Now entirely sure if we need it in the future.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
Self::from_seed_holder(&seed_holder)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
|
||||
(Self::from_seed_holder(&seed_holder), mnemonic)
|
||||
}
|
||||
|
||||
fn from_seed_holder(seed_holder: &SeedHolder) -> Self {
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||
@ -42,29 +56,6 @@ impl KeyChain {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) {
|
||||
// Currently dropping SeedHolder at the end of initialization.
|
||||
// Not entirely sure if we need it in the future.
|
||||
let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase);
|
||||
let secret_spending_key = seed_holder.produce_top_secret_key_holder();
|
||||
|
||||
let private_key_holder = secret_spending_key.produce_private_key_holder(None);
|
||||
|
||||
let nullifier_public_key = private_key_holder.generate_nullifier_public_key();
|
||||
let viewing_public_key = private_key_holder.generate_viewing_public_key();
|
||||
|
||||
(
|
||||
Self {
|
||||
secret_spending_key,
|
||||
private_key_holder,
|
||||
nullifier_public_key,
|
||||
viewing_public_key,
|
||||
},
|
||||
mnemonic,
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn calculate_shared_secret_receiver(
|
||||
&self,
|
||||
|
||||
@ -43,16 +43,7 @@ pub struct PrivateKeyHolder {
|
||||
impl SeedHolder {
|
||||
#[must_use]
|
||||
pub fn new_os_random() -> Self {
|
||||
let mut enthopy_bytes: [u8; 32] = [0; 32];
|
||||
OsRng.fill_bytes(&mut enthopy_bytes);
|
||||
|
||||
let mnemonic = Mnemonic::from_entropy(&enthopy_bytes)
|
||||
.expect("Enthropy must be a multiple of 32 bytes");
|
||||
let seed_wide = mnemonic.to_seed("mnemonic");
|
||||
|
||||
Self {
|
||||
seed: seed_wide.to_vec(),
|
||||
}
|
||||
Self::new_mnemonic("mnemonic").0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -62,14 +53,8 @@ impl SeedHolder {
|
||||
|
||||
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,
|
||||
)
|
||||
(Self::from_mnemonic(&mnemonic, passphrase), mnemonic)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -107,10 +92,7 @@ impl SecretSpendingKey {
|
||||
const SUFFIX_1: &[u8; 1] = &[1];
|
||||
const SUFFIX_2: &[u8; 19] = &[0; 19];
|
||||
|
||||
let index = match index {
|
||||
None => 0_u32,
|
||||
_ => index.expect("Expect a valid u32"),
|
||||
};
|
||||
let index = index.unwrap_or(0);
|
||||
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
@ -129,10 +111,7 @@ impl SecretSpendingKey {
|
||||
const SUFFIX_1: &[u8; 1] = &[2];
|
||||
const SUFFIX_2: &[u8; 19] = &[0; 19];
|
||||
|
||||
let index = match index {
|
||||
None => 0_u32,
|
||||
_ => index.expect("Expect a valid u32"),
|
||||
};
|
||||
let index = index.unwrap_or(0);
|
||||
|
||||
let mut bytes: Vec<u8> = Vec::with_capacity(64);
|
||||
bytes.extend_from_slice(PREFIX);
|
||||
@ -146,14 +125,7 @@ impl SecretSpendingKey {
|
||||
|
||||
let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed");
|
||||
|
||||
ViewingSecretKey::new(
|
||||
*full_seed
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
*full_seed
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32"),
|
||||
)
|
||||
Self::generate_viewing_secret_key(full_seed)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@ -181,7 +153,7 @@ impl From<&ViewingSecretKey> for ViewingPublicKey {
|
||||
seed_bytes[32..].copy_from_slice(&sk.z);
|
||||
let dk = <MlKem768 as Kem>::DecapsulationKey::from_seed(Seed::from(seed_bytes));
|
||||
Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec())
|
||||
.expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes")
|
||||
.expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always ViewingPublicKey::LEN bytes")
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,7 +173,6 @@ impl PrivateKeyHolder {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// TODO? are these necessary?
|
||||
#[test]
|
||||
fn seed_generation_test() {
|
||||
let seed_holder = SeedHolder::new_os_random();
|
||||
|
||||
@ -6,6 +6,7 @@ use std::{
|
||||
use lee_core::{
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{
|
||||
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
|
||||
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
|
||||
@ -21,7 +22,7 @@ pub struct ExecutionState {
|
||||
block_validity_window: BlockValidityWindow,
|
||||
timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk,
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, vpk,
|
||||
/// identifier)` 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`
|
||||
@ -43,12 +44,13 @@ pub struct ExecutionState {
|
||||
/// `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 private-PDA `pre_state`'s position in `account_identities` to the (npk,
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, vpk,
|
||||
/// identifier) supplied for that position. Built once in `derive_from_outputs` by walking
|
||||
/// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and
|
||||
/// `account_identities` and consulting `npk_vpk_if_private_pda`. Used later by the claim and
|
||||
/// caller-seeds authorization paths to verify
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id`.
|
||||
private_pda_by_position: HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
@ -63,11 +65,13 @@ impl ExecutionState {
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
// order, so position here matches `pre_state_position` used downstream in
|
||||
// `validate_and_sync_states`.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
|
||||
HashMap::new();
|
||||
let mut private_pda_by_position: HashMap<
|
||||
usize,
|
||||
(NullifierPublicKey, ViewingPublicKey, Identifier),
|
||||
> = HashMap::new();
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if let Some((npk, identifier)) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, (npk, identifier));
|
||||
if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() {
|
||||
private_pda_by_position.insert(pos, (npk, vpk, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +111,7 @@ impl ExecutionState {
|
||||
timestamp_validity_window,
|
||||
private_pda_bound_positions: HashMap::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_npk_by_position,
|
||||
private_pda_by_position,
|
||||
authorized_accounts: HashSet::new(),
|
||||
};
|
||||
|
||||
@ -289,7 +293,7 @@ impl ExecutionState {
|
||||
let is_authorized = resolve_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_npk_by_position,
|
||||
&self.private_pda_by_position,
|
||||
&mut self.authorized_accounts,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
@ -309,6 +313,7 @@ impl ExecutionState {
|
||||
let external_seed = match account_identities.get(pre_state_position) {
|
||||
Some(InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@ -317,6 +322,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@ -327,6 +333,7 @@ impl ExecutionState {
|
||||
}
|
||||
Some(InputAccountIdentity::PrivatePdaUpdate {
|
||||
nsk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@ -336,6 +343,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
&npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@ -416,14 +424,19 @@ impl ExecutionState {
|
||||
match claim {
|
||||
Claim::Authorized => {}
|
||||
Claim::Pda(seed) => {
|
||||
let (npk, identifier) = self
|
||||
.private_pda_npk_by_position
|
||||
let (npk, vpk, identifier) = self
|
||||
.private_pda_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, *identifier);
|
||||
let pda = AccountId::for_private_pda(
|
||||
&program_id,
|
||||
&seed,
|
||||
npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
pre_account_id, pda,
|
||||
"Invalid private PDA claim for account {pre_account_id}"
|
||||
@ -548,7 +561,7 @@ fn bind_private_pda_position(
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_npk_by_position: &HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
authorized_accounts: &mut HashSet<AccountId>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
@ -562,9 +575,10 @@ fn resolve_authorization_and_record_bindings(
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
}
|
||||
if let Some((npk, identifier)) =
|
||||
private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id
|
||||
if let Some((npk, vpk, identifier)) =
|
||||
private_pda_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
|
||||
== pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey,
|
||||
InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
|
||||
use crate::execution_state::ExecutionState;
|
||||
@ -40,14 +41,14 @@ pub fn compute_circuit_output(
|
||||
output.public_post_states.push(post_state);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
nsk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -62,7 +63,7 @@ pub fn compute_circuit_output(
|
||||
|
||||
let new_nullifier = (
|
||||
Nullifier::for_account_initialization(&account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
*commitment_root,
|
||||
);
|
||||
let new_nonce = Nonce::private_account_nonce_init(&account_id);
|
||||
|
||||
@ -72,23 +73,22 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -110,21 +110,21 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
} => {
|
||||
let account_id = AccountId::for_regular_private_account(npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert_eq!(
|
||||
@ -139,7 +139,7 @@ pub fn compute_circuit_output(
|
||||
|
||||
let new_nullifier = (
|
||||
Nullifier::for_account_initialization(&account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
*commitment_root,
|
||||
);
|
||||
let new_nonce = Nonce::private_account_nonce_init(&account_id);
|
||||
|
||||
@ -149,25 +149,25 @@ pub fn compute_circuit_output(
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
npk,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag,
|
||||
npk: _,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
npk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
seed: _,
|
||||
} => {
|
||||
// The npk-to-account_id binding is established upstream in
|
||||
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
|
||||
// match. Here we only enforce the init pre-conditions. The supplied npk on
|
||||
// the variant has been recorded into `private_pda_npk_by_position` and used
|
||||
// the variant has been recorded into `private_pda_by_position` and used
|
||||
// for the binding check; we use `pre_state.account_id` directly for nullifier
|
||||
// and commitment derivation.
|
||||
assert!(
|
||||
@ -182,7 +182,7 @@ pub fn compute_circuit_output(
|
||||
|
||||
let new_nullifier = (
|
||||
Nullifier::for_account_initialization(&pre_state.account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
*commitment_root,
|
||||
);
|
||||
let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id);
|
||||
|
||||
@ -200,17 +200,16 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
npk,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
@ -235,6 +234,7 @@ pub fn compute_circuit_output(
|
||||
let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk);
|
||||
|
||||
let account_id = pre_state.account_id;
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let (authority_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
|
||||
@ -248,9 +248,9 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
&npk,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
@ -271,9 +271,9 @@ fn emit_private_output(
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
epk: &EphemeralPublicKey,
|
||||
view_tag: u8,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
random_seed: &[u8; 32],
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
) {
|
||||
@ -283,10 +283,23 @@ fn emit_private_output(
|
||||
post_with_updated_nonce.nonce = new_nonce;
|
||||
|
||||
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
|
||||
|
||||
let esk = EphemeralSecretKey::new(account_id, random_seed, &new_nonce);
|
||||
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, &esk);
|
||||
|
||||
// Currently the view tag is properlty generated for all accounts.
|
||||
// To increase privacy, this will be changed in the later version
|
||||
// to only be generated explicitly for initialized accounts and
|
||||
// fed by the prover directly for updated accounts.
|
||||
//
|
||||
// See issue 573:
|
||||
// https://github.com/logos-blockchain/logos-execution-zone/issues/573
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
kind,
|
||||
shared_secret,
|
||||
&shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
);
|
||||
@ -296,7 +309,7 @@ fn emit_private_output(
|
||||
.encrypted_private_post_states
|
||||
.push(EncryptedAccountData {
|
||||
ciphertext: encrypted_account,
|
||||
epk: epk.clone(),
|
||||
epk,
|
||||
view_tag,
|
||||
});
|
||||
*output_index = output_index
|
||||
|
||||
@ -28,10 +28,8 @@ build_utils.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lee_core = { workspace = true, features = ["test_utils"] }
|
||||
token_core.workspace = true
|
||||
test_methods = { path = "test_methods" }
|
||||
|
||||
env_logger.workspace = true
|
||||
hex-literal = "1.0.0"
|
||||
test-case = "3.3.1"
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ thiserror.workspace = true
|
||||
bytemuck.workspace = true
|
||||
bytesize.workspace = true
|
||||
base58.workspace = true
|
||||
ml-kem = { workspace = true, optional = true, features = ["getrandom"] }
|
||||
ml-kem = { workspace = true }
|
||||
chacha20 = { version = "0.10" }
|
||||
|
||||
[dev-dependencies]
|
||||
@ -24,5 +24,5 @@ serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
host = ["dep:ml-kem"]
|
||||
host = ["ml-kem/getrandom"]
|
||||
test_utils = ["host"]
|
||||
|
||||
@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
NullifierSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
|
||||
encryption::{EncryptedAccountData, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
@ -14,15 +14,13 @@ pub struct PrivacyPreservingCircuitInput {
|
||||
pub program_outputs: Vec<ProgramOutput>,
|
||||
/// One entry per `pre_state`, in the same order as the program's `pre_states`.
|
||||
/// Length must equal the number of `pre_states` derived from `program_outputs`.
|
||||
/// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions`
|
||||
/// The guest's `private_pda_by_position` and `private_pda_bound_positions`
|
||||
/// rely on this position alignment.
|
||||
pub account_identities: Vec<InputAccountIdentity>,
|
||||
/// Program ID.
|
||||
pub program_id: ProgramId,
|
||||
}
|
||||
|
||||
/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields
|
||||
/// the guest needs for that account's code path.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum InputAccountIdentity {
|
||||
/// Public account. The guest reads pre/post state from `program_outputs` and emits no
|
||||
@ -30,21 +28,20 @@ pub enum InputAccountIdentity {
|
||||
Public,
|
||||
/// Init of an authorized standalone private account: no membership proof. The `pre_state`
|
||||
/// must be `Account::default()`. The `account_id` is derived as
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
|
||||
/// matched against `pre_state.account_id`.
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), vpk, identifier)`
|
||||
/// and matched against `pre_state.account_id`.
|
||||
PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
nsk: NullifierSecretKey,
|
||||
identifier: Identifier,
|
||||
commitment_root: CommitmentSetDigest,
|
||||
},
|
||||
/// Update of an authorized standalone private account: existing on-chain commitment, with
|
||||
/// membership proof.
|
||||
PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
@ -52,25 +49,25 @@ pub enum InputAccountIdentity {
|
||||
/// Init of a standalone private account the caller does not own (e.g. a recipient who
|
||||
/// doesn't yet exist on chain). No `nsk`, no membership proof.
|
||||
PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
commitment_root: CommitmentSetDigest,
|
||||
},
|
||||
/// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream
|
||||
/// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the
|
||||
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
|
||||
/// as the 4th input.
|
||||
PrivatePdaInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
commitment_root: CommitmentSetDigest,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
|
||||
/// external derivation check
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
|
||||
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
|
||||
/// == false`.
|
||||
@ -80,15 +77,14 @@ pub enum InputAccountIdentity {
|
||||
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
|
||||
/// previously-seen authorization in a chained call.
|
||||
PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
/// When `Some((seed, authority_program_id))`, the circuit binds this position via the
|
||||
/// external derivation check
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, identifier) ==
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish
|
||||
/// the binding. The `pre_state` must have `is_authorized == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
@ -109,17 +105,23 @@ impl InputAccountIdentity {
|
||||
)
|
||||
}
|
||||
|
||||
/// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both
|
||||
/// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`.
|
||||
#[must_use]
|
||||
pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> {
|
||||
pub fn npk_vpk_if_private_pda(
|
||||
&self,
|
||||
) -> Option<(NullifierPublicKey, ViewingPublicKey, Identifier)> {
|
||||
match self {
|
||||
Self::PrivatePdaInit {
|
||||
npk, identifier, ..
|
||||
} => Some((*npk, *identifier)),
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
..
|
||||
} => Some((*npk, vpk.clone(), *identifier)),
|
||||
Self::PrivatePdaUpdate {
|
||||
nsk, identifier, ..
|
||||
} => Some((NullifierPublicKey::from(nsk), *identifier)),
|
||||
nsk,
|
||||
vpk,
|
||||
identifier,
|
||||
..
|
||||
} => Some((NullifierPublicKey::from(nsk), vpk.clone(), *identifier)),
|
||||
Self::Public
|
||||
| Self::PrivateAuthorizedInit { .. }
|
||||
| Self::PrivateAuthorizedUpdate { .. }
|
||||
@ -158,7 +160,7 @@ mod tests {
|
||||
use crate::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
encryption::Ciphertext,
|
||||
encryption::{Ciphertext, EphemeralPublicKey},
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@ -7,7 +7,7 @@ use std::io::Read as _;
|
||||
#[cfg(feature = "host")]
|
||||
use crate::Nullifier;
|
||||
#[cfg(feature = "host")]
|
||||
use crate::encryption::EphemeralPublicKey;
|
||||
use crate::encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN};
|
||||
#[cfg(feature = "host")]
|
||||
use crate::error::LeeCoreError;
|
||||
use crate::{
|
||||
@ -168,7 +168,7 @@ impl EphemeralPublicKey {
|
||||
/// Deserializes an ML-KEM-768 ciphertext from a cursor.
|
||||
/// Reads exactly 1088 bytes — the fixed ciphertext size for ML-KEM-768.
|
||||
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, LeeCoreError> {
|
||||
let mut value = vec![0_u8; 1088];
|
||||
let mut value = vec![0_u8; ML_KEM_768_CIPHERTEXT_LEN];
|
||||
cursor.read_exact(&mut value)?;
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
@ -5,20 +5,46 @@ use chacha20::{
|
||||
};
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
pub mod shared_key_derivation;
|
||||
|
||||
/// Length in bytes of an ML-KEM-768 ciphertext (the `EphemeralPublicKey` payload).
|
||||
pub const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088;
|
||||
|
||||
pub type Scalar = [u8; 32];
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct EphemeralSecretKey(pub [u8; 32]);
|
||||
|
||||
impl EphemeralSecretKey {
|
||||
/// Derives an ephemeral secret key from OS randomness and account-specific values.
|
||||
///
|
||||
/// For updates, `nonce` carries `nsk`-derived entropy, making `esk` strong even
|
||||
/// with a compromised RNG. For inits, `nonce` is deterministic, so `random_seed`
|
||||
/// is the sole entropy source.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
account_id: &crate::account::AccountId,
|
||||
random_seed: &[u8; 32],
|
||||
nonce: &crate::account::Nonce,
|
||||
) -> Self {
|
||||
const PREFIX: &[u8; 14] = b"/LEE/v0.3/esk/";
|
||||
let mut input = [0_u8; 14 + 32 + 32 + 16];
|
||||
input[0..14].copy_from_slice(PREFIX);
|
||||
input[14..46].copy_from_slice(account_id.value());
|
||||
input[46..78].copy_from_slice(random_seed);
|
||||
input[78..94].copy_from_slice(&nonce.0.to_le_bytes());
|
||||
Self(Impl::hash_bytes(&input).as_bytes().try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct SharedSecretKey(pub [u8; 32]);
|
||||
|
||||
/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the
|
||||
/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768.
|
||||
/// former ECDH ephemeral public key. Always `ML_KEM_768_CIPHERTEXT_LEN` (1088) bytes.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
|
||||
@ -52,6 +78,18 @@ pub struct EncryptedAccountData {
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
const PREFIX: &[u8; 18] = b"/LEE/v0.3/ViewTag/";
|
||||
let mut bytes = [0_u8; 18 + 32 + ViewingPublicKey::LEN];
|
||||
bytes[0..18].copy_from_slice(PREFIX);
|
||||
bytes[18..50].copy_from_slice(&npk.to_byte_array());
|
||||
bytes[50..].copy_from_slice(vpk.to_bytes());
|
||||
Impl::hash_bytes(&bytes).as_bytes()[0]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
@ -68,16 +106,6 @@ impl EncryptedAccountData {
|
||||
view_tag,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk).
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/");
|
||||
bytes.extend_from_slice(&npk.to_byte_array());
|
||||
bytes.extend_from_slice(vpk.to_bytes());
|
||||
Impl::hash_bytes(&bytes).as_bytes()[0]
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptionScheme {
|
||||
@ -113,12 +141,12 @@ impl EncryptionScheme {
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
) -> [u8; 32] {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
bytes.extend_from_slice(b"LEE/v0.2/KDF-SHA256/");
|
||||
bytes.extend_from_slice(&shared_secret.0);
|
||||
bytes.extend_from_slice(&commitment.to_byte_array());
|
||||
bytes.extend_from_slice(&output_index.to_le_bytes());
|
||||
const PREFIX: &[u8; 20] = b"LEE/v0.2/KDF-SHA256/";
|
||||
let mut bytes = [0_u8; 20 + 32 + 32 + 4];
|
||||
bytes[0..20].copy_from_slice(PREFIX);
|
||||
bytes[20..52].copy_from_slice(&shared_secret.0);
|
||||
bytes[52..84].copy_from_slice(&commitment.to_byte_array());
|
||||
bytes[84..88].copy_from_slice(&output_index.to_le_bytes());
|
||||
|
||||
Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()
|
||||
}
|
||||
@ -235,4 +263,41 @@ mod tests {
|
||||
"wrong shared secret must not produce the correct plaintext"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_is_deterministic() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let random_seed = [2_u8; 32];
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk1 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
|
||||
let esk2 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
|
||||
assert_eq!(esk1.0, esk2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_account_id() {
|
||||
let random_seed = [2_u8; 32];
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk_a = EphemeralSecretKey::new(&AccountId::new([0_u8; 32]), &random_seed, &nonce);
|
||||
let esk_b = EphemeralSecretKey::new(&AccountId::new([1_u8; 32]), &random_seed, &nonce);
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_random_seed() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk_a = EphemeralSecretKey::new(&account_id, &[0_u8; 32], &nonce);
|
||||
let esk_b = EphemeralSecretKey::new(&account_id, &[1_u8; 32], &nonce);
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_nonce() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let random_seed = [2_u8; 32];
|
||||
let esk_a = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(0));
|
||||
let esk_b = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(1));
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
|
||||
#[cfg(feature = "host")]
|
||||
use ml_kem::Encapsulate as _;
|
||||
use ml_kem::{Decapsulate as _, KeyExport as _, Seed};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{EphemeralPublicKey, SharedSecretKey};
|
||||
@ -26,6 +28,7 @@ impl MlKem768EncapsulationKey {
|
||||
pub const LEN: usize = 1184;
|
||||
|
||||
/// Construct from raw bytes, returning an error if the length is not [`Self::LEN`].
|
||||
#[cfg(feature = "host")]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, crate::error::LeeCoreError> {
|
||||
if bytes.len() != Self::LEN {
|
||||
return Err(crate::error::LeeCoreError::DeserializationError(format!(
|
||||
@ -59,12 +62,13 @@ impl SharedSecretKey {
|
||||
/// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction
|
||||
/// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via
|
||||
/// [`Self::decapsulate`].
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) {
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
ek.0.as_slice()
|
||||
.try_into()
|
||||
.expect("MlKem768EncapsulationKey must be 1184 bytes");
|
||||
.expect("MlKem768EncapsulationKey must be MlKem768EncapsulationKey::LEN bytes");
|
||||
let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect(
|
||||
"MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key",
|
||||
);
|
||||
@ -76,35 +80,23 @@ impl SharedSecretKey {
|
||||
(Self(ss_bytes), EphemeralPublicKey(ct.to_vec()))
|
||||
}
|
||||
|
||||
/// Deterministically encapsulate a shared secret toward `ek` for use in tests.
|
||||
/// Deterministically encapsulate a shared secret toward `ek` using a
|
||||
/// pre-derived `esk` as the ML-KEM encapsulation randomness.
|
||||
///
|
||||
/// The shared secret has no secret entropy — it is fully determined by `ek`,
|
||||
/// `message_hash`, and `output_index`, all of which are public. This makes it
|
||||
/// unsuitable for real encryption but useful for producing stable, reproducible
|
||||
/// shared secrets in unit tests. Use a distinct `output_index` per output to
|
||||
/// avoid EPK collisions across multiple outputs in the same test.
|
||||
///
|
||||
/// For production use [`Self::encapsulate`], which draws randomness from the OS.
|
||||
#[cfg(any(test, feature = "test_utils"))]
|
||||
/// The `esk` must be derived via `derive_esk(account_id, random_seed, nonce)`
|
||||
/// which binds it to the account and incorporates OS entropy.
|
||||
#[must_use]
|
||||
pub fn encapsulate_deterministic(
|
||||
ek: &MlKem768EncapsulationKey,
|
||||
message_hash: &[u8; 32],
|
||||
output_index: u32,
|
||||
esk: &crate::encryption::EphemeralSecretKey,
|
||||
) -> (Self, EphemeralPublicKey) {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut input = Vec::with_capacity(36);
|
||||
input.extend_from_slice(message_hash);
|
||||
input.extend_from_slice(&output_index.to_le_bytes());
|
||||
let hash = Impl::hash_bytes(&input);
|
||||
let m: ml_kem::B32 =
|
||||
ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes");
|
||||
let m: ml_kem::B32 = ml_kem::array::Array::try_from(esk.0.as_slice())
|
||||
.expect("EphemeralSecretKey is 32 bytes");
|
||||
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
ek.0.as_slice()
|
||||
.try_into()
|
||||
.expect("MlKem768EncapsulationKey must be 1184 bytes");
|
||||
.expect("MlKem768EncapsulationKey must be MlKem768EncapsulationKey::LEN bytes");
|
||||
let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect(
|
||||
"MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key",
|
||||
);
|
||||
@ -118,8 +110,9 @@ impl SharedSecretKey {
|
||||
|
||||
/// Receiver: decapsulate the shared secret from a KEM ciphertext.
|
||||
///
|
||||
/// Returns `None` if the `EphemeralPublicKey` is not exactly 1088 bytes — callers on
|
||||
/// the wallet scan path should skip the output rather than panic on malformed chain data.
|
||||
/// Returns `None` if the `EphemeralPublicKey` is not exactly [`ML_KEM_768_CIPHERTEXT_LEN`]
|
||||
/// bytes — callers on the wallet scan path should skip the output rather than panic on
|
||||
/// malformed chain data.
|
||||
///
|
||||
/// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed.
|
||||
#[must_use]
|
||||
@ -146,6 +139,7 @@ mod tests {
|
||||
use ml_kem::KeyExport as _;
|
||||
|
||||
use super::*;
|
||||
use crate::ML_KEM_768_CIPHERTEXT_LEN;
|
||||
|
||||
#[test]
|
||||
fn encapsulate_decapsulate_round_trip() {
|
||||
@ -164,11 +158,15 @@ mod tests {
|
||||
let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap();
|
||||
|
||||
assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match");
|
||||
assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes");
|
||||
assert_eq!(
|
||||
epk.0.len(),
|
||||
ML_KEM_768_CIPHERTEXT_LEN,
|
||||
"ML-KEM-768 ciphertext length"
|
||||
);
|
||||
assert_eq!(
|
||||
ek.0.len(),
|
||||
1184,
|
||||
"ML-KEM-768 encapsulation key is 1184 bytes"
|
||||
MlKem768EncapsulationKey::LEN,
|
||||
"ML-KEM-768 encapsulation key length"
|
||||
);
|
||||
}
|
||||
|
||||
@ -177,15 +175,15 @@ mod tests {
|
||||
let d = [1_u8; 32];
|
||||
let z = [2_u8; 32];
|
||||
|
||||
// Too short — 100 bytes instead of 1088.
|
||||
// Too short — 100 bytes instead of ML_KEM_768_CIPHERTEXT_LEN.
|
||||
let short_epk = EphemeralPublicKey(vec![42_u8; 100]);
|
||||
assert!(
|
||||
SharedSecretKey::decapsulate(&short_epk, &d, &z).is_none(),
|
||||
"short EphemeralPublicKey must return None"
|
||||
);
|
||||
|
||||
// Too long — 1089 bytes instead of 1088.
|
||||
let long_epk = EphemeralPublicKey(vec![42_u8; 1089]);
|
||||
// Too long — ML_KEM_768_CIPHERTEXT_LEN + 1.
|
||||
let long_epk = EphemeralPublicKey(vec![42_u8; ML_KEM_768_CIPHERTEXT_LEN + 1]);
|
||||
assert!(
|
||||
SharedSecretKey::decapsulate(&long_epk, &d, &z).is_none(),
|
||||
"long EphemeralPublicKey must return None"
|
||||
|
||||
@ -11,7 +11,8 @@ pub use commitment::{
|
||||
compute_digest_for_path,
|
||||
};
|
||||
pub use encryption::{
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, SharedSecretKey, ViewTag,
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey,
|
||||
ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag,
|
||||
};
|
||||
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Commitment, account::AccountId};
|
||||
use crate::{Commitment, account::AccountId, encryption::ViewingPublicKey};
|
||||
|
||||
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
|
||||
|
||||
@ -16,12 +16,16 @@ impl AccountId {
|
||||
/// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public
|
||||
/// key and identifier.
|
||||
#[must_use]
|
||||
pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self {
|
||||
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
|
||||
let mut bytes = [0; 80];
|
||||
pub fn for_regular_private_account(
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Self {
|
||||
let mut bytes = [0_u8; 32 + 32 + ViewingPublicKey::LEN + 16];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
|
||||
bytes[32..64].copy_from_slice(&npk.0);
|
||||
bytes[64..80].copy_from_slice(&identifier.to_le_bytes());
|
||||
bytes[64..64 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
|
||||
bytes[64 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
|
||||
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
@ -32,9 +36,9 @@ impl AccountId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
|
||||
fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self {
|
||||
Self::for_regular_private_account(npk, identifier)
|
||||
impl From<(&NullifierPublicKey, &ViewingPublicKey, Identifier)> for AccountId {
|
||||
fn from((npk, vpk, identifier): (&NullifierPublicKey, &ViewingPublicKey, Identifier)) -> Self {
|
||||
Self::for_regular_private_account(npk, vpk, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,12 +162,13 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50,
|
||||
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
|
||||
242, 239, 57, 244, 89, 109, 65, 201, 223, 100, 43, 87, 205, 83, 148, 161, 176, 22, 208,
|
||||
220, 68, 135, 10, 171, 182, 80, 54, 74, 228, 244, 236, 7,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 0);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -175,12 +180,13 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95,
|
||||
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
|
||||
149, 125, 157, 109, 119, 81, 9, 163, 231, 181, 214, 43, 57, 113, 221, 72, 180, 149,
|
||||
189, 170, 32, 181, 255, 231, 19, 92, 235, 59, 153, 185, 172, 206,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 1);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -193,12 +199,13 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174,
|
||||
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
|
||||
30, 232, 222, 201, 233, 125, 124, 194, 58, 39, 121, 96, 185, 84, 168, 109, 80, 111,
|
||||
159, 112, 84, 100, 133, 244, 16, 34, 221, 35, 128, 131, 98, 159,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
BlockId, Identifier, NullifierPublicKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
|
||||
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
|
||||
@ -154,19 +155,21 @@ impl AccountId {
|
||||
program_id: &ProgramId,
|
||||
seed: &PdaSeed,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> 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; 144];
|
||||
let mut bytes = [0_u8; 32 + 32 + 32 + 32 + ViewingPublicKey::LEN + 16];
|
||||
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());
|
||||
bytes[128..144].copy_from_slice(&identifier.to_le_bytes());
|
||||
bytes[128..128 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
|
||||
bytes[128 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
@ -177,16 +180,20 @@ impl AccountId {
|
||||
|
||||
/// Derives the [`AccountId`] for a private account from the nullifier public key and kind.
|
||||
#[must_use]
|
||||
pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self {
|
||||
pub fn for_private_account(
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
kind: &PrivateAccountKind,
|
||||
) -> Self {
|
||||
match kind {
|
||||
PrivateAccountKind::Regular(identifier) => {
|
||||
Self::for_regular_private_account(npk, *identifier)
|
||||
Self::for_regular_private_account(npk, vpk, *identifier)
|
||||
}
|
||||
PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier,
|
||||
} => Self::for_private_pda(program_id, seed, npk, *identifier),
|
||||
} => Self::for_private_pda(program_id, seed, npk, vpk, *identifier),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,7 +237,7 @@ impl ChainedCall {
|
||||
/// Represents the final state of an `Account` after a program execution.
|
||||
///
|
||||
/// A post state may optionally request that the executing program
|
||||
/// becomes the owner of the account (a “claim”). This is used to signal
|
||||
/// becomes the owner of the account (a "claim"). This is used to signal
|
||||
/// that the program intends to take ownership of the account.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))]
|
||||
@ -317,7 +324,7 @@ impl AccountPostState {
|
||||
pub type BlockValidityWindow = ValidityWindow<BlockId>;
|
||||
pub type TimestampValidityWindow = ValidityWindow<Timestamp>;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
|
||||
@ -766,338 +773,4 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validity_window_unbounded_accepts_any_value() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_bounded_range_includes_from_excludes_to() {
|
||||
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(9));
|
||||
assert!(!w.is_valid_for(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_from_bound() {
|
||||
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_to_bound() {
|
||||
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(4));
|
||||
assert!(!w.is_valid_for(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_adjacent_bounds_are_invalid() {
|
||||
// [5, 5) is an empty range — from == to
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
|
||||
assert_eq!(w.start(), Some(3));
|
||||
assert_eq!(w.end(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_for_unbounded() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_empty_is_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_inverted_is_invalid() {
|
||||
let from = 10_u64;
|
||||
let to = 5_u64;
|
||||
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_range() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(10_u64..100)
|
||||
.unwrap();
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_from() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(10_u64..);
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_to() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(..100_u64);
|
||||
assert_eq!(output.block_validity_window.start(), None);
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_empty_range_fails() {
|
||||
let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(5_u64..5);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_without_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.required_claim().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_account_getter() {
|
||||
let mut account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let mut account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk, identifier)` tuple. 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 identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
|
||||
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
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, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`,
|
||||
/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_identifier() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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, u128::MAX);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_header_round_trips() {
|
||||
let regular = PrivateAccountKind::Regular(42);
|
||||
let pda = PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: u128::MAX,
|
||||
};
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()),
|
||||
Some(regular)
|
||||
);
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()),
|
||||
Some(pda)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_unknown_discriminant_returns_none() {
|
||||
let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN];
|
||||
bytes[0] = 0xFF;
|
||||
assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_private_account_dispatches_correctly() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
);
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
341
lee/state_machine/core/src/program/tests.rs
Normal file
341
lee/state_machine/core/src/program/tests.rs
Normal file
@ -0,0 +1,341 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validity_window_unbounded_accepts_any_value() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_bounded_range_includes_from_excludes_to() {
|
||||
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(9));
|
||||
assert!(!w.is_valid_for(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_from_bound() {
|
||||
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_to_bound() {
|
||||
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(4));
|
||||
assert!(!w.is_valid_for(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_adjacent_bounds_are_invalid() {
|
||||
// [5, 5) is an empty range — from == to
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
|
||||
assert_eq!(w.start(), Some(3));
|
||||
assert_eq!(w.end(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_for_unbounded() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_empty_is_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_inverted_is_invalid() {
|
||||
let from = 10_u64;
|
||||
let to = 5_u64;
|
||||
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_range() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(10_u64..100)
|
||||
.unwrap();
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_from() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(10_u64..);
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_to() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(..100_u64);
|
||||
assert_eq!(output.block_validity_window.start(), None);
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_empty_range_fails() {
|
||||
let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(5_u64..5);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_without_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.required_claim().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_account_getter() {
|
||||
let mut account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let mut account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk, identifier)` tuple. 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 vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
5, 87, 128, 244, 206, 244, 65, 130, 178, 88, 225, 183, 0, 159, 201, 201, 212, 206, 6, 156,
|
||||
13, 55, 32, 139, 91, 222, 209, 83, 172, 148, 123, 179,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier),
|
||||
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]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a, &vpk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, &vpk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk, &vpk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, &vpk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk, &vpk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, &vpk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`,
|
||||
/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_identifier() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_header_round_trips() {
|
||||
let regular = PrivateAccountKind::Regular(42);
|
||||
let pda = PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: u128::MAX,
|
||||
};
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()),
|
||||
Some(regular)
|
||||
);
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()),
|
||||
Some(pda)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_unknown_discriminant_returns_none() {
|
||||
let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN];
|
||||
bytes[0] = 0xFF;
|
||||
assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_private_account_dispatches_correctly() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, &vpk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&vpk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier),
|
||||
);
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
@ -164,398 +164,4 @@ const fn prev_power_of_two(x: usize) -> usize {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl MerkleTree {
|
||||
pub fn new(values: &[Value]) -> Self {
|
||||
let mut this = Self::with_capacity(values.len());
|
||||
for value in values.iter().copied() {
|
||||
this.insert(value);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_merkle_tree() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
let expected_root =
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_0() {
|
||||
let values = [[0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert_eq!(tree.root(), hash_value(&[0; 32]));
|
||||
assert_eq!(tree.capacity, 1);
|
||||
assert_eq!(tree.length, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_4() {
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499");
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 8);
|
||||
assert_eq!(tree.length, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_5() {
|
||||
let values = [
|
||||
[11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32],
|
||||
[13; 32], [15; 32], [11; 32],
|
||||
];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 16);
|
||||
assert_eq!(tree.length, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_6() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_4() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 7);
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}");
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}");
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_5() {
|
||||
let tree = MerkleTree::with_capacity(5);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 15);
|
||||
for i in 7..15 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]);
|
||||
}
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]);
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_6() {
|
||||
let mut tree = MerkleTree::with_capacity(100);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_7() {
|
||||
let mut tree = MerkleTree::with_capacity(599);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_8() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_1() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_2() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_3() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
tree.insert(values[0]);
|
||||
tree.insert(values[1]);
|
||||
tree.insert(values[2]);
|
||||
tree.insert(values[3]);
|
||||
tree.insert(values[4]);
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
// Reference implementation
|
||||
fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool {
|
||||
let mut result = hash_value(value);
|
||||
let mut level_index = index;
|
||||
for node in path {
|
||||
let is_left_child = level_index & 1 == 0;
|
||||
if is_left_child {
|
||||
result = hash_two(&result, node);
|
||||
} else {
|
||||
result = hash_two(node, &result);
|
||||
}
|
||||
level_index >>= 1;
|
||||
}
|
||||
&result == root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"),
|
||||
hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(2).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"),
|
||||
hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(0).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"),
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(4).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_4() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert!(tree.get_authentication_path_for(5).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_5() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let index = 4;
|
||||
let value = values[index];
|
||||
let path = tree.get_authentication_path_for(index).unwrap();
|
||||
assert!(verify_authentication_path(
|
||||
&value,
|
||||
index,
|
||||
&path,
|
||||
&tree.root()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_with_63_insertions() {
|
||||
let values = [
|
||||
hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"),
|
||||
hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"),
|
||||
hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"),
|
||||
hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"),
|
||||
hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"),
|
||||
hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"),
|
||||
hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"),
|
||||
hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"),
|
||||
hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"),
|
||||
hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"),
|
||||
hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"),
|
||||
hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"),
|
||||
hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"),
|
||||
hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"),
|
||||
hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"),
|
||||
hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"),
|
||||
hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"),
|
||||
hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"),
|
||||
hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"),
|
||||
hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"),
|
||||
hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"),
|
||||
hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"),
|
||||
hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"),
|
||||
hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"),
|
||||
hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"),
|
||||
hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"),
|
||||
hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"),
|
||||
hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"),
|
||||
hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"),
|
||||
hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"),
|
||||
hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"),
|
||||
hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"),
|
||||
hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"),
|
||||
hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"),
|
||||
hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"),
|
||||
hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"),
|
||||
hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"),
|
||||
hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"),
|
||||
hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"),
|
||||
hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"),
|
||||
hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"),
|
||||
hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"),
|
||||
hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"),
|
||||
hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"),
|
||||
hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"),
|
||||
hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"),
|
||||
hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"),
|
||||
hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"),
|
||||
hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"),
|
||||
hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"),
|
||||
hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"),
|
||||
hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"),
|
||||
hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"),
|
||||
hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"),
|
||||
hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"),
|
||||
hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"),
|
||||
hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"),
|
||||
hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"),
|
||||
hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"),
|
||||
hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"),
|
||||
hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"),
|
||||
hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"),
|
||||
hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"),
|
||||
];
|
||||
|
||||
let expected_root =
|
||||
hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0");
|
||||
|
||||
let mut tree_less_capacity = MerkleTree::with_capacity(1);
|
||||
let mut tree_exact_capacity = MerkleTree::with_capacity(64);
|
||||
let mut tree_more_capacity = MerkleTree::with_capacity(128);
|
||||
|
||||
for value in &values {
|
||||
tree_less_capacity.insert(*value);
|
||||
tree_exact_capacity.insert(*value);
|
||||
tree_more_capacity.insert(*value);
|
||||
}
|
||||
|
||||
assert_eq!(tree_more_capacity.root(), expected_root);
|
||||
assert_eq!(tree_less_capacity.root(), expected_root);
|
||||
assert_eq!(tree_exact_capacity.root(), expected_root);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
mod tests;
|
||||
|
||||
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
@ -0,0 +1,380 @@
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl MerkleTree {
|
||||
pub fn new(values: &[Value]) -> Self {
|
||||
let mut this = Self::with_capacity(values.len());
|
||||
for value in values.iter().copied() {
|
||||
this.insert(value);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_merkle_tree() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
let expected_root = hex!("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_0() {
|
||||
let values = [[0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert_eq!(tree.root(), hash_value(&[0; 32]));
|
||||
assert_eq!(tree.capacity, 1);
|
||||
assert_eq!(tree.length, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_4() {
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499");
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 8);
|
||||
assert_eq!(tree.length, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_5() {
|
||||
let values = [
|
||||
[11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32], [13; 32],
|
||||
[15; 32], [11; 32],
|
||||
];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 16);
|
||||
assert_eq!(tree.length, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_6() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_4() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 7);
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}");
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}");
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_5() {
|
||||
let tree = MerkleTree::with_capacity(5);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 15);
|
||||
for i in 7..15 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]);
|
||||
}
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]);
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_6() {
|
||||
let mut tree = MerkleTree::with_capacity(100);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
|
||||
let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_7() {
|
||||
let mut tree = MerkleTree::with_capacity(599);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_8() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_1() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_2() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_3() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
tree.insert(values[0]);
|
||||
tree.insert(values[1]);
|
||||
tree.insert(values[2]);
|
||||
tree.insert(values[3]);
|
||||
tree.insert(values[4]);
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
// Reference implementation
|
||||
fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool {
|
||||
let mut result = hash_value(value);
|
||||
let mut level_index = index;
|
||||
for node in path {
|
||||
let is_left_child = level_index & 1 == 0;
|
||||
if is_left_child {
|
||||
result = hash_two(&result, node);
|
||||
} else {
|
||||
result = hash_two(node, &result);
|
||||
}
|
||||
level_index >>= 1;
|
||||
}
|
||||
&result == root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"),
|
||||
hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(2).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"),
|
||||
hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(0).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"),
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(4).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_4() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert!(tree.get_authentication_path_for(5).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_5() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let index = 4;
|
||||
let value = values[index];
|
||||
let path = tree.get_authentication_path_for(index).unwrap();
|
||||
assert!(verify_authentication_path(
|
||||
&value,
|
||||
index,
|
||||
&path,
|
||||
&tree.root()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_with_63_insertions() {
|
||||
let values = [
|
||||
hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"),
|
||||
hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"),
|
||||
hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"),
|
||||
hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"),
|
||||
hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"),
|
||||
hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"),
|
||||
hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"),
|
||||
hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"),
|
||||
hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"),
|
||||
hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"),
|
||||
hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"),
|
||||
hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"),
|
||||
hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"),
|
||||
hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"),
|
||||
hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"),
|
||||
hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"),
|
||||
hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"),
|
||||
hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"),
|
||||
hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"),
|
||||
hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"),
|
||||
hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"),
|
||||
hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"),
|
||||
hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"),
|
||||
hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"),
|
||||
hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"),
|
||||
hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"),
|
||||
hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"),
|
||||
hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"),
|
||||
hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"),
|
||||
hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"),
|
||||
hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"),
|
||||
hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"),
|
||||
hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"),
|
||||
hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"),
|
||||
hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"),
|
||||
hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"),
|
||||
hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"),
|
||||
hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"),
|
||||
hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"),
|
||||
hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"),
|
||||
hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"),
|
||||
hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"),
|
||||
hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"),
|
||||
hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"),
|
||||
hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"),
|
||||
hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"),
|
||||
hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"),
|
||||
hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"),
|
||||
hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"),
|
||||
hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"),
|
||||
hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"),
|
||||
hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"),
|
||||
hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"),
|
||||
hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"),
|
||||
hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"),
|
||||
hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"),
|
||||
hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"),
|
||||
hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"),
|
||||
hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"),
|
||||
hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"),
|
||||
hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"),
|
||||
hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"),
|
||||
hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"),
|
||||
];
|
||||
|
||||
let expected_root = hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0");
|
||||
|
||||
let mut tree_less_capacity = MerkleTree::with_capacity(1);
|
||||
let mut tree_exact_capacity = MerkleTree::with_capacity(64);
|
||||
let mut tree_more_capacity = MerkleTree::with_capacity(128);
|
||||
|
||||
for value in &values {
|
||||
tree_less_capacity.insert(*value);
|
||||
tree_exact_capacity.insert(*value);
|
||||
tree_more_capacity.insert(*value);
|
||||
}
|
||||
|
||||
assert_eq!(tree_more_capacity.root(), expected_root);
|
||||
assert_eq!(tree_less_capacity.root(), expected_root);
|
||||
assert_eq!(tree_exact_capacity.root(), expected_root);
|
||||
}
|
||||
@ -1,906 +0,0 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
account::AccountWithMetadata,
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
/// Proof of the privacy preserving execution circuit.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Proof(pub(crate) Vec<u8>);
|
||||
|
||||
impl Proof {
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_inner(inner: Vec<u8>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool {
|
||||
let Ok(inner) = borsh::from_slice::<InnerReceipt>(&self.0) else {
|
||||
return false;
|
||||
};
|
||||
let receipt = Receipt::new(inner, circuit_output.to_bytes());
|
||||
receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
pub dependencies: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl ProgramWithDependencies {
|
||||
#[must_use]
|
||||
pub const fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
|
||||
Self {
|
||||
program,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Program> for ProgramWithDependencies {
|
||||
fn from(program: Program) -> Self {
|
||||
Self::new(program, HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a proof of the execution of a LEE program inside the privacy preserving execution
|
||||
/// circuit.
|
||||
pub fn execute_and_prove(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_data: InstructionData,
|
||||
account_identities: Vec<InputAccountIdentity>,
|
||||
program_with_dependencies: &ProgramWithDependencies,
|
||||
) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> {
|
||||
let ProgramWithDependencies {
|
||||
program: initial_program,
|
||||
dependencies,
|
||||
} = program_with_dependencies;
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
let mut program_outputs = Vec::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: initial_program.id(),
|
||||
instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(LeeError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
|
||||
let program_output: ProgramOutput = inner_receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
// TODO: remove clone
|
||||
program_outputs.push(program_output.clone());
|
||||
|
||||
// Prove circuit.
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
let circuit_input = PrivacyPreservingCircuitInput {
|
||||
program_outputs,
|
||||
account_identities,
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
let env = env_builder.build().unwrap();
|
||||
let prover = default_prover();
|
||||
let opts = ProverOpts::succinct();
|
||||
let prove_info = prover
|
||||
.prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts)
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?);
|
||||
|
||||
let circuit_output: PrivacyPreservingCircuitOutput = prove_info
|
||||
.receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
Ok((circuit_output, proof))
|
||||
}
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<Receipt, LeeError> {
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
Program::write_inputs(
|
||||
program.id(),
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
&mut env_builder,
|
||||
)?;
|
||||
let env = env_builder.build().unwrap();
|
||||
|
||||
// Prove the program
|
||||
let prover = default_prover();
|
||||
Ok(prover
|
||||
.prove(env, program.elf())
|
||||
.map_err(|e| LeeError::ProgramProveFailed(e.to_string()))?
|
||||
.receipt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
privacy_preserving_transaction::circuit::execute_and_prove,
|
||||
program::Program,
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
fn decrypt_kind(
|
||||
output: &PrivacyPreservingCircuitOutput,
|
||||
ssk: &SharedSecretKey,
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
kind
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_inner_roundtrip() {
|
||||
// `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches
|
||||
// mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`,
|
||||
// and of `from_inner` discarding its argument.
|
||||
let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF];
|
||||
assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes);
|
||||
assert!(Proof::from_inner(vec![]).into_inner().is_empty());
|
||||
assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() {
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let expected_sender_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: Nonce::default(),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_sender_pre = sender.clone();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
let [sender_post] = output.public_post_states.try_into().unwrap();
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
program_owner: program.id(),
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), 0),
|
||||
);
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let mut commitment_set = CommitmentSet::with_capacity(2);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment_sender));
|
||||
let expected_new_nullifiers = vec![
|
||||
(
|
||||
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
|
||||
commitment_set.digest(),
|
||||
),
|
||||
(
|
||||
Nullifier::for_account_initialization(&recipient_account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
),
|
||||
];
|
||||
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let expected_private_account_1 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_private_account_2 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_new_commitments = vec![
|
||||
Commitment::new(&sender_account_id, &expected_private_account_1),
|
||||
Commitment::new(&recipient_account_id, &expected_private_account_2),
|
||||
];
|
||||
|
||||
let shared_secret_1 =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let shared_secret_2 =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret_1,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: commitment_set
|
||||
.get_proof_for(&commitment_sender)
|
||||
.expect("sender's commitment must be in the set"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret_2,
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
assert_eq!(output.new_commitments, expected_new_commitments);
|
||||
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&account_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller();
|
||||
let validity_window = crate::test_methods::validity_window();
|
||||
|
||||
let instruction = Program::serialize_instruction((
|
||||
Some(1_u64),
|
||||
Some(4_u64),
|
||||
validity_window.id(),
|
||||
Some(4_u64),
|
||||
Some(7_u64),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
validity_window_chain_caller,
|
||||
[(validity_window.id(), validity_window)].into(),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let (output, _proof) = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier,
|
||||
seed: None,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &shared_secret, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// PDA init: initializes a new PDA under `simple_balance_transfer`'s ownership.
|
||||
/// The `simple_transfer_proxy` program chains to `simple_balance_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the private PDA binding.
|
||||
#[test]
|
||||
fn private_pda_init() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Recipient (public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses the standard unauthorized private account path and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_simple_transfer() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
// Recipient: shared private account (new, unauthorized)
|
||||
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&shared_npk,
|
||||
&shared_keys.vpk(),
|
||||
),
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![recipient],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_update_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let account = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment));
|
||||
|
||||
let sender = AccountWithMetadata::new(account, true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program.clone(),
|
||||
[(simple_transfer_id, simple_transfer)].into(),
|
||||
);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
account::AccountWithMetadata,
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
/// Proof of the privacy preserving execution circuit.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Proof(pub(crate) Vec<u8>);
|
||||
|
||||
impl Proof {
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_inner(inner: Vec<u8>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool {
|
||||
let Ok(inner) = borsh::from_slice::<InnerReceipt>(&self.0) else {
|
||||
return false;
|
||||
};
|
||||
let receipt = Receipt::new(inner, circuit_output.to_bytes());
|
||||
receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
pub dependencies: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl ProgramWithDependencies {
|
||||
#[must_use]
|
||||
pub const fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
|
||||
Self {
|
||||
program,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Program> for ProgramWithDependencies {
|
||||
fn from(program: Program) -> Self {
|
||||
Self::new(program, HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a proof of the execution of a LEE program inside the privacy preserving execution
|
||||
/// circuit.
|
||||
pub fn execute_and_prove(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_data: InstructionData,
|
||||
account_identities: Vec<InputAccountIdentity>,
|
||||
program_with_dependencies: &ProgramWithDependencies,
|
||||
) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> {
|
||||
let ProgramWithDependencies {
|
||||
program: initial_program,
|
||||
dependencies,
|
||||
} = program_with_dependencies;
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
let mut program_outputs = Vec::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: initial_program.id(),
|
||||
instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(LeeError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
|
||||
let program_output: ProgramOutput = inner_receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
// TODO: remove clone
|
||||
program_outputs.push(program_output.clone());
|
||||
|
||||
// Prove circuit.
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
let circuit_input = PrivacyPreservingCircuitInput {
|
||||
program_outputs,
|
||||
account_identities,
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
let env = env_builder.build().unwrap();
|
||||
let prover = default_prover();
|
||||
let opts = ProverOpts::succinct();
|
||||
let prove_info = prover
|
||||
.prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts)
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?);
|
||||
|
||||
let circuit_output: PrivacyPreservingCircuitOutput = prove_info
|
||||
.receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
Ok((circuit_output, proof))
|
||||
}
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<Receipt, LeeError> {
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
Program::write_inputs(
|
||||
program.id(),
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
&mut env_builder,
|
||||
)?;
|
||||
let env = env_builder.build().unwrap();
|
||||
|
||||
// Prove the program
|
||||
let prover = default_prover();
|
||||
Ok(prover
|
||||
.prove(env, program.elf())
|
||||
.map_err(|e| LeeError::ProgramProveFailed(e.to_string()))?
|
||||
.receipt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -0,0 +1,722 @@
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, EphemeralSecretKey, Nullifier,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
privacy_preserving_transaction::circuit::execute_and_prove,
|
||||
program::Program,
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
fn decrypt_kind(
|
||||
output: &PrivacyPreservingCircuitOutput,
|
||||
ssk: &SharedSecretKey,
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
kind
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_inner_roundtrip() {
|
||||
// `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches
|
||||
// mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`,
|
||||
// and of `from_inner` discarding its argument.
|
||||
let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF];
|
||||
assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes);
|
||||
assert!(Proof::from_inner(vec![]).into_inner().is_empty());
|
||||
assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() {
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let recipient_account_id =
|
||||
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let expected_sender_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: Nonce::default(),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_sender_pre = sender.clone();
|
||||
|
||||
let init_nonce = Nonce::private_account_nonce_init(&recipient_account_id);
|
||||
let esk = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce);
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
let [sender_post] = output.public_post_states.try_into().unwrap();
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
program_owner: program.id(),
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
|
||||
|
||||
let recipient_account_id =
|
||||
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let mut commitment_set = CommitmentSet::with_capacity(2);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment_sender));
|
||||
let expected_new_nullifiers = vec![
|
||||
(
|
||||
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
|
||||
commitment_set.digest(),
|
||||
),
|
||||
(
|
||||
Nullifier::for_account_initialization(&recipient_account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
),
|
||||
];
|
||||
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let expected_private_account_1 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_private_account_2 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_new_commitments = vec![
|
||||
Commitment::new(&sender_account_id, &expected_private_account_1),
|
||||
Commitment::new(&recipient_account_id, &expected_private_account_2),
|
||||
];
|
||||
|
||||
let esk_1 = EphemeralSecretKey::new(
|
||||
&sender_account_id,
|
||||
&[0; 32],
|
||||
&sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
);
|
||||
let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0;
|
||||
|
||||
let init_nonce_2 = Nonce::private_account_nonce_init(&recipient_account_id);
|
||||
let esk_2 = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce_2);
|
||||
let shared_secret_2 =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk_2).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: commitment_set
|
||||
.get_proof_for(&commitment_sender)
|
||||
.expect("sender's commitment must be in the set"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
assert_eq!(output.new_commitments, expected_new_commitments);
|
||||
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&account_keys.npk(), &account_keys.vpk(), 0),
|
||||
);
|
||||
|
||||
let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller();
|
||||
let validity_window = crate::test_methods::validity_window();
|
||||
|
||||
let instruction = Program::serialize_instruction((
|
||||
Some(1_u64),
|
||||
Some(4_u64),
|
||||
validity_window.id(),
|
||||
Some(4_u64),
|
||||
Some(7_u64),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
validity_window_chain_caller,
|
||||
[(validity_window.id(), validity_window)].into(),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: account_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: account_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let account_id =
|
||||
AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
|
||||
let init_nonce = Nonce::private_account_nonce_init(&account_id);
|
||||
let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &init_nonce);
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
|
||||
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let (output, _proof) = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &shared_secret, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// PDA init: initializes a new PDA under `simple_balance_transfer`'s ownership.
|
||||
/// The `simple_transfer_proxy` program chains to `simple_balance_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the private PDA binding.
|
||||
#[test]
|
||||
fn private_pda_init() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Recipient (public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses the standard unauthorized private account path and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_simple_transfer() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
// Recipient: shared private account (new, unauthorized)
|
||||
let shared_account_id = AccountId::from((&shared_npk, &shared_keys.vpk(), shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: shared_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: shared_npk,
|
||||
identifier: shared_identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
|
||||
let esk = EphemeralSecretKey::new(
|
||||
&account_id,
|
||||
&[0; 32],
|
||||
&Nonce::private_account_nonce_init(&account_id),
|
||||
);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
|
||||
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
|
||||
let esk = EphemeralSecretKey::new(
|
||||
&recipient_id,
|
||||
&[0; 32],
|
||||
&Nonce::private_account_nonce_init(&recipient_id),
|
||||
);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![recipient],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: keys.npk(),
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_update_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
|
||||
let esk = EphemeralSecretKey::new(
|
||||
&account_id,
|
||||
&[0; 32],
|
||||
&Nonce::default().private_account_nonce_increment(&keys.nsk),
|
||||
);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
|
||||
let account = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment));
|
||||
|
||||
let sender = AccountWithMetadata::new(account, true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
|
||||
let esk = EphemeralSecretKey::new(
|
||||
&pda_id,
|
||||
&[0; 32],
|
||||
&Nonce::default().private_account_nonce_increment(&keys.nsk),
|
||||
);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program.clone(),
|
||||
[(simple_transfer_id, simple_transfer)].into(),
|
||||
);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
identifier: 99,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
vpk: keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
@ -11,7 +11,7 @@ use crate::{AccountId, error::LeeError};
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(Clone, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
pub nonces: Vec<Nonce>,
|
||||
@ -84,13 +84,27 @@ impl Message {
|
||||
|
||||
Sha256::digest(bytes).into()
|
||||
}
|
||||
|
||||
/// Ensure that the commitments, nullifiers, and ciphertexts agree.
|
||||
pub fn validate_note_lengths(&self) -> Result<usize, LeeError> {
|
||||
let count = self.new_nullifiers.len();
|
||||
if self.new_commitments.len() != count || self.encrypted_private_post_states.len() != count
|
||||
{
|
||||
return Err(LeeError::InvalidInput(format!(
|
||||
"Note vectors disagree in length with {count} nullifiers, {} commitments, and {} ciphertexts",
|
||||
self.new_commitments.len(),
|
||||
self.encrypted_private_post_states.len(),
|
||||
)));
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use lee_core::{
|
||||
Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind,
|
||||
SharedSecretKey,
|
||||
Commitment, EncryptionScheme, EphemeralSecretKey, Nullifier, NullifierPublicKey,
|
||||
PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
@ -109,6 +123,7 @@ pub mod tests {
|
||||
|
||||
let npk1 = NullifierPublicKey::from(&nsk1);
|
||||
let npk2 = NullifierPublicKey::from(&nsk2);
|
||||
let vpk = ViewingPublicKey::from_seed(&[7; 32], &[8; 32]);
|
||||
|
||||
let public_account_ids = vec![AccountId::new([1; 32])];
|
||||
|
||||
@ -118,10 +133,10 @@ pub mod tests {
|
||||
|
||||
let encrypted_private_post_states = Vec::new();
|
||||
|
||||
let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, 0);
|
||||
let account_id2 = lee_core::account::AccountId::for_regular_private_account(&npk2, &vpk, 0);
|
||||
let new_commitments = vec![Commitment::new(&account_id2, &account2)];
|
||||
|
||||
let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, 0);
|
||||
let account_id1 = lee_core::account::AccountId::for_regular_private_account(&npk1, &vpk, 0);
|
||||
let old_commitment = Commitment::new(&account_id1, &account1);
|
||||
let new_nullifiers = vec![(
|
||||
Nullifier::for_account_update(&old_commitment, &nsk1),
|
||||
@ -140,6 +155,20 @@ pub mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_note_lengths_accepts_matching_and_rejects_mismatched() {
|
||||
assert_eq!(Message::default().validate_note_lengths().unwrap(), 0);
|
||||
|
||||
let mismatched = Message {
|
||||
new_commitments: vec![Commitment::new(
|
||||
&AccountId::new([0; 32]),
|
||||
&Account::default(),
|
||||
)],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(mismatched.validate_note_lengths().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_privacy_pinned() {
|
||||
let msg = Message {
|
||||
@ -197,9 +226,10 @@ pub mod tests {
|
||||
let npk = NullifierPublicKey::from(&[1; 32]);
|
||||
let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]);
|
||||
let account = Account::default();
|
||||
let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, 0);
|
||||
let account_id = lee_core::account::AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0);
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&vpk, &EphemeralSecretKey([0_u8; 32]));
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(0),
|
||||
|
||||
@ -111,42 +111,4 @@ impl Program {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::account::{Account, AccountId, AccountWithMetadata};
|
||||
|
||||
use crate::program::Program;
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let balance_to_move: u128 = 11_223_344_556_677;
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 77_665_544_332_211,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let recipient =
|
||||
AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
|
||||
|
||||
let expected_sender_post = Account {
|
||||
balance: 77_665_544_332_211 - balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let expected_recipient_post = Account {
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.unwrap();
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
assert_eq!(sender_post.account(), &expected_sender_post);
|
||||
assert_eq!(recipient_post.account(), &expected_recipient_post);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
36
lee/state_machine/src/program/tests.rs
Normal file
36
lee/state_machine/src/program/tests.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use lee_core::account::{Account, AccountId, AccountWithMetadata};
|
||||
|
||||
use crate::program::Program;
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let balance_to_move: u128 = 11_223_344_556_677;
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 77_665_544_332_211,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
|
||||
|
||||
let expected_sender_post = Account {
|
||||
balance: 77_665_544_332_211 - balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let expected_recipient_post = Account {
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.unwrap();
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
assert_eq!(sender_post.account(), &expected_sender_post);
|
||||
assert_eq!(recipient_post.account(), &expected_recipient_post);
|
||||
}
|
||||
@ -1,10 +1,18 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub(crate) bytecode: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Message {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Message")
|
||||
.field("bytecode", &format_args!("<{} bytes>", self.bytecode.len()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
#[must_use]
|
||||
pub const fn new(bytecode: Vec<u8>) -> Self {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
333
lee/state_machine/src/state/mod.rs
Normal file
333
lee/state_machine/src/state/mod.rs
Normal file
@ -0,0 +1,333 @@
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
|
||||
Timestamp,
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
merkle_tree::MerkleTree,
|
||||
privacy_preserving_transaction::PrivacyPreservingTransaction,
|
||||
program::Program,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
validated_state_diff::{StateDiff, ValidatedStateDiff},
|
||||
};
|
||||
|
||||
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||
|
||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
pub struct CommitmentSet {
|
||||
merkle_tree: MerkleTree,
|
||||
commitments: HashMap<Commitment, usize>,
|
||||
root_history: HashSet<CommitmentSetDigest>,
|
||||
}
|
||||
|
||||
impl CommitmentSet {
|
||||
pub(crate) fn digest(&self) -> CommitmentSetDigest {
|
||||
self.merkle_tree.root()
|
||||
}
|
||||
|
||||
/// Queries the `CommitmentSet` for a membership proof of commitment.
|
||||
pub fn get_proof_for(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
let index = *self.commitments.get(commitment)?;
|
||||
|
||||
self.merkle_tree
|
||||
.get_authentication_path_for(index)
|
||||
.map(|path| (index, path))
|
||||
}
|
||||
|
||||
/// Inserts a list of commitments to the `CommitmentSet`.
|
||||
pub(crate) fn extend(&mut self, commitments: &[Commitment]) {
|
||||
for commitment in commitments.iter().cloned() {
|
||||
let index = self.merkle_tree.insert(commitment.to_byte_array());
|
||||
self.commitments.insert(commitment, index);
|
||||
}
|
||||
self.root_history.insert(self.digest());
|
||||
}
|
||||
|
||||
fn contains(&self, commitment: &Commitment) -> bool {
|
||||
self.commitments.contains_key(commitment)
|
||||
}
|
||||
|
||||
/// Initializes an empty `CommitmentSet` with a given capacity.
|
||||
/// If the capacity is not a `power_of_two`, then capacity is taken
|
||||
/// to be the next `power_of_two`.
|
||||
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
merkle_tree: MerkleTree::with_capacity(capacity),
|
||||
commitments: HashMap::new(),
|
||||
root_history: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
#[derive(Clone)]
|
||||
struct NullifierSet(BTreeSet<Nullifier>);
|
||||
|
||||
impl NullifierSet {
|
||||
const fn new() -> Self {
|
||||
Self(BTreeSet::new())
|
||||
}
|
||||
|
||||
fn extend(&mut self, new_nullifiers: &[Nullifier]) {
|
||||
self.0.extend(new_nullifiers);
|
||||
}
|
||||
|
||||
fn contains(&self, nullifier: &Nullifier) -> bool {
|
||||
self.0.contains(nullifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorshSerialize for NullifierSet {
|
||||
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
|
||||
self.0.iter().collect::<Vec<_>>().serialize(writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorshDeserialize for NullifierSet {
|
||||
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
|
||||
let vec = Vec::<Nullifier>::deserialize_reader(reader)?;
|
||||
|
||||
let mut set = BTreeSet::new();
|
||||
for n in vec {
|
||||
if !set.insert(n) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"duplicate nullifier in NullifierSet",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(set))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
pub struct V03State {
|
||||
public_state: HashMap<AccountId, Account>,
|
||||
private_state: (CommitmentSet, NullifierSet),
|
||||
programs: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl Default for V03State {
|
||||
fn default() -> Self {
|
||||
let mut commitment_set = CommitmentSet::with_capacity(32);
|
||||
commitment_set.extend(&[DUMMY_COMMITMENT]);
|
||||
let nullifier_set = NullifierSet::new();
|
||||
let private_state = (commitment_set, nullifier_set);
|
||||
|
||||
Self {
|
||||
public_state: HashMap::default(),
|
||||
private_state,
|
||||
programs: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl V03State {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn commitment_root(&self) -> CommitmentSetDigest {
|
||||
self.private_state.0.digest()
|
||||
}
|
||||
|
||||
/// Initializes state with given public account balances leaving other account fields at their
|
||||
/// default values.
|
||||
#[must_use]
|
||||
pub fn with_public_account_balances(
|
||||
mut self,
|
||||
balances: impl IntoIterator<Item = (AccountId, u128)>,
|
||||
) -> Self {
|
||||
let public_accounts = balances.into_iter().map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
});
|
||||
self.public_state.extend(public_accounts);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given public accounts.
|
||||
#[must_use]
|
||||
pub fn with_public_accounts(
|
||||
mut self,
|
||||
public_accounts: impl IntoIterator<Item = (AccountId, Account)>,
|
||||
) -> Self {
|
||||
self.public_state.extend(public_accounts);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given private accounts.
|
||||
#[must_use]
|
||||
pub fn with_private_accounts(
|
||||
mut self,
|
||||
private_accounts: impl IntoIterator<Item = (Commitment, Nullifier)>,
|
||||
) -> Self {
|
||||
let (commitments, nullifiers): (Vec<Commitment>, Vec<Nullifier>) =
|
||||
private_accounts.into_iter().unzip();
|
||||
self.private_state.0.extend(&commitments);
|
||||
self.private_state.1.extend(&nullifiers);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given builtin programs.
|
||||
#[must_use]
|
||||
pub fn with_programs(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
|
||||
for program in programs {
|
||||
self.insert_program(program);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn insert_program(&mut self, program: Program) {
|
||||
self.programs.insert(program.id(), program);
|
||||
}
|
||||
|
||||
/// Seeds a single genesis account that is not produced by any transaction
|
||||
/// (e.g. the cross-zone inbox config or a bridge-lock holding). Lets the
|
||||
/// sequencer and indexer seed identical zone-specific state after building
|
||||
/// the shared initial state.
|
||||
pub fn insert_genesis_account(&mut self, account_id: AccountId, account: Account) {
|
||||
self.public_state.insert(account_id, account);
|
||||
}
|
||||
|
||||
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
|
||||
let StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
program,
|
||||
} = diff.into_state_diff();
|
||||
#[expect(
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "Iteration order doesn't matter here"
|
||||
)]
|
||||
for (account_id, account) in public_diff {
|
||||
*self.get_account_by_id_mut(account_id) = account;
|
||||
}
|
||||
for account_id in signer_account_ids {
|
||||
self.get_account_by_id_mut(account_id)
|
||||
.nonce
|
||||
.public_account_nonce_increment();
|
||||
}
|
||||
self.private_state.0.extend(&new_commitments);
|
||||
self.private_state.1.extend(&new_nullifiers);
|
||||
if let Some(program) = program {
|
||||
self.insert_program(program);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_public_transaction(
|
||||
&mut self,
|
||||
tx: &PublicTransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transition_from_privacy_preserving_transaction(
|
||||
&mut self,
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff =
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transition_from_program_deployment_transaction(
|
||||
&mut self,
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_account_by_id_mut(&mut self, account_id: AccountId) -> &mut Account {
|
||||
self.public_state.entry(account_id).or_default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_account_by_id(&self, account_id: AccountId) -> Account {
|
||||
self.public_state
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(Account::default)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
self.private_state.0.get_proof_for(commitment)
|
||||
}
|
||||
|
||||
pub(crate) const fn programs(&self) -> &HashMap<ProgramId, Program> {
|
||||
&self.programs
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn commitment_set_digest(&self) -> CommitmentSetDigest {
|
||||
self.private_state.0.digest()
|
||||
}
|
||||
|
||||
pub(crate) fn check_commitments_are_new(
|
||||
&self,
|
||||
new_commitments: &[Commitment],
|
||||
) -> Result<(), LeeError> {
|
||||
for commitment in new_commitments {
|
||||
if self.private_state.0.contains(commitment) {
|
||||
return Err(LeeError::InvalidInput("Commitment already seen".to_owned()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn check_nullifiers_are_valid(
|
||||
&self,
|
||||
new_nullifiers: &[(Nullifier, CommitmentSetDigest)],
|
||||
) -> Result<(), LeeError> {
|
||||
for (nullifier, digest) in new_nullifiers {
|
||||
if self.private_state.1.contains(nullifier) {
|
||||
return Err(LeeError::InvalidInput("Nullifier already seen".to_owned()));
|
||||
}
|
||||
if !self.private_state.0.root_history.contains(digest) {
|
||||
return Err(LeeError::InvalidInput(
|
||||
"Unrecognized commitment set digest".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
impl V03State {
|
||||
pub fn force_insert_account(&mut self, account_id: AccountId, account: Account) {
|
||||
self.public_state.insert(account_id, account);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
149
lee/state_machine/src/state/tests/authenticated_transfer.rs
Normal file
149
lee/state_machine/src/state/tests/authenticated_transfer.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_default_account_destination() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let from = account_id;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
let balance_to_move = 5;
|
||||
|
||||
let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(from).balance, 95);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 5);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_insuficient_balance() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(account_id, 100)])
|
||||
.with_test_programs();
|
||||
let from = account_id;
|
||||
let from_key = key;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let balance_to_move = 101;
|
||||
assert!(state.get_account_by_id(from).balance < balance_to_move);
|
||||
|
||||
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_))));
|
||||
assert_eq!(state.get_account_by_id(from).balance, 100);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 0);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(0));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_non_default_account_destination() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [
|
||||
(
|
||||
account_id1,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
account_id2,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 200,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let from = account_id2;
|
||||
let from_key = key2;
|
||||
let to = account_id1;
|
||||
let to_key = key1;
|
||||
assert_ne!(state.get_account_by_id(to), Account::default());
|
||||
let balance_to_move = 8;
|
||||
|
||||
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(from).balance, 192);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 108);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_sequence_of_authenticated_transfer_program_invocations() {
|
||||
let key1 = PrivateKey::try_new([8; 32]).unwrap();
|
||||
let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [(
|
||||
account_id1,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let key3 = PrivateKey::try_new([3; 32]).unwrap();
|
||||
let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3));
|
||||
let balance_to_move = 5;
|
||||
|
||||
let tx = transfer_transaction(
|
||||
account_id1,
|
||||
&key1,
|
||||
0,
|
||||
account_id2,
|
||||
&key2,
|
||||
0,
|
||||
balance_to_move,
|
||||
);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
let balance_to_move = 3;
|
||||
let tx = transfer_transaction(
|
||||
account_id2,
|
||||
&key2,
|
||||
1,
|
||||
account_id3,
|
||||
&key3,
|
||||
0,
|
||||
balance_to_move,
|
||||
);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id1).balance, 95);
|
||||
assert_eq!(state.get_account_by_id(account_id2).balance, 2);
|
||||
assert_eq!(state.get_account_by_id(account_id3).balance, 3);
|
||||
assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2));
|
||||
assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1));
|
||||
}
|
||||
116
lee/state_machine/src/state/tests/changer_claimer.rs
Normal file
116
lee/state_machine/src/state/tests/changer_claimer.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_changer_claimer_no_data_change_no_claim_succeeds() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::changer_claimer().id();
|
||||
// Don't change data (None) and don't claim (false)
|
||||
let instruction: (Option<Vec<u8>>, bool) = (None, false);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should succeed - no changes made, no claim needed
|
||||
assert!(result.is_ok());
|
||||
// Account should remain default/unclaimed
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_changer_claimer_data_change_no_claim_fails() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::changer_claimer().id();
|
||||
// Change data but don't claim (false) - should fail
|
||||
let new_data = vec![1, 2, 3, 4, 5];
|
||||
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim {
|
||||
account_id: err_account_id
|
||||
}
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_changer_claimer_no_data_change_no_claim_succeeds() {
|
||||
let program = crate::test_methods::changer_claimer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let private_account = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
true,
|
||||
(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
// Don't change data (None) and don't claim (false)
|
||||
let instruction: (Option<Vec<u8>>, bool) = (None, false);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (0, vec![]),
|
||||
identifier: 0,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
// Should succeed - no changes made, no claim needed
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_changer_claimer_data_change_no_claim_fails() {
|
||||
let program = crate::test_methods::changer_claimer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let private_account = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
true,
|
||||
(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
// Change data but don't claim (false) - should fail
|
||||
let new_data = vec![1, 2, 3, 4, 5];
|
||||
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (0, vec![]),
|
||||
identifier: 0,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
1136
lee/state_machine/src/state/tests/circuit.rs
Normal file
1136
lee/state_machine/src/state/tests/circuit.rs
Normal file
File diff suppressed because it is too large
Load Diff
607
lee/state_machine/src/state/tests/claiming.rs
Normal file
607
lee/state_machine/src/state/tests/claiming.rs
Normal file
@ -0,0 +1,607 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let from_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: amount,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![from, to],
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let recipient_post = state.get_account_by_id(to);
|
||||
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_public_account_claiming_fails() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_key = PrivateKey::try_new([9; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::InvalidProgramBehavior(_))));
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authorized_public_account_claiming_succeeds() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_key = PrivateKey::try_new([10; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![account_id],
|
||||
vec![Nonce(0)],
|
||||
0_u128,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(account_id),
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_chained_call() {
|
||||
let program = crate::test_methods::chain_caller();
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
2,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: amount * 2, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
// The `chain_caller` program calls the program twice
|
||||
assert_eq!(from_post.balance, initial_balance - 2 * amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_fails_if_chained_calls_exceeds_depth() {
|
||||
let program = crate::test_methods::chain_caller();
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 0;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
u32::try_from(MAX_NUMBER_CHAINED_CALLS).expect("MAX_NUMBER_CHAINED_CALLS fits in u32") + 1,
|
||||
None,
|
||||
);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::MaxChainedCallsDepthExceeded)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let pda_seed = PdaSeed::new([37; 32]);
|
||||
let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed);
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let amount: u128 = 58;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
1,
|
||||
Some(pda_seed),
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: amount, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism_within_chain_call() {
|
||||
// This test calls the authenticated transfer program through the chain_caller program.
|
||||
// The transfer is made from an initialized sender to an uninitialized recipient. And
|
||||
// it is expected that the recipient account is claimed by the authenticated transfer
|
||||
// program and not the chained_caller program.
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let from_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
|
||||
let expected_to_post = Account {
|
||||
// The expected program owner is the authenticated transfer program
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: amount,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// The transaction executes the chain_caller program, which internally calls the
|
||||
// authenticated_transfer program
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
1,
|
||||
None,
|
||||
);
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_public_account_claiming_fails_when_executed_privately() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_id = AccountId::new([11; 32]);
|
||||
let public_account = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authorized_public_account_claiming_succeeds_when_executed_privately() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let program_id = program.id();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: program_id,
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
|
||||
let mut state =
|
||||
V03State::new().with_private_accounts([(sender_commitment.clone(), sender_init_nullifier)]);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account,
|
||||
true,
|
||||
(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let recipient_account_id =
|
||||
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id);
|
||||
|
||||
let balance = 37;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk);
|
||||
assert!(state.private_state.1.contains(&nullifier));
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(recipient_account_id),
|
||||
Account {
|
||||
program_owner: program_id,
|
||||
balance,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(1; "single call")]
|
||||
#[test_case::test_case(2; "two calls")]
|
||||
fn private_chained_call(number_of_calls: u32) {
|
||||
// Arrange
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let simple_transfers = crate::test_methods::simple_balance_transfer();
|
||||
let from_keys = test_private_account_keys_1();
|
||||
let to_keys = test_private_account_keys_2();
|
||||
let initial_balance = 100;
|
||||
let from_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
(&from_keys.npk(), &from_keys.vpk(), 0),
|
||||
);
|
||||
let to_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
(&to_keys.npk(), &to_keys.vpk(), 0),
|
||||
);
|
||||
|
||||
let from_account_id =
|
||||
AccountId::for_regular_private_account(&from_keys.npk(), &from_keys.vpk(), 0);
|
||||
let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), &to_keys.vpk(), 0);
|
||||
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
|
||||
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
|
||||
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
|
||||
let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id);
|
||||
let mut state = V03State::new()
|
||||
.with_private_accounts([
|
||||
(from_commitment.clone(), from_init_nullifier),
|
||||
(to_commitment.clone(), to_init_nullifier),
|
||||
])
|
||||
.with_test_programs();
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
number_of_calls,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies);
|
||||
|
||||
let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk);
|
||||
let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk);
|
||||
|
||||
let from_expected_post = Account {
|
||||
balance: initial_balance - u128::from(number_of_calls) * amount,
|
||||
nonce: from_new_nonce,
|
||||
..from_account.account.clone()
|
||||
};
|
||||
let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post);
|
||||
|
||||
let to_expected_post = Account {
|
||||
balance: u128::from(number_of_calls) * amount,
|
||||
nonce: to_new_nonce,
|
||||
..to_account.account.clone()
|
||||
};
|
||||
let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post);
|
||||
|
||||
// Act
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![to_account, from_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: from_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: from_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&from_commitment)
|
||||
.expect("from's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: to_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: to_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&to_commitment)
|
||||
.expect("to's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let transaction = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&transaction, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert!(
|
||||
state
|
||||
.get_proof_for_commitment(&from_expected_commitment)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
.get_proof_for_commitment(&to_expected_commitment)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism_cannot_claim_initialied_accounts() {
|
||||
let claimer = crate::test_methods::claimer();
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let account_id = AccountId::new([2; 32]);
|
||||
|
||||
// Insert an account with non-default program owner
|
||||
state.force_insert_account(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id }
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
/// This test ensures that even if a malicious program tries to perform overflow of balances
|
||||
/// it will not be able to break the balance validation.
|
||||
#[test]
|
||||
fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() {
|
||||
let sender_key = PrivateKey::try_new([37; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let sender_init_balance: u128 = 10;
|
||||
|
||||
let recipient_key = PrivateKey::try_new([42; 32]).unwrap();
|
||||
let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key));
|
||||
let recipient_init_balance: u128 = 10;
|
||||
|
||||
let modified_transfer_id = crate::test_methods::modified_transfer_program().id();
|
||||
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts([
|
||||
(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: modified_transfer_id,
|
||||
balance: sender_init_balance,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
recipient_id,
|
||||
Account {
|
||||
program_owner: modified_transfer_id,
|
||||
balance: recipient_init_balance,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
])
|
||||
.with_test_programs();
|
||||
|
||||
let balance_to_move: u128 = 4;
|
||||
|
||||
let sender = AccountWithMetadata::new(state.get_account_by_id(sender_id), true, sender_id);
|
||||
|
||||
let sender_nonce = sender.account.nonce;
|
||||
|
||||
let _recipient =
|
||||
AccountWithMetadata::new(state.get_account_by_id(recipient_id), false, sender_id);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
modified_transfer_id,
|
||||
vec![sender_id, recipient_id],
|
||||
vec![sender_nonce],
|
||||
balance_to_move,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let res = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
let expected_total_balance_pre_states =
|
||||
WrappedBalanceSum::from_balances([sender_init_balance, recipient_init_balance].into_iter())
|
||||
.unwrap();
|
||||
let expected_total_balance_post_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
)
|
||||
)) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states
|
||||
));
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_id);
|
||||
let recipient_post = state.get_account_by_id(recipient_id);
|
||||
|
||||
let expected_sender_post = {
|
||||
let mut this = state.get_account_by_id(sender_id);
|
||||
this.balance = sender_init_balance;
|
||||
this.nonce = Nonce(0);
|
||||
this
|
||||
};
|
||||
|
||||
let expected_recipient_post = {
|
||||
let mut this = state.get_account_by_id(sender_id);
|
||||
this.balance = recipient_init_balance;
|
||||
this.nonce = Nonce(0);
|
||||
this
|
||||
};
|
||||
|
||||
assert_eq!(expected_sender_post, sender_post);
|
||||
assert_eq!(expected_recipient_post, recipient_post);
|
||||
}
|
||||
235
lee/state_machine/src/state/tests/flash_swap.rs
Normal file
235
lee/state_machine/src/state/tests/flash_swap.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flash_swap_successful() {
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
// Callback instruction: return funds
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: true,
|
||||
token_program_id: token.id(),
|
||||
amount: amount_out,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(result.is_ok(), "flash swap should succeed: {result:?}");
|
||||
|
||||
// Vault balance restored, receiver back to 0
|
||||
assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance);
|
||||
assert_eq!(state.get_account_by_id(receiver_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_callback_keeps_funds_rollback() {
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
// Callback instruction: do NOT return funds
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: false,
|
||||
token_program_id: token.id(),
|
||||
amount: amount_out,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Invariant check fails → entire tx rolls back
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"flash swap should fail when callback keeps funds"
|
||||
);
|
||||
|
||||
// State unchanged (rollback)
|
||||
assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance);
|
||||
assert_eq!(state.get_account_by_id(receiver_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_self_call_targets_correct_program() {
|
||||
// Zero-amount flash swap: the invariant self-call still runs and succeeds
|
||||
// because vault balance doesn't decrease.
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: true,
|
||||
token_program_id: token.id(),
|
||||
amount: 0,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out: 0,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"zero-amount flash swap should succeed: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_standalone_invariant_check_rejected() {
|
||||
// Calling InvariantCheck directly (not as a chained self-call) should fail
|
||||
// because caller_program_id will be None.
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
|
||||
let instruction = FlashSwapInstruction::InvariantCheck {
|
||||
min_vault_balance: 1000,
|
||||
};
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(initiator.id(), vec![vault_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"standalone InvariantCheck should be rejected (caller_program_id is None)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_self_program_id_rejected_in_public_execution() {
|
||||
let program = crate::test_methods::malicious_self_program_id();
|
||||
let acc_id = AccountId::new([99; 32]);
|
||||
let account = Account::default();
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(acc_id, account);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"program with wrong self_program_id in output should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_caller_program_id_rejected_in_public_execution() {
|
||||
let program = crate::test_methods::malicious_caller_program_id();
|
||||
let acc_id = AccountId::new([99; 32]);
|
||||
let account = Account::default();
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(acc_id, account);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"program with spoofed caller_program_id in output should be rejected"
|
||||
);
|
||||
}
|
||||
128
lee/state_machine/src/state/tests/genesis.rs
Normal file
128
lee/state_machine/src/state/tests/genesis.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let addr1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let expected_public_state = {
|
||||
let mut this = HashMap::new();
|
||||
this.insert(
|
||||
addr1,
|
||||
Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this.insert(
|
||||
addr2,
|
||||
Account {
|
||||
balance: 151,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this
|
||||
};
|
||||
let expected_builtin_programs = HashMap::new();
|
||||
|
||||
let state =
|
||||
V03State::new().with_public_account_balances([(addr1, 100_u128), (addr2, 151_u128)]);
|
||||
|
||||
assert_eq!(state.public_state, expected_public_state);
|
||||
assert_eq!(state.programs, expected_builtin_programs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_includes_nullifiers_for_private_accounts() {
|
||||
let keys1 = test_private_account_keys_1();
|
||||
let keys2 = test_private_account_keys_2();
|
||||
|
||||
let account = Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), &keys1.vpk(), 0);
|
||||
let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), &keys2.vpk(), 0);
|
||||
|
||||
let init_commitment1 = Commitment::new(&account_id1, &account);
|
||||
let init_commitment2 = Commitment::new(&account_id2, &account);
|
||||
let init_nullifier1 = Nullifier::for_account_initialization(&account_id1);
|
||||
let init_nullifier2 = Nullifier::for_account_initialization(&account_id2);
|
||||
|
||||
let initial_private_accounts = vec![
|
||||
(init_commitment1, init_nullifier1),
|
||||
(init_commitment2, init_nullifier2),
|
||||
];
|
||||
|
||||
let state = V03State::new().with_private_accounts(initial_private_accounts);
|
||||
|
||||
assert!(state.private_state.1.contains(&init_nullifier1));
|
||||
assert!(state.private_state.1.contains(&init_nullifier2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_program() {
|
||||
let mut state = V03State::new();
|
||||
let program_to_insert = crate::test_methods::simple_balance_transfer();
|
||||
let program_id = program_to_insert.id();
|
||||
assert!(!state.programs.contains_key(&program_id));
|
||||
|
||||
state.insert_program(program_to_insert);
|
||||
|
||||
assert!(state.programs.contains_key(&program_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_account_by_account_id_non_default_account() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let state = V03State::new().with_public_accounts(initial_data);
|
||||
let expected_account = &state.public_state[&account_id];
|
||||
|
||||
let account = state.get_account_by_id(account_id);
|
||||
|
||||
assert_eq!(&account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_account_by_account_id_default_account() {
|
||||
let addr2 = AccountId::new([0; 32]);
|
||||
let state = V03State::new();
|
||||
let expected_account = Account::default();
|
||||
|
||||
let account = state.get_account_by_id(addr2);
|
||||
|
||||
assert_eq!(account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_programs_getter() {
|
||||
let state = V03State::new();
|
||||
|
||||
let builtin_programs = state.programs();
|
||||
|
||||
assert_eq!(builtin_programs, &state.programs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_serialization_roundtrip() {
|
||||
let account_id_1 = AccountId::new([1; 32]);
|
||||
let account_id_2 = AccountId::new([2; 32]);
|
||||
let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)];
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let bytes = borsh::to_vec(&state).unwrap();
|
||||
let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap();
|
||||
assert_eq!(state, state_from_bytes);
|
||||
}
|
||||
425
lee/state_machine/src/state/tests/mod.rs
Normal file
425
lee/state_machine/src/state/tests/mod.rs
Normal file
@ -0,0 +1,425 @@
|
||||
#![expect(
|
||||
clippy::arithmetic_side_effects,
|
||||
clippy::shadow_unrelated,
|
||||
reason = "We don't care about it in tests"
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier,
|
||||
NullifierPublicKey, NullifierSecretKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{
|
||||
BlockValidityWindow, ExecutionValidationError, MAX_NUMBER_CHAINED_CALLS, PdaSeed,
|
||||
ProgramId, TimestampValidityWindow, WrappedBalanceSum,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PublicKey, PublicTransaction, V03State,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::ProgramWithDependencies, message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
program::Program,
|
||||
public_transaction,
|
||||
signature::PrivateKey,
|
||||
};
|
||||
|
||||
mod authenticated_transfer;
|
||||
mod changer_claimer;
|
||||
mod circuit;
|
||||
mod claiming;
|
||||
mod flash_swap;
|
||||
mod genesis;
|
||||
mod privacy_preserving;
|
||||
mod public_program_rules;
|
||||
mod validity_window;
|
||||
|
||||
impl V03State {
|
||||
/// Include test programs in the builtin programs map.
|
||||
#[must_use]
|
||||
pub fn with_test_programs(mut self) -> Self {
|
||||
self.insert_program(crate::test_methods::simple_balance_transfer());
|
||||
self.insert_program(crate::test_methods::nonce_changer());
|
||||
self.insert_program(crate::test_methods::extra_output());
|
||||
self.insert_program(crate::test_methods::missing_output());
|
||||
self.insert_program(crate::test_methods::program_owner_changer());
|
||||
self.insert_program(crate::test_methods::data_changer());
|
||||
self.insert_program(crate::test_methods::minter());
|
||||
self.insert_program(crate::test_methods::burner());
|
||||
self.insert_program(crate::test_methods::auth_asserting_noop());
|
||||
self.insert_program(crate::test_methods::private_pda_delegator());
|
||||
self.insert_program(crate::test_methods::pda_claimer());
|
||||
self.insert_program(crate::test_methods::two_pda_claimer());
|
||||
self.insert_program(crate::test_methods::noop());
|
||||
self.insert_program(crate::test_methods::chain_caller());
|
||||
self.insert_program(crate::test_methods::modified_transfer_program());
|
||||
self.insert_program(crate::test_methods::malicious_authorization_changer());
|
||||
self.insert_program(crate::test_methods::validity_window());
|
||||
self.insert_program(crate::test_methods::flash_swap_initiator());
|
||||
self.insert_program(crate::test_methods::flash_swap_callback());
|
||||
self.insert_program(crate::test_methods::malicious_self_program_id());
|
||||
self.insert_program(crate::test_methods::malicious_caller_program_id());
|
||||
self.insert_program(crate::test_methods::pda_spend_proxy());
|
||||
self.insert_program(crate::test_methods::claimer());
|
||||
self.insert_program(crate::test_methods::changer_claimer());
|
||||
self.insert_program(crate::test_methods::validity_window_chain_caller());
|
||||
self.insert_program(crate::test_methods::simple_transfer_proxy());
|
||||
self.insert_program(crate::test_methods::malicious_injector());
|
||||
self.insert_program(crate::test_methods::malicious_launderer());
|
||||
self.insert_program(crate::test_methods::modified_transfer_program());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_non_default_accounts_but_default_program_owners(mut self) -> Self {
|
||||
let account_with_default_values_except_balance = Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let account_with_default_values_except_nonce = Account {
|
||||
nonce: Nonce(37),
|
||||
..Account::default()
|
||||
};
|
||||
let account_with_default_values_except_data = Account {
|
||||
data: vec![0xca, 0xfe].try_into().unwrap(),
|
||||
..Account::default()
|
||||
};
|
||||
self.force_insert_account(
|
||||
AccountId::new([255; 32]),
|
||||
account_with_default_values_except_balance,
|
||||
);
|
||||
self.force_insert_account(
|
||||
AccountId::new([254; 32]),
|
||||
account_with_default_values_except_nonce,
|
||||
);
|
||||
self.force_insert_account(
|
||||
AccountId::new([253; 32]),
|
||||
account_with_default_values_except_data,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_account_owned_by_burner_program(mut self) -> Self {
|
||||
let account = Account {
|
||||
program_owner: crate::test_methods::burner().id(),
|
||||
balance: 100,
|
||||
..Default::default()
|
||||
};
|
||||
self.force_insert_account(AccountId::new([252; 32]), account);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0);
|
||||
let commitment = Commitment::new(&account_id, account);
|
||||
self.private_state.0.extend(&[commitment]);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPublicKeys {
|
||||
pub signing_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TestPublicKeys {
|
||||
pub fn account_id(&self) -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&self.signing_key))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPrivateKeys {
|
||||
pub nsk: NullifierSecretKey,
|
||||
pub d: [u8; 32],
|
||||
pub z: [u8; 32],
|
||||
}
|
||||
|
||||
impl TestPrivateKeys {
|
||||
pub fn npk(&self) -> NullifierPublicKey {
|
||||
NullifierPublicKey::from(&self.nsk)
|
||||
}
|
||||
|
||||
pub fn vpk(&self) -> ViewingPublicKey {
|
||||
ViewingPublicKey::from_seed(&self.d, &self.z)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flash Swap types (mirrors of guest types for host-side serialisation) ──
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct CallbackInstruction {
|
||||
return_funds: bool,
|
||||
token_program_id: ProgramId,
|
||||
amount: u128,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
enum FlashSwapInstruction {
|
||||
Initiate {
|
||||
token_program_id: ProgramId,
|
||||
callback_program_id: ProgramId,
|
||||
amount_out: u128,
|
||||
callback_instruction_data: Vec<u32>,
|
||||
},
|
||||
InvariantCheck {
|
||||
min_vault_balance: u128,
|
||||
},
|
||||
}
|
||||
|
||||
fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<AccountId, Account> {
|
||||
initial_data
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn transfer_transaction(
|
||||
from: AccountId,
|
||||
from_key: &PrivateKey,
|
||||
from_nonce: u128,
|
||||
to: AccountId,
|
||||
to_key: &PrivateKey,
|
||||
to_nonce: u128,
|
||||
balance: u128,
|
||||
) -> PublicTransaction {
|
||||
let account_ids = vec![from, to];
|
||||
let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)];
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn build_flash_swap_tx(
|
||||
initiator: &Program,
|
||||
vault_id: AccountId,
|
||||
receiver_id: AccountId,
|
||||
instruction: FlashSwapInstruction,
|
||||
) -> PublicTransaction {
|
||||
let message = public_transaction::Message::try_new(
|
||||
initiator.id(),
|
||||
vec![vault_id, receiver_id],
|
||||
vec![], // no signers — vault is PDA-authorised
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn test_public_account_keys_1() -> TestPublicKeys {
|
||||
TestPublicKeys {
|
||||
signing_key: PrivateKey::try_new([37; 32]).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_public_account_keys_2() -> TestPublicKeys {
|
||||
TestPublicKeys {
|
||||
signing_key: PrivateKey::try_new([38; 32]).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_private_account_keys_1() -> TestPrivateKeys {
|
||||
TestPrivateKeys {
|
||||
nsk: [13; 32],
|
||||
d: [31; 32],
|
||||
z: [32; 32],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_private_account_keys_2() -> TestPrivateKeys {
|
||||
TestPrivateKeys {
|
||||
nsk: [38; 32],
|
||||
d: [83; 32],
|
||||
z: [84; 32],
|
||||
}
|
||||
}
|
||||
|
||||
fn shielded_balance_transfer_for_tests(
|
||||
sender_keys: &TestPublicKeys,
|
||||
recipient_keys: &TestPrivateKeys,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let sender = AccountWithMetadata::new(
|
||||
state.get_account_by_id(sender_keys.account_id()),
|
||||
true,
|
||||
sender_keys.account_id(),
|
||||
);
|
||||
|
||||
let sender_nonce = sender.account.nonce;
|
||||
|
||||
let recipient = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
|
||||
);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender_keys.account_id()],
|
||||
vec![sender_nonce],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn private_balance_transfer_for_tests(
|
||||
sender_keys: &TestPrivateKeys,
|
||||
sender_private_account: &Account,
|
||||
recipient_keys: &TestPrivateKeys,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
true,
|
||||
(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
|
||||
);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn deshielded_balance_transfer_for_tests(
|
||||
sender_keys: &TestPrivateKeys,
|
||||
sender_private_account: &Account,
|
||||
recipient_account_id: &AccountId,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
true,
|
||||
(&sender_keys.npk(), &sender_keys.vpk(), 0),
|
||||
);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(*recipient_account_id),
|
||||
false,
|
||||
*recipient_account_id,
|
||||
);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: sender_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: Nonce(0xdead_beef),
|
||||
..Account::default()
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
let state = V03State::new().with_private_account(&sender_keys, &sender_private_account);
|
||||
let tx = private_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys,
|
||||
37,
|
||||
&state,
|
||||
);
|
||||
(state, tx)
|
||||
}
|
||||
539
lee/state_machine/src/state/tests/privacy_preserving.rs
Normal file
539
lee/state_machine/src/state/tests/privacy_preserving.rs
Normal file
@ -0,0 +1,539 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_shielded() {
|
||||
let sender_keys = test_public_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let mut state = V03State::new().with_public_accounts([(
|
||||
sender_keys.account_id(),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 200,
|
||||
..Account::default()
|
||||
},
|
||||
)]);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let tx =
|
||||
shielded_balance_transfer_for_tests(&sender_keys, &recipient_keys, balance_to_move, &state);
|
||||
|
||||
let expected_sender_post = {
|
||||
let mut this = state.get_account_by_id(sender_keys.account_id());
|
||||
this.balance -= balance_to_move;
|
||||
this.nonce.public_account_nonce_increment();
|
||||
this
|
||||
};
|
||||
|
||||
let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap();
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_keys.account_id());
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment));
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(sender_keys.account_id()).balance,
|
||||
200 - balance_to_move
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_private() {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
data: Data::default(),
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let mut state = V03State::new().with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let tx = private_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys,
|
||||
balance_to_move,
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let recipient_account_id =
|
||||
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
|
||||
let expected_new_commitment_1 = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
balance: sender_private_account.balance - balance_to_move,
|
||||
data: Data::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let expected_new_nullifier =
|
||||
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
|
||||
|
||||
let expected_new_commitment_2 = Commitment::new(
|
||||
&recipient_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let previous_public_state = state.public_state.clone();
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment_1));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment_2));
|
||||
assert!(!state.private_state.1.contains(&expected_new_nullifier));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.public_state, previous_public_state);
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment_1));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment_2));
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
}
|
||||
|
||||
/// After a valid fully-private tx is proven, tampering with a note's epk should
|
||||
/// make the shielding proof invalid.
|
||||
#[test]
|
||||
fn privacy_tampered_epk_is_rejected() {
|
||||
use crate::validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
let (state, mut tx) = valid_private_transfer_tx_and_state();
|
||||
|
||||
// Baseline: the untampered tx verifies
|
||||
assert!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(),
|
||||
"the unmodified private transfer must verify"
|
||||
);
|
||||
|
||||
// Flip a byte of the first note's epk
|
||||
tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0),
|
||||
Err(LeeError::InvalidPrivacyPreservingProof)
|
||||
),
|
||||
"a tampered epk must be rejected by proof verification"
|
||||
);
|
||||
}
|
||||
|
||||
/// After a valid fully-private tx is proven, tampering with a note's view tag should
|
||||
/// make the shielding proof invalid.
|
||||
#[test]
|
||||
fn privacy_tampered_view_tag_is_rejected() {
|
||||
use crate::validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
let (state, mut tx) = valid_private_transfer_tx_and_state();
|
||||
|
||||
// Baseline: the untampered tx verifies.
|
||||
assert!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(),
|
||||
"the unmodified private transfer must verify"
|
||||
);
|
||||
|
||||
// Flip the first note's view_tag
|
||||
tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0),
|
||||
Err(LeeError::InvalidPrivacyPreservingProof)
|
||||
),
|
||||
"a tampered view_tag must be rejected by proof verification"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_deshielded() {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
data: Data::default(),
|
||||
};
|
||||
let recipient_keys = test_public_account_keys_1();
|
||||
let recipient_initial_balance = 400;
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts([(
|
||||
recipient_keys.account_id(),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: recipient_initial_balance,
|
||||
..Account::default()
|
||||
},
|
||||
)])
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let expected_recipient_post = {
|
||||
let mut this = state.get_account_by_id(recipient_keys.account_id());
|
||||
this.balance += balance_to_move;
|
||||
this
|
||||
};
|
||||
|
||||
let tx = deshielded_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys.account_id(),
|
||||
balance_to_move,
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id =
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0);
|
||||
let expected_new_commitment = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
balance: sender_private_account.balance - balance_to_move,
|
||||
data: Data::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let expected_new_nullifier =
|
||||
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
|
||||
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment));
|
||||
assert!(!state.private_state.1.contains(&expected_new_nullifier));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let recipient_post = state.get_account_by_id(recipient_keys.account_id());
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment));
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
assert_eq!(
|
||||
state.get_account_by_id(recipient_keys.account_id()).balance,
|
||||
recipient_initial_balance + balance_to_move
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burner_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::burner();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minter_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::minter();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonce_changer_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::nonce_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::data_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(vec![0]).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::data_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let large_data: Vec<u8> =
|
||||
vec![
|
||||
0;
|
||||
usize::try_from(lee_core::account::data::DATA_MAX_LENGTH.as_u64())
|
||||
.expect("DATA_MAX_LENGTH fits in usize")
|
||||
+ 1
|
||||
];
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(large_data).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::ProgramProveFailed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_output_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::extra_output();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_output_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::missing_output();
|
||||
let public_account_1 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let public_account_2 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([1; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account_1, public_account_2],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_owner_changer_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::program_owner_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let public_account_1 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let public_account_2 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([1; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account_1, public_account_2],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
|
||||
// Arrange
|
||||
let malicious_program = crate::test_methods::malicious_authorization_changer();
|
||||
let simple_transfers = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_public_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let sender_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
balance: 100,
|
||||
..Default::default()
|
||||
},
|
||||
false,
|
||||
sender_keys.account_id(),
|
||||
);
|
||||
let recipient_account = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
true,
|
||||
(&recipient_keys.npk(), &recipient_keys.vpk(), 0),
|
||||
);
|
||||
|
||||
let recipient_account_id =
|
||||
AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0);
|
||||
let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account);
|
||||
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(
|
||||
sender_account.account_id,
|
||||
sender_account.account.balance,
|
||||
)]))
|
||||
.with_private_accounts([(recipient_commitment.clone(), recipient_init_nullifier)])
|
||||
.with_test_programs();
|
||||
|
||||
let balance_to_transfer = 10_u128;
|
||||
let instruction = (balance_to_transfer, simple_transfers.id());
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
|
||||
|
||||
// Act - execute the malicious program - this should fail during proving
|
||||
let result = execute_and_prove(
|
||||
vec![sender_account, recipient_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: recipient_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: recipient_keys.nsk,
|
||||
membership_proof: state
|
||||
.get_proof_for_commitment(&recipient_commitment)
|
||||
.expect("recipient's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
// Assert - should fail because the malicious program tries to manipulate is_authorized
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
325
lee/state_machine/src/state/tests/public_program_rules.rs
Normal file
325
lee/state_machine/src/state/tests/public_program_rules.rs
Normal file
@ -0,0 +1,325 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_nonces() {
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(account_id, 100)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![account_id];
|
||||
let program_id = crate::test_methods::nonce_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedNonce { account_id: err_account_id }
|
||||
)
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_output_accounts_exceed_inputs() {
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(AccountId::new([1; 32]), 0)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let program_id = crate::test_methods::extra_output().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 1 && post_state_length == 2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_with_missing_output_accounts() {
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(AccountId::new([1; 32]), 100)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])];
|
||||
let program_id = crate::test_methods::missing_output().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 2 && post_state_length == 1
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() {
|
||||
let initial_data = [(
|
||||
AccountId::new([1; 32]),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in the program owner
|
||||
// field
|
||||
assert_ne!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in balance field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_ne!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([254; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in nonce field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_ne!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([253; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in data field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_ne!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_transfers_balance_from_non_owned_account() {
|
||||
let sender_account_id = AccountId::new([1; 32]);
|
||||
let receiver_account_id = AccountId::new([2; 32]);
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(sender_account_id, 100)])
|
||||
.with_test_programs();
|
||||
let balance_to_move: u128 = 1;
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
assert_ne!(
|
||||
state.get_account_by_id(sender_account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![sender_account_id, receiver_account_id],
|
||||
vec![],
|
||||
balance_to_move,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id }
|
||||
))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_data_of_non_owned_account() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
let program_id = crate::test_methods::data_changer().id();
|
||||
|
||||
assert_ne!(state.get_account_by_id(account_id), Account::default());
|
||||
assert_ne!(
|
||||
state.get_account_by_id(account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0])
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id }
|
||||
))) if err_account_id == account_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_minting() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::minter().id();
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_burning() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_account_owned_by_burner_program();
|
||||
let program_id = crate::test_methods::burner().id();
|
||||
let account_id = AccountId::new([252; 32]);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let balance_to_burn: u128 = 1;
|
||||
assert!(state.get_account_by_id(account_id).balance > balance_to_burn);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], balance_to_burn)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into()
|
||||
));
|
||||
}
|
||||
237
lee/state_machine/src/state/tests/validity_window.rs
Normal file
237
lee/state_machine/src/state/tests/validity_window.rs
Normal file
@ -0,0 +1,237 @@
|
||||
use super::*;
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn validity_window_works_in_public_transactions(
|
||||
validity_window: (Option<BlockId>, Option<BlockId>),
|
||||
block_id: BlockId,
|
||||
) {
|
||||
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_public_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
let program_id = validity_window_program.id();
|
||||
let instruction = (
|
||||
block_validity_window,
|
||||
TimestampValidityWindow::new_unbounded(),
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, 0);
|
||||
let is_inside_validity_window =
|
||||
match (block_validity_window.start(), block_validity_window.end()) {
|
||||
(Some(s), Some(e)) => s <= block_id && block_id < e,
|
||||
(Some(s), None) => s <= block_id,
|
||||
(None, Some(e)) => block_id < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn timestamp_validity_window_works_in_public_transactions(
|
||||
validity_window: (Option<Timestamp>, Option<Timestamp>),
|
||||
timestamp: Timestamp,
|
||||
) {
|
||||
let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_public_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
let program_id = validity_window_program.id();
|
||||
let instruction = (
|
||||
BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window,
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_public_transaction(&tx, 1, timestamp);
|
||||
let is_inside_validity_window = match (
|
||||
timestamp_validity_window.start(),
|
||||
timestamp_validity_window.end(),
|
||||
) {
|
||||
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
|
||||
(Some(s), None) => s <= timestamp,
|
||||
(None, Some(e)) => timestamp < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn validity_window_works_in_privacy_preserving_transactions(
|
||||
validity_window: (Option<BlockId>, Option<BlockId>),
|
||||
block_id: BlockId,
|
||||
) {
|
||||
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
(&account_keys.npk(), &account_keys.vpk(), 0),
|
||||
);
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let instruction = (
|
||||
block_validity_window,
|
||||
TimestampValidityWindow::new_unbounded(),
|
||||
);
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: account_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: account_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&validity_window_program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0);
|
||||
let is_inside_validity_window =
|
||||
match (block_validity_window.start(), block_validity_window.end()) {
|
||||
(Some(s), Some(e)) => s <= block_id && block_id < e,
|
||||
(Some(s), None) => s <= block_id,
|
||||
(None, Some(e)) => block_id < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn timestamp_validity_window_works_in_privacy_preserving_transactions(
|
||||
validity_window: (Option<Timestamp>, Option<Timestamp>),
|
||||
timestamp: Timestamp,
|
||||
) {
|
||||
let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
(&account_keys.npk(), &account_keys.vpk(), 0),
|
||||
);
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let instruction = (
|
||||
BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window,
|
||||
);
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
vpk: account_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
npk: account_keys.npk(),
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&validity_window_program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp);
|
||||
let is_inside_validity_window = match (
|
||||
timestamp_validity_window.start(),
|
||||
timestamp_validity_window.end(),
|
||||
) {
|
||||
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
|
||||
(Some(s), None) => s <= timestamp,
|
||||
(None, Some(e)) => timestamp < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
521
lee/state_machine/src/validated_state_diff/mod.rs
Normal file
521
lee/state_machine/src/validated_state_diff/mod.rs
Normal file
@ -0,0 +1,521 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas,
|
||||
validate_execution,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
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, LeeError> {
|
||||
let signer_account_ids = authenticate_public_transaction_signers(tx, state)?;
|
||||
let message = tx.message();
|
||||
|
||||
ensure!(
|
||||
!message.account_ids.is_empty(),
|
||||
LeeError::InvalidInput("Public transaction must have at least one account".into())
|
||||
);
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
LeeError::InvalidInput("Duplicate account_ids found in message".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 initial_caller_data = CallerData {
|
||||
program_id: None,
|
||||
authorized_accounts: signer_account_ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
let mut chained_calls =
|
||||
VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_data)) = chained_calls.pop_front() {
|
||||
ensure!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
LeeError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// Check that the `program_id` corresponds to a deployed program
|
||||
let Some(program) = state.programs().get(&chained_call.program_id) else {
|
||||
return Err(LeeError::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_data.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_data.program_id, &chained_call.pda_seeds);
|
||||
|
||||
// Account is authorized if it is either in the caller's authorized accounts or in the
|
||||
// list of PDAs the caller has authorized.
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
authorized_pdas.contains(account_id)
|
||||
|| caller_data.authorized_accounts.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 the program output pre_states marked as authorized are indeed
|
||||
// authorized, and vice-versa.
|
||||
let is_indeed_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
!pre.is_authorized || is_indeed_authorized,
|
||||
InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id }
|
||||
);
|
||||
ensure!(
|
||||
pre.is_authorized || !is_indeed_authorized,
|
||||
InvalidProgramBehaviorError::AuthorizedAccountMarkedAsNotAuthorized {
|
||||
account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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_data.program_id,
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_data.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),
|
||||
LeeError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter_mut().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let pre = &program_output.pre_states[i];
|
||||
let account_id = pre.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!(
|
||||
pre.is_authorized,
|
||||
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());
|
||||
}
|
||||
|
||||
// Source from `program_output.pre_states`, not `chained_call.pre_states`:
|
||||
// the loop above already gates program_output's `is_authorized` via the
|
||||
// `!pre.is_authorized || is_indeed_authorized` check, while `chained_call.
|
||||
// pre_states` is caller-controlled and can be forged (audit-issue 91).
|
||||
//
|
||||
// Union with the caller's authorized set so that authorization is monotonically
|
||||
// growing: once an account is authorized at any point in the chain it remains
|
||||
// authorized for all subsequent calls.
|
||||
let authorized_accounts: HashSet<_> = caller_data
|
||||
.authorized_accounts
|
||||
.into_iter()
|
||||
.chain(
|
||||
program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.filter(|pre| pre.is_authorized)
|
||||
.map(|pre| pre.account_id),
|
||||
)
|
||||
.collect();
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((
|
||||
new_call,
|
||||
CallerData {
|
||||
program_id: Some(chained_call.program_id),
|
||||
authorized_accounts: authorized_accounts.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
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, LeeError> {
|
||||
let message = &tx.message;
|
||||
let witness_set = &tx.witness_set;
|
||||
|
||||
// 1. Commitments or nullifiers are non empty
|
||||
ensure!(
|
||||
!message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(),
|
||||
LeeError::InvalidInput(
|
||||
"Empty commitments and empty nullifiers found in message".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// 2. Check there are no duplicate account_ids in the public_account_ids list.
|
||||
ensure!(
|
||||
n_unique(&message.public_account_ids) == message.public_account_ids.len(),
|
||||
LeeError::InvalidInput("Duplicate account_ids found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate nullifiers in the new_nullifiers list
|
||||
ensure!(
|
||||
n_unique(
|
||||
&message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.map(|(n, _)| n)
|
||||
.collect::<Vec<_>>()
|
||||
) == message.new_nullifiers.len(),
|
||||
LeeError::InvalidInput("Duplicate nullifiers found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate commitments in the new_commitments list
|
||||
ensure!(
|
||||
n_unique(&message.new_commitments) == message.new_commitments.len(),
|
||||
LeeError::InvalidInput("Duplicate commitments found in message".into())
|
||||
);
|
||||
|
||||
// 3. Nonce checks and Valid signatures
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
LeeError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.signatures_are_valid_for(message),
|
||||
LeeError::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,
|
||||
LeeError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
message.block_validity_window.is_valid_for(block_id)
|
||||
&& message.timestamp_validity_window.is_valid_for(timestamp),
|
||||
LeeError::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, LeeError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(tx.message.bytecode.clone().into())?;
|
||||
if state.programs().contains_key(&program.id()) {
|
||||
return Err(LeeError::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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CallerData {
|
||||
program_id: Option<ProgramId>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
fn authenticate_public_transaction_signers(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Vec<AccountId>, LeeError> {
|
||||
let message = tx.message();
|
||||
let witness_set = tx.witness_set();
|
||||
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
LeeError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
LeeError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
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,
|
||||
LeeError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
Ok(signer_account_ids)
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), LeeError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
encrypted_private_post_states: message.encrypted_private_post_states.clone(),
|
||||
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(LeeError::InvalidPrivacyPreservingProof)
|
||||
}
|
||||
|
||||
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
|
||||
let set: HashSet<&T> = data.iter().collect();
|
||||
set.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
526
lee/state_machine/src/validated_state_diff/tests.rs
Normal file
526
lee/state_machine/src/validated_state_diff/tests.rs
Normal file
@ -0,0 +1,526 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::account::{Account, AccountId, Nonce};
|
||||
|
||||
use crate::{
|
||||
PrivateKey, PublicKey, V03State,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<AccountId, Account> {
|
||||
initial_data
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_diff_reflects_a_successful_transfer() {
|
||||
// A successful native transfer must record the debited sender in
|
||||
// `public_diff()`. Catches the mutation that replaces `public_diff` with
|
||||
// `HashMap::new()` (which would hide every account change).
|
||||
let from_key = PrivateKey::try_new([1_u8; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let to_key = PrivateKey::try_new([2_u8; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(from, 100)]))
|
||||
.with_programs(std::iter::once(
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
));
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
let message =
|
||||
Message::try_new(program_id, vec![from, to], vec![Nonce(0), Nonce(0)], 5_u128).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = crate::PublicTransaction::new(message, witness_set);
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("a valid native transfer must validate");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
assert!(
|
||||
public_diff.contains_key(&from),
|
||||
"public_diff must contain the debited sender",
|
||||
);
|
||||
assert_eq!(
|
||||
public_diff[&from].balance, 95,
|
||||
"sender balance in the diff must reflect the debit",
|
||||
);
|
||||
}
|
||||
|
||||
/// Privacy-path version of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the victim's balance is left untouched.
|
||||
///
|
||||
/// `execute_and_prove` succeeds because each inner receipt is individually valid and the
|
||||
/// outer circuit faithfully commits whatever the attacker's program output says, including
|
||||
/// `victim(is_authorized=true)`. The circuit has no access to chain state and cannot know
|
||||
/// the victim never signed.
|
||||
///
|
||||
/// The host-side validator is what catches the attack: it independently reconstructs
|
||||
/// `public_pre_states` from chain state using `signer_account_ids.contains(victim_id) = false`,
|
||||
/// so it expects `victim(is_authorized=false)`. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
state::{CommitmentSet, tests::test_private_account_keys_1},
|
||||
};
|
||||
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
// Attacker controls a private account.
|
||||
let attacker_keys = test_private_account_keys_1();
|
||||
let attacker_id =
|
||||
AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0);
|
||||
|
||||
let victim_id = AccountId::new([20_u8; 32]);
|
||||
let recipient_id = AccountId::new([42_u8; 32]);
|
||||
let victim_balance = 5_000_u128;
|
||||
|
||||
// genesis sets program_owner = simple_balance_transfer_program.id() on all accounts.
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[
|
||||
(victim_id, victim_balance),
|
||||
(recipient_id, 0),
|
||||
]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Build attacker's private account and its local commitment tree.
|
||||
let attacker_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let attacker_commitment = Commitment::new(&attacker_id, &attacker_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&attacker_commitment));
|
||||
let membership_proof = commitment_set
|
||||
.get_proof_for(&attacker_commitment)
|
||||
.expect("attacker commitment must be in the set");
|
||||
|
||||
let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id);
|
||||
|
||||
let victim_account = state.get_account_by_id(victim_id);
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_account.balance,
|
||||
victim_account.nonce.0,
|
||||
victim_account.program_owner,
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
|
||||
let p2 = crate::test_methods::malicious_launderer();
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
// [0] attacker — first seen in P1's program_output.pre_states
|
||||
// [1] victim — first seen in simple_balance_transfer's program_output.pre_states
|
||||
// [2] recipient — first seen in simple_balance_transfer's program_output.pre_states
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: attacker_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public, // victim
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: all inner receipts are valid.
|
||||
// The outer circuit commits victim(is_authorized=true) to its journal.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack privacy transaction should be rejected with InvalidPrivacyPreservingProof"
|
||||
);
|
||||
assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Private-victim variant of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the recipient's balance remains zero.
|
||||
///
|
||||
/// After the circuit's Vacant branch accepts the injected `victim(is_authorized=true)`
|
||||
/// verbatim, the attacker must choose how to declare the victim in `account_identities`.
|
||||
/// There are two routes, both closed:
|
||||
///
|
||||
/// - **mask=1 (`PrivateAuthorizedUpdate`)**: the circuit derives `account_id =
|
||||
/// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches
|
||||
/// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker
|
||||
/// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced.
|
||||
///
|
||||
/// - **mask=0 (`Public`)**: the circuit places the account in `public_pre_states` and
|
||||
/// `execute_and_prove` succeeds. The host-side validator then reconstructs `public_pre_states`
|
||||
/// from chain state; `state.get_account_by_id(victim_id)` returns the default account (balance=0)
|
||||
/// because the victim has no public state entry. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied. This test exercises this route.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
// Attacker controls a private account.
|
||||
let attacker_keys = test_private_account_keys_1();
|
||||
let attacker_id =
|
||||
AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0);
|
||||
|
||||
// Victim is a private account — not registered in public chain state.
|
||||
let victim_keys = test_private_account_keys_2();
|
||||
let victim_id =
|
||||
AccountId::for_regular_private_account(&victim_keys.npk(), &victim_keys.vpk(), 0);
|
||||
let victim_balance = 5_000_u128;
|
||||
|
||||
let recipient_id = AccountId::new([42_u8; 32]);
|
||||
|
||||
// Victim has no public state entry; only recipient is registered at genesis.
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(recipient_id, 0)]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Build attacker's private account and its local commitment tree.
|
||||
let attacker_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let attacker_commitment = Commitment::new(&attacker_id, &attacker_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&attacker_commitment));
|
||||
let membership_proof = commitment_set
|
||||
.get_proof_for(&attacker_commitment)
|
||||
.expect("attacker commitment must be in the set");
|
||||
|
||||
let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id);
|
||||
|
||||
// The attacker supplies the victim's account data directly — it cannot be read from
|
||||
// public state. The injected balance and program_owner allow simple_balance_transfer
|
||||
// to succeed inside the circuit, which has no access to chain state and cannot detect
|
||||
// that these values are fabricated.
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_balance,
|
||||
0_u128, // nonce
|
||||
crate::test_methods::simple_balance_transfer().id(), // program_owner
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
|
||||
let p2 = crate::test_methods::malicious_launderer();
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
// [0] attacker — first seen in P1's program_output.pre_states
|
||||
// [1] victim — first seen in simple_balance_transfer's program_output.pre_states
|
||||
// [2] recipient — first seen in simple_balance_transfer's program_output.pre_states
|
||||
//
|
||||
// Victim is marked Public: the attacker has no nsk for the victim's private account,
|
||||
// so PrivateAuthorizedUpdate is not an option.
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
vpk: attacker_keys.vpk(),
|
||||
random_seed: [0; 32],
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public, // victim — attacker lacks victim's nsk
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: simple_balance_transfer runs against the injected
|
||||
// victim(balance=5000, is_authorized=true) and produces valid inner receipts.
|
||||
// The outer circuit commits victim(is_authorized=true) to public_pre_states.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack on private victim should be rejected with InvalidPrivacyPreservingProof"
|
||||
);
|
||||
// Victim has no public balance to check; confirming the recipient received nothing
|
||||
// is sufficient to show no funds moved.
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Two malicious programs (injector + launderer) attempt to drain a victim's balance
|
||||
/// without the victim signing anything. The test passes when the attack is rejected
|
||||
/// and the victim's balance is left untouched.
|
||||
///
|
||||
/// Attack flow:
|
||||
/// Transaction (attacker signs) → P1 (`malicious_injector`)
|
||||
/// → injects `victim(is_authorized=true)` into chained-call `pre_states` for P2
|
||||
/// P2 (`malicious_launderer`)
|
||||
/// → outputs empty pre/post states, forwarding the forged flag to `simple_balance_transfer`
|
||||
/// → if `authorized_accounts` were built from the injected `pre_states`,
|
||||
/// `{victim}.contains(victim)` would pass and the transfer would execute.
|
||||
///
|
||||
/// The validator must reject this: `authorized_accounts` must be derived from the
|
||||
/// parent program's own validated `program_output.pre_states`, not from the chained-call
|
||||
/// input, so a forged `is_authorized=true` flag is never trusted.
|
||||
#[test]
|
||||
fn malicious_programs_cannot_drain_victim_without_signature() {
|
||||
// p2_id, simple_balance_transfer_id, victim_id_raw, victim_balance, victim_nonce,
|
||||
// victim_program_owner, recipient_id_raw, amount.
|
||||
// Primitives only — AccountId/Account cannot round-trip through instruction_data
|
||||
// via risc0_zkvm::serde (SerializeDisplay issue).
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
let attacker_key = PrivateKey::try_new([10; 32]).unwrap();
|
||||
let attacker_id = AccountId::from(&PublicKey::new_from_private_key(&attacker_key));
|
||||
|
||||
let victim_key = PrivateKey::try_new([20; 32]).unwrap();
|
||||
let victim_id = AccountId::from(&PublicKey::new_from_private_key(&victim_key));
|
||||
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
|
||||
let victim_balance = 5_000_u128;
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[
|
||||
(attacker_id, 100),
|
||||
(victim_id, victim_balance),
|
||||
(recipient_id, 0),
|
||||
]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Read victim state from chain, exactly as the attacker would.
|
||||
let victim_account = state.get_account_by_id(victim_id);
|
||||
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_account.balance,
|
||||
victim_account.nonce.0,
|
||||
victim_account.program_owner,
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
|
||||
let message = Message::try_new(
|
||||
crate::test_methods::malicious_injector().id(),
|
||||
vec![attacker_id],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&attacker_key]);
|
||||
let tx = crate::PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id }
|
||||
)) if account_id == victim_id
|
||||
),
|
||||
"attack transaction should be rejected with InvalidAccountAuthorization for the victim"
|
||||
);
|
||||
|
||||
// Confirm the victim's balance is untouched.
|
||||
let victim_balance_after = state.get_account_by_id(victim_id).balance;
|
||||
let recipient_balance_after = state.get_account_by_id(recipient_id).balance;
|
||||
|
||||
assert_eq!(
|
||||
victim_balance_after, victim_balance,
|
||||
"victim balance should be unchanged"
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after, 0,
|
||||
"recipient should receive nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test: a `PrivacyPreservingTransaction` carrying a structurally invalid
|
||||
/// proof must be rejected with a clean `Err`.
|
||||
#[test]
|
||||
fn privacy_garbage_proof_is_rejected() {
|
||||
use lee_core::{
|
||||
Commitment,
|
||||
account::Account,
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::Proof, message::Message, witness_set::WitnessSet,
|
||||
},
|
||||
};
|
||||
|
||||
let state = V03State::new();
|
||||
|
||||
// Minimal message that passes every check up to proof verification: a single
|
||||
// commitment satisfies the non-empty requirement, no signers makes the
|
||||
// nonce/signature checks vacuously true, and unbounded validity windows are valid
|
||||
// for any block/timestamp.
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(
|
||||
&PrivateKey::try_new([1_u8; 32]).unwrap(),
|
||||
));
|
||||
let commitment = Commitment::new(&account_id, &Account::default());
|
||||
let message = Message {
|
||||
public_account_ids: vec![],
|
||||
nonces: vec![],
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![commitment],
|
||||
new_nullifiers: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
// Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`.
|
||||
let garbage_proof = Proof::from_inner(vec![0xff_u8; 64]);
|
||||
let witness_set = WitnessSet::for_message(&message, garbage_proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
match result {
|
||||
Err(LeeError::InvalidPrivacyPreservingProof) => {}
|
||||
Err(other) => panic!("expected InvalidPrivacyPreservingProof, got {other:?}"),
|
||||
Ok(_) => panic!("garbage proof was accepted instead of rejected"),
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,31 @@ pub struct Block {
|
||||
pub bedrock_status: BedrockStatus,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
/// Recomputes the hash from this block's contents, for integrity verification
|
||||
/// against the value stored in `header.hash`.
|
||||
#[must_use]
|
||||
pub fn recompute_hash(&self) -> BlockHash {
|
||||
HashableBlockData {
|
||||
block_id: self.header.block_id,
|
||||
prev_block_hash: self.header.prev_block_hash,
|
||||
timestamp: self.header.timestamp,
|
||||
transactions: self.body.transactions.clone(),
|
||||
}
|
||||
.compute_hash()
|
||||
}
|
||||
|
||||
/// Recomputes the signed hash from the block contents and checks the header
|
||||
/// signature against `expected_pubkey`. Used to pin a peer zone's
|
||||
/// block-signing key, so a block inscribed by anyone other than that zone's
|
||||
/// sequencer is rejected even if it reached the channel.
|
||||
#[must_use]
|
||||
pub fn is_signed_by(&self, expected_pubkey: &lee::PublicKey) -> bool {
|
||||
let hash = HashableBlockData::from(self.clone()).compute_hash();
|
||||
self.header.signature.is_valid_for(&hash.0, expected_pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Block {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
crate::borsh_base64::serialize(self, serializer)
|
||||
@ -76,11 +101,13 @@ pub struct HashableBlockData {
|
||||
}
|
||||
|
||||
impl HashableBlockData {
|
||||
/// Domain-separated hash of the block contents: `SHA256(PREFIX || borsh(self))`.
|
||||
/// The single source of truth for both producing and verifying a block hash.
|
||||
#[must_use]
|
||||
pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block {
|
||||
pub fn compute_hash(&self) -> BlockHash {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Block/\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
let data_bytes = borsh::to_vec(&self).unwrap();
|
||||
let data_bytes = borsh::to_vec(self).unwrap();
|
||||
let mut bytes = Vec::with_capacity(
|
||||
PREFIX
|
||||
.len()
|
||||
@ -89,8 +116,12 @@ impl HashableBlockData {
|
||||
);
|
||||
bytes.extend_from_slice(PREFIX);
|
||||
bytes.extend_from_slice(&data_bytes);
|
||||
OwnHasher::hash(&bytes)
|
||||
}
|
||||
|
||||
let hash = OwnHasher::hash(&bytes);
|
||||
#[must_use]
|
||||
pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block {
|
||||
let hash = self.compute_hash();
|
||||
let signature = lee::Signature::new(signing_key, &hash.0);
|
||||
Block {
|
||||
header: BlockHeader {
|
||||
@ -132,4 +163,33 @@ mod tests {
|
||||
let block_from_bytes = borsh::from_slice::<HashableBlockData>(&bytes).unwrap();
|
||||
assert_eq!(hashable, block_from_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recompute_hash_matches_header_for_well_formed_block() {
|
||||
let key = lee::PrivateKey::try_new([7_u8; 32]).expect("valid key");
|
||||
let block = HashableBlockData {
|
||||
block_id: 5,
|
||||
prev_block_hash: HashType([9_u8; 32]),
|
||||
timestamp: 42,
|
||||
transactions: vec![test_utils::produce_dummy_empty_transaction()],
|
||||
}
|
||||
.into_pending_block(&key);
|
||||
assert_eq!(block.recompute_hash(), block.header.hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recompute_hash_detects_tampering() {
|
||||
let key = lee::PrivateKey::try_new([7_u8; 32]).expect("valid key");
|
||||
let block = HashableBlockData {
|
||||
block_id: 5,
|
||||
prev_block_hash: HashType([9_u8; 32]),
|
||||
timestamp: 42,
|
||||
transactions: vec![test_utils::produce_dummy_empty_transaction()],
|
||||
}
|
||||
.into_pending_block(&key);
|
||||
|
||||
let mut tampered = block;
|
||||
tampered.header.timestamp = 99; // header changed; stale hash no longer matches
|
||||
assert_ne!(tampered.recompute_hash(), tampered.header.hash);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,9 +92,8 @@ impl LeeTransaction {
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Computes the validated state diff without enforcing the system-account
|
||||
/// restriction. Shared by [`Self::validate_on_state`] and
|
||||
/// [`Self::execute_without_system_accounts_check_on_state`].
|
||||
/// Computes the validated state diff. Shared by [`Self::validate_on_state`]
|
||||
/// (which adds the system-account guards) and [`Self::execute_on_state`].
|
||||
fn compute_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
@ -129,16 +128,12 @@ impl LeeTransaction {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Similar to [`Self::execute_check_on_state`], but skips the system-account guard.
|
||||
/// Executes the transaction against the current state and applies the resulting diff,
|
||||
/// without the system-account guards enforced by [`Self::execute_check_on_state`].
|
||||
///
|
||||
/// FIXME: HOT FIX (testnet v0.2): the indexer replays blocks the sequencer already
|
||||
/// accepted, including sequencer-generated deposit transactions that
|
||||
/// legitimately modify the bridge account. The `TransactionOrigin::Sequencer`
|
||||
/// tag that lets the sequencer bypass the guard is not carried in the block,
|
||||
/// so the indexer cannot yet distinguish deposit txs from user txs.
|
||||
///
|
||||
/// REMOVE ME when the indexer can authenticate deposit transactions.
|
||||
pub fn execute_without_system_accounts_check_on_state(
|
||||
/// The indexer replays blocks the sequencer already validated and inscribed on Bedrock,
|
||||
/// so it trusts those inscriptions and re-derives state without re-validating them.
|
||||
pub fn execute_on_state(
|
||||
self,
|
||||
state: &mut V03State,
|
||||
block_id: BlockId,
|
||||
|
||||
@ -3,5 +3,6 @@
|
||||
"bedrock_config": {
|
||||
"addr": "http://logos-blockchain-node-0:18080"
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101"
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"allow_chain_reset": true
|
||||
}
|
||||
|
||||
17
lez/cross_zone/Cargo.toml
Normal file
17
lez/cross_zone/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "cross_zone"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
lee.workspace = true
|
||||
lee_core.workspace = true
|
||||
programs.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
189
lez/cross_zone/src/lib.rs
Normal file
189
lez/cross_zone/src/lib.rs
Normal file
@ -0,0 +1,189 @@
|
||||
//! Host-side cross-zone helpers that need program ids (`programs`) or the state
|
||||
//! machine (`lee`), kept out of the guest-pure cores. Mirrors `system_accounts`:
|
||||
//! it resolves builtin program ids and bakes them into transactions and genesis
|
||||
//! accounts for the watcher (sequencer) and verifier (indexer).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, InboxConfig, Instruction, ZoneId, inbox_config_account_id,
|
||||
inbox_seen_shard_account_id,
|
||||
};
|
||||
use lee_core::{
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
|
||||
/// The cross-zone emission fields a watcher or verifier reads off a source
|
||||
/// transaction, common to every emitter program.
|
||||
pub struct Emission {
|
||||
pub target_zone: ZoneId,
|
||||
pub target_program_id: ProgramId,
|
||||
pub target_accounts: Vec<[u8; 32]>,
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Whether a program may only be invoked by sequencer-origin transactions.
|
||||
///
|
||||
/// The cross-zone inbox is injected solely by the watcher; a user-submitted call
|
||||
/// must be rejected at ingress, since `TransactionOrigin` is not carried in the
|
||||
/// block.
|
||||
#[must_use]
|
||||
pub fn is_sequencer_only_program(program_id: ProgramId) -> bool {
|
||||
program_id == programs::cross_zone_inbox().id()
|
||||
}
|
||||
|
||||
/// Extracts the cross-zone emission from a source transaction.
|
||||
///
|
||||
/// Recognizes the known emitter programs (`ping_sender`, `bridge_lock`). The
|
||||
/// watcher and verifier both use this so they agree on what a given source tx
|
||||
/// emits.
|
||||
#[must_use]
|
||||
pub fn extract_emission(program_id: ProgramId, instruction_data: &[u32]) -> Option<Emission> {
|
||||
if program_id == programs::ping_sender().id() {
|
||||
let ping_core::SenderInstruction::Send {
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
..
|
||||
} = risc0_zkvm::serde::from_slice(instruction_data).ok()?;
|
||||
Some(Emission {
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
})
|
||||
} else if program_id == programs::bridge_lock().id() {
|
||||
let bridge_lock_core::Instruction::Lock {
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
..
|
||||
} = risc0_zkvm::serde::from_slice(instruction_data).ok()?;
|
||||
Some(Emission {
|
||||
target_zone,
|
||||
target_program_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the sequencer-origin dispatch transaction. Pure for fixed inputs, so
|
||||
/// the watcher's injected tx and the indexer's re-derived tx are byte-identical.
|
||||
fn build_inbox_dispatch_tx(
|
||||
inbox_id: ProgramId,
|
||||
msg: &CrossZoneMessage,
|
||||
target_account_ids: Vec<AccountId>,
|
||||
) -> lee::PublicTransaction {
|
||||
let mut account_ids = Vec::with_capacity(target_account_ids.len().saturating_add(2));
|
||||
account_ids.push(inbox_config_account_id(inbox_id));
|
||||
account_ids.push(inbox_seen_shard_account_id(
|
||||
inbox_id,
|
||||
&msg.src_zone,
|
||||
msg.src_block_id,
|
||||
));
|
||||
account_ids.extend(target_account_ids);
|
||||
|
||||
let message = lee::public_transaction::Message::try_new(
|
||||
inbox_id,
|
||||
account_ids,
|
||||
vec![],
|
||||
Instruction::Dispatch(msg.clone()),
|
||||
)
|
||||
.expect("inbox dispatch instruction must serialize");
|
||||
|
||||
lee::PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the dispatch transaction for one peer emission.
|
||||
///
|
||||
/// Both the sequencer's watcher and the indexer's verifier go through this so
|
||||
/// their transactions are byte-identical for the same emission (the basis of the
|
||||
/// Option B check).
|
||||
#[must_use]
|
||||
pub fn build_dispatch_from_emission(
|
||||
src_zone: ZoneId,
|
||||
src_block_id: u64,
|
||||
src_tx_index: u32,
|
||||
src_program_id: ProgramId,
|
||||
target_program_id: ProgramId,
|
||||
target_accounts: &[[u8; 32]],
|
||||
payload: Vec<u8>,
|
||||
) -> lee::PublicTransaction {
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index,
|
||||
src_program_id,
|
||||
target_program_id,
|
||||
payload,
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
let target_ids = target_accounts
|
||||
.iter()
|
||||
.copied()
|
||||
.map(AccountId::new)
|
||||
.collect();
|
||||
build_inbox_dispatch_tx(programs::cross_zone_inbox().id(), &msg, target_ids)
|
||||
}
|
||||
|
||||
/// Builds the inbox config account a zone seeds into genesis state.
|
||||
///
|
||||
/// Lets the inbox guest authorize inbound peer messages. The sequencer and
|
||||
/// indexer seed the same account from the same config, keeping their replayed
|
||||
/// state consistent.
|
||||
#[must_use]
|
||||
pub fn build_inbox_config_account(
|
||||
self_zone: ZoneId,
|
||||
cross_zone: &CrossZoneConfig,
|
||||
) -> (AccountId, Account) {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
for peer in &cross_zone.peers {
|
||||
allowed_targets.insert(peer.channel_id, peer.allowed_targets.clone());
|
||||
}
|
||||
let config = InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
};
|
||||
|
||||
let account = Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: config
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("inbox config fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
};
|
||||
(inbox_config_account_id(inbox_id), account)
|
||||
}
|
||||
|
||||
/// Builds the genesis holding account funding a holder's bridgeable balance.
|
||||
///
|
||||
/// Owned by `bridge_lock`, data is the LE balance. Not produced by any
|
||||
/// transaction, so the sequencer and indexer both seed it through this one
|
||||
/// builder.
|
||||
#[must_use]
|
||||
pub fn build_holding_account(holder: AccountId, amount: u128) -> (AccountId, Account) {
|
||||
let account = Account {
|
||||
program_owner: programs::bridge_lock().id(),
|
||||
data: bridge_lock_core::balance_bytes(amount)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("balance fits in account data"),
|
||||
..Default::default()
|
||||
};
|
||||
(holder, account)
|
||||
}
|
||||
58
lez/explorer_service/src/components/account_nonce_list.rs
Normal file
58
lez/explorer_service/src/components/account_nonce_list.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use indexer_service_protocol::AccountId;
|
||||
use itertools::{EitherOrBoth, Itertools as _};
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
|
||||
#[component]
|
||||
pub fn AccountNonceList(account_ids: Vec<AccountId>, nonces: Vec<u128>) -> impl IntoView {
|
||||
view! {
|
||||
<div class="accounts-list">
|
||||
{account_ids
|
||||
.into_iter()
|
||||
.zip_longest(nonces.into_iter())
|
||||
.map(|maybe_pair| {
|
||||
match maybe_pair {
|
||||
EitherOrBoth::Both(account_id, nonce) => {
|
||||
let account_id_str = account_id.to_string();
|
||||
view! {
|
||||
<div class="account-item">
|
||||
<A href=format!("/account/{}", account_id_str)>
|
||||
<span class="hash">{account_id_str}</span>
|
||||
</A>
|
||||
<span class="nonce">
|
||||
" (nonce: " {nonce.to_string()} ")"
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
EitherOrBoth::Left(account_id) => {
|
||||
let account_id_str = account_id.to_string();
|
||||
view! {
|
||||
<div class="account-item">
|
||||
<A href=format!("/account/{}", account_id_str)>
|
||||
<span class="hash">{account_id_str}</span>
|
||||
</A>
|
||||
<span class="nonce">
|
||||
" (nonce: "{"Not affected by this transaction".to_owned()}" )"
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
EitherOrBoth::Right(_) => {
|
||||
view! {
|
||||
<div class="account-item">
|
||||
<A href=format!("/account/{}", "Account not found")>
|
||||
<span class="hash">{"Account not found"}</span>
|
||||
</A>
|
||||
<span class="nonce">
|
||||
" (nonce: "{"Account not found".to_owned()}" )"
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user