mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-26 00:49:27 +00:00
Merge branch 'main' into Pravdyvy/ffi-mnemonic
This commit is contained in:
commit
c6d7f37a1a
@ -59,6 +59,8 @@ allow-git = [
|
||||
"https://github.com/EspressoSystems/jellyfish.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/arkworks-rs/spongefish.git"
|
||||
]
|
||||
unknown-git = "deny"
|
||||
unknown-registry = "deny"
|
||||
|
||||
@ -16,4 +16,4 @@ runs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
run: |
|
||||
curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/6ac348bea4160ca708b70a86b3964e9f1ce82fff/scripts/setup-logos-blockchain-circuits.sh | bash
|
||||
curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/6ac348bea4160ca708b70a86b3964e9f1ce82fff/scripts/setup-logos-blockchain-circuits.sh | bash
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@ -87,10 +87,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
|
||||
|
||||
@ -122,10 +118,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
|
||||
|
||||
@ -157,10 +149,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
|
||||
|
||||
@ -221,10 +209,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
|
||||
|
||||
@ -254,10 +238,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
|
||||
|
||||
|
||||
612
Cargo.lock
generated
612
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@ -133,18 +133,19 @@ chrono = "0.4.41"
|
||||
borsh = "1.5.7"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
num-bigint = "0.4.6"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" }
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = [
|
||||
"snappy",
|
||||
@ -157,6 +158,7 @@ k256 = { version = "0.13.3", features = [
|
||||
"expose-field",
|
||||
"serde",
|
||||
"pem",
|
||||
"schnorr",
|
||||
] }
|
||||
ml-kem = { version = "0.3", features = ["hazmat"] }
|
||||
elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
|
||||
@ -165,7 +167,7 @@ actix-web = { version = "4.13.0", default-features = false, features = [
|
||||
] }
|
||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
|
||||
pyo3 = { version = "0.24", features = ["auto-initialize"] }
|
||||
pyo3 = { version = "0.29", features = ["auto-initialize"] }
|
||||
zeroize = "1"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
|
||||
45
Justfile
45
Justfile
@ -8,7 +8,7 @@ METHODS_PATH := "program_methods"
|
||||
TEST_METHODS_PATH := "test_program_methods"
|
||||
ARTIFACTS := "artifacts"
|
||||
|
||||
# Build risc0 program artifacts
|
||||
# Build risc0 program artifacts.
|
||||
build-artifacts:
|
||||
@echo "🔨 Building artifacts"
|
||||
@for methods_path in {{METHODS_PATH}} {{TEST_METHODS_PATH}}; do \
|
||||
@ -18,7 +18,13 @@ build-artifacts:
|
||||
cp target/$methods_path/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/$methods_path; \
|
||||
done
|
||||
|
||||
# Run tests
|
||||
# Format codebase.
|
||||
fmt:
|
||||
@echo "🎨 Formatting codebase"
|
||||
cargo +nightly fmt
|
||||
taplo fmt
|
||||
|
||||
# Run tests.
|
||||
test:
|
||||
@echo "🧪 Running tests"
|
||||
RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast
|
||||
@ -29,42 +35,59 @@ bench:
|
||||
cargo bench -p crypto_primitives_bench --bench primitives
|
||||
cargo bench -p cycle_bench --features ppe --bench verify
|
||||
|
||||
# Run Bedrock node in docker
|
||||
# Run Bedrock node in docker.
|
||||
[working-directory: 'bedrock']
|
||||
run-bedrock:
|
||||
@echo "⛓️ Running bedrock"
|
||||
docker compose up
|
||||
|
||||
# Run Sequencer
|
||||
# Run Sequencer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
[working-directory: 'lez/sequencer/service']
|
||||
run-sequencer:
|
||||
run-sequencer standalone="":
|
||||
@echo "🧠 Running sequencer"
|
||||
RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p sequencer_service configs/debug/sequencer_config.json
|
||||
@if [ "{{standalone}}" = "standalone" ]; then \
|
||||
echo "🧪 Running in standalone mode"; \
|
||||
RUST_LOG=info cargo run --features standalone --release -p sequencer_service configs/debug/sequencer_config.json; \
|
||||
else \
|
||||
echo "🚀 Running in normal mode"; \
|
||||
RUST_LOG=info cargo run --release -p sequencer_service configs/debug/sequencer_config.json; \
|
||||
fi
|
||||
|
||||
# Run Indexer
|
||||
# Run Indexer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
[working-directory: 'lez/indexer/service']
|
||||
run-indexer mock="":
|
||||
@echo "🔍 Running indexer"
|
||||
@if [ "{{mock}}" = "mock" ]; then \
|
||||
echo "🧪 Using mock data"; \
|
||||
RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \
|
||||
RUST_LOG=info cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \
|
||||
else \
|
||||
echo "🚀 Using real data"; \
|
||||
RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p indexer_service configs/indexer_config.json; \
|
||||
RUST_LOG=info cargo run --release -p indexer_service configs/indexer_config.json; \
|
||||
fi
|
||||
|
||||
# Run Explorer
|
||||
# Run Explorer.
|
||||
[working-directory: 'lez/explorer_service']
|
||||
run-explorer:
|
||||
@echo "🌐 Running explorer"
|
||||
RUST_LOG=info cargo leptos serve
|
||||
|
||||
# Run Wallet
|
||||
# Run Wallet.
|
||||
[working-directory: 'lez/wallet']
|
||||
run-wallet +args:
|
||||
@echo "🔑 Running wallet"
|
||||
LEE_WALLET_HOME_DIR=$(pwd)/configs/debug cargo run --release -p wallet -- {{args}}
|
||||
|
||||
# Import test accounts supplied in sequencer configuration.
|
||||
wallet-import-test-accounts:
|
||||
@echo "⚙️ Initializing accounts"
|
||||
just run-wallet account import public --private-key 7f273098f25b71e6c005a9519f2678da8d1c7f01f6a27778e2d9948abdf901fb
|
||||
just run-wallet vault claim --account-id Public/CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r --amount 10000
|
||||
|
||||
just run-wallet account import public --private-key f434f8741720014586ae43356d2aec6257da086222f604ddb75d69733b86fc4c
|
||||
just run-wallet vault claim --account-id Public/2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2 --amount 20000
|
||||
|
||||
just run-wallet account list
|
||||
|
||||
# Clean runtime data
|
||||
clean:
|
||||
@echo "🧹 Cleaning run artifacts"
|
||||
|
||||
@ -147,8 +147,6 @@ The sequencer and logos blockchain node can be run locally:
|
||||
1. On one terminal go to the `logos-blockchain/logos-blockchain` repo and run a local logos blockchain node:
|
||||
- `git checkout master; git pull`
|
||||
- `cargo clean`
|
||||
- `rm -r ~/.logos-blockchain-circuits`
|
||||
- `./scripts/setup-logos-blockchain-circuits.sh`
|
||||
- `cargo build --all-features`
|
||||
- `./target/debug/logos-blockchain-node --deployment nodes/node/standalone-deployment-config.yaml nodes/node/standalone-node-config.yaml`
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -8,7 +8,6 @@ blend:
|
||||
scheduler:
|
||||
cover:
|
||||
message_frequency_per_round: 1.0
|
||||
intervals_for_safety_buffer: 100
|
||||
delayer:
|
||||
maximum_release_delay_in_rounds: 3
|
||||
minimum_messages_coefficient: 1
|
||||
@ -31,10 +30,9 @@ cryptarchia:
|
||||
sdp_config:
|
||||
service_params:
|
||||
BN:
|
||||
lock_period: 10
|
||||
inactivity_period: 1
|
||||
retention_period: 1
|
||||
timestamp: 0
|
||||
epoch: 0
|
||||
min_stake:
|
||||
threshold: 1
|
||||
timestamp: 0
|
||||
@ -72,13 +70,10 @@ cryptarchia:
|
||||
inscription: '0c000000000000006c6f676f732d6465766e65742c046269000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
parent: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
signer: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
execution_gas_price: 0
|
||||
storage_gas_price: 0
|
||||
ops_proofs:
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
time:
|
||||
slot_duration: '1.0'
|
||||
chain_start_time: PLACEHOLDER_CHAIN_START_TIME
|
||||
mempool:
|
||||
pubsub_topic: mantle_e2e_tests
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
logos-blockchain-node-0:
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:f160cfbf898a06554451cc066d84cfd0f8ab62d59bd3e62d9cde3bd5582c12ab
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:91d6c5bf07e07fcfba5e7cf07d21ee686a6bc4b9f6210f2d28bffbcad9a3729f
|
||||
ports:
|
||||
- "${PORT:-8080}:18080/tcp"
|
||||
volumes:
|
||||
|
||||
@ -46,7 +46,7 @@ _wallet() {
|
||||
cword=$COMP_CWORD
|
||||
}
|
||||
|
||||
local commands="auth-transfer chain-info account pinata token amm ata check-health config restore-keys deploy-program help"
|
||||
local commands="auth-transfer chain-info account pinata token amm ata vault bridge check-health config restore-keys deploy-program help"
|
||||
|
||||
# Find the main command and subcommand by scanning words before the cursor.
|
||||
# Global options that take a value are skipped along with their argument.
|
||||
@ -535,6 +535,56 @@ _wallet() {
|
||||
esac
|
||||
;;
|
||||
|
||||
vault)
|
||||
case "$subcmd" in
|
||||
"")
|
||||
COMPREPLY=($(compgen -W "transfer claim help" -- "$cur"))
|
||||
;;
|
||||
transfer)
|
||||
case "$prev" in
|
||||
--from | --to)
|
||||
_wallet_complete_account_id "$cur"
|
||||
;;
|
||||
--amount)
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--from --to --amount" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
claim)
|
||||
case "$prev" in
|
||||
--account-id)
|
||||
_wallet_complete_account_id "$cur"
|
||||
;;
|
||||
--amount)
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--account-id --amount" -- "$cur"))
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
bridge)
|
||||
case "$subcmd" in
|
||||
"")
|
||||
COMPREPLY=($(compgen -W "withdraw help" -- "$cur"))
|
||||
;;
|
||||
withdraw)
|
||||
case "$prev" in
|
||||
--from)
|
||||
_wallet_complete_account_id "$cur"
|
||||
;;
|
||||
--amount | --bedrock-account-pk)
|
||||
;; # no specific completion
|
||||
*)
|
||||
COMPREPLY=($(compgen -W "--from --amount --bedrock-account-pk" -- "$cur"))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
config)
|
||||
case "$subcmd" in
|
||||
"")
|
||||
|
||||
@ -25,6 +25,8 @@ _wallet() {
|
||||
'token:Token program interaction subcommand'
|
||||
'amm:AMM program interaction subcommand'
|
||||
'ata:Associated Token Account program interaction subcommand'
|
||||
'vault:Vault program interaction subcommand'
|
||||
'bridge:Bridge program interaction subcommand'
|
||||
'check-health:Check the wallet can connect to the node and builtin local programs match the remote versions'
|
||||
'config:Command to setup config, get and set config fields'
|
||||
'restore-keys:Restoring keys from given password at given depth'
|
||||
@ -56,6 +58,11 @@ _wallet() {
|
||||
ata)
|
||||
_wallet_ata
|
||||
;;
|
||||
vault)
|
||||
_wallet_vault
|
||||
bridge)
|
||||
_wallet_bridge
|
||||
;;
|
||||
config)
|
||||
_wallet_config
|
||||
;;
|
||||
@ -442,6 +449,70 @@ _wallet_ata() {
|
||||
esac
|
||||
}
|
||||
|
||||
# vault subcommand
|
||||
_wallet_vault() {
|
||||
local -a subcommands
|
||||
|
||||
_arguments -C \
|
||||
'1: :->subcommand' \
|
||||
'*:: :->args'
|
||||
|
||||
case $state in
|
||||
subcommand)
|
||||
subcommands=(
|
||||
'transfer:Transfer native tokens from sender to recipient vault account'
|
||||
'claim:Claim native tokens from account vault account'
|
||||
'help:Print this message or the help of the given subcommand(s)'
|
||||
)
|
||||
_describe -t subcommands 'vault subcommands' subcommands
|
||||
;;
|
||||
args)
|
||||
case $line[1] in
|
||||
transfer)
|
||||
_arguments \
|
||||
'--from[Source account with privacy prefix or label]:from:_wallet_account_ids' \
|
||||
'--to[Recipient account with privacy prefix or label]:to:_wallet_account_ids' \
|
||||
'--amount[Amount of native tokens to transfer]:amount:'
|
||||
;;
|
||||
claim)
|
||||
_arguments \
|
||||
'--account-id[Account with privacy prefix or label; vault id is derived automatically]:account:_wallet_account_ids' \
|
||||
'--amount[Amount of native tokens to claim]:amount:'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# bridge subcommand
|
||||
_wallet_bridge() {
|
||||
local -a subcommands
|
||||
|
||||
_arguments -C \
|
||||
'1: :->subcommand' \
|
||||
'*:: :->args'
|
||||
|
||||
case $state in
|
||||
subcommand)
|
||||
subcommands=(
|
||||
'withdraw:Withdraw native tokens through the bridge'
|
||||
'help:Print this message or the help of the given subcommand(s)'
|
||||
)
|
||||
_describe -t subcommands 'bridge subcommands' subcommands
|
||||
;;
|
||||
args)
|
||||
case $line[1] in
|
||||
withdraw)
|
||||
_arguments \
|
||||
'--from[Sender account with privacy prefix]:from:_wallet_account_ids' \
|
||||
'--amount[Amount of native tokens to withdraw]:amount:' \
|
||||
'--bedrock-account-pk[Bedrock account public key (32-byte hex)]:bedrock_pk:'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# config subcommand
|
||||
_wallet_config() {
|
||||
local -a subcommands
|
||||
@ -515,6 +586,8 @@ _wallet_help() {
|
||||
'token:Token program interaction subcommand'
|
||||
'amm:AMM program interaction subcommand'
|
||||
'ata:Associated Token Account program interaction subcommand'
|
||||
'vault:Vault program interaction subcommand'
|
||||
'bridge:Bridge program interaction subcommand'
|
||||
'check-health:Check the wallet can connect to the node'
|
||||
'config:Command to setup config, get and set config fields'
|
||||
'restore-keys:Restoring keys from given password at given depth'
|
||||
|
||||
@ -14,21 +14,37 @@ Per-program Risc0 cycle counts, prover wall time, PPE composition cost, and veri
|
||||
| Profile | release |
|
||||
| GPU acceleration | none |
|
||||
|
||||
## Executor cycles
|
||||
## Executor cycles and public-execution ms
|
||||
|
||||
`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over 5 timed iterations (1 warmup discarded).
|
||||
`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over the timed iterations (1 warmup discarded; `--exec-iters` sets the count, 50 below). `calib_ms` and `net_ms` are the public-execution time in milliseconds, on the same axis as the private `G_verify` so the fee model has one common unit for both paths. See the calibration block below for how they are derived.
|
||||
|
||||
| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) |
|
||||
|---|---|---:|---:|---|
|
||||
| authenticated_transfer | Initialize | 43,642 | 1 | 18.86 / 19.41 ± 0.48 |
|
||||
| authenticated_transfer | Transfer | 77,095 | 1 | 19.67 / 20.84 ± 1.16 |
|
||||
| token | Burn | 116,546 | 1 | 24.86 / 25.46 ± 0.63 |
|
||||
| token | Mint | 116,862 | 1 | 24.47 / 25.08 ± 0.42 |
|
||||
| token | Transfer | 127,726 | 1 | 25.00 / 25.40 ± 0.29 |
|
||||
| clock | Tick (no rollups) | 137,022 | 1 | 21.18 / 21.57 ± 0.41 |
|
||||
| ata | Create | 175,056 | 1 | 23.64 / 24.94 ± 1.09 |
|
||||
| amm | SwapExactInput | 508,634 | 1 | 34.21 / 34.77 ± 0.55 |
|
||||
| amm | AddLiquidity | 642,774 | 1 | 37.59 / 37.87 ± 0.28 |
|
||||
| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) | calib_ms | net_ms |
|
||||
|---|---|---:|---:|---|---:|---:|
|
||||
| authenticated_transfer | Initialize | 43,818 | 1 | 30.69 / 31.93 ± 1.03 | 1.31 | 0.29 |
|
||||
| authenticated_transfer | Transfer | 79,958 | 1 | 31.02 / 32.35 ± 0.59 | 2.38 | 0.61 |
|
||||
| token | Burn | 116,546 | 1 | 36.08 / 37.18 ± 0.60 | 3.47 | 5.67 |
|
||||
| token | Mint | 116,862 | 1 | 35.67 / 37.73 ± 2.54 | 3.48 | 5.26 |
|
||||
| token | Transfer | 127,726 | 1 | 35.49 / 36.86 ± 0.90 | 3.81 | 5.08 |
|
||||
| clock | Tick (no rollups) | 137,022 | 1 | 32.12 / 33.16 ± 0.89 | 4.08 | 1.72 |
|
||||
| ata | Create | 174,515 | 1 | 35.41 / 36.49 ± 0.65 | 5.20 | 5.00 |
|
||||
| amm | SwapExactInput | 508,904 | 1 | 46.71 / 48.06 ± 0.86 | 15.17 | 16.30 |
|
||||
| amm | AddLiquidity | 643,464 | 1 | 48.57 / 50.28 ± 0.98 | 19.18 | 18.16 |
|
||||
|
||||
### Public-execution ms calibration
|
||||
|
||||
The binary fits `best_ms = intercept + slope · user_cycles` by ordinary least squares across the nine cases (best-of-N, not mean, so one OS scheduling spike cannot tilt the slope). On the machine above:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| throughput (1 / slope) | 33,546 cycles/ms |
|
||||
| fixed overhead (intercept) | 30.41 ms per call |
|
||||
| R² | 0.935 |
|
||||
|
||||
- `calib_ms = user_cycles / throughput` is the compute-only time, a pure function of the deterministic cycle count and the one pinned-hardware constant, so it reproduces run to run where raw wall-time does not. This is the number to put on the common public/private ms axis.
|
||||
- `net_ms = best exec_ms − fixed overhead` is the measured compute with the host-side overhead stripped; it agrees with `calib_ms` to within the per-program overhead scatter (the intercept is an ELF-size-averaged constant, so this decomposition is first-order, not mechanistic).
|
||||
- The `fixed overhead` is host-side per-call setup (ELF parse into a `MemoryImage`, `ExecutorEnv` build) that is outside the cycle count and does not scale with the instruction's work.
|
||||
|
||||
The fixed overhead is paid per transaction in the current node, not amortized. The public-execution path at `lee/state_machine/src/program.rs:56-87` builds a fresh `ExecutorEnv` and calls `default_executor().execute(env, self.elf())` per call with the raw ELF bytes; no parsed image is cached across transactions. So today the real per-public-tx sequencer cost is the raw `exec_ms` (≈ 31 ms for the cheapest program), overhead-dominated. Caching the parsed `MemoryImage` per `ProgramId` would drop the per-tx cost to `calib_ms` (1–19 ms). Public execution is also cycle-capped at `MAX_NUM_CYCLES_PUBLIC_EXECUTION` (`program.rs:64`), which bounds the worst-case public-tx cost.
|
||||
|
||||
## Real proving (`--prove`)
|
||||
|
||||
@ -85,7 +101,8 @@ The corresponding `proof_bytes` (S_agg) for the bench receipt is captured by `--
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
cargo run --release -p cycle_bench
|
||||
# Executor cycles + public-execution ms calibration (no proving). --exec-iters sets the sample count.
|
||||
cargo run --release -p cycle_bench -- --exec-iters 50
|
||||
cargo run --release -p cycle_bench --features prove -- --prove
|
||||
cargo run --release -p cycle_bench --features ppe -- --prove --ppe
|
||||
|
||||
|
||||
7692
flake.lock
generated
7692
flake.lock
generated
File diff suppressed because it is too large
Load Diff
16
flake.nix
16
flake.nix
@ -13,8 +13,14 @@
|
||||
|
||||
crane.url = "github:ipetkov/crane";
|
||||
|
||||
# Must stay in sync with the lbc-* tags in logos-blockchain/Cargo.lock.
|
||||
logos-blockchain-circuits = {
|
||||
url = "github:logos-blockchain/logos-blockchain-circuits";
|
||||
url = "github:logos-blockchain/logos-blockchain-circuits/2846ee7a4cfa24458bb8063412ab2e753b344d2f";
|
||||
};
|
||||
|
||||
# Must stay in sync with the rust-rapidsnark rev in Cargo.lock.
|
||||
rust-rapidsnark = {
|
||||
url = "github:logos-blockchain/logos-blockchain-rust-rapidsnark/e91187f8ccb5bbfc7bb00dac88169112428da78f";
|
||||
};
|
||||
};
|
||||
|
||||
@ -25,6 +31,7 @@
|
||||
rust-overlay,
|
||||
crane,
|
||||
logos-blockchain-circuits,
|
||||
rust-rapidsnark,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@ -53,6 +60,7 @@
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
src = ./.;
|
||||
cargoLock = builtins.fromTOML (builtins.readFile ./Cargo.lock);
|
||||
lbc_dir = logos-blockchain-circuits.packages.${system}.default;
|
||||
|
||||
# Parse Cargo.lock at eval time to find the locked risc0-circuit-recursion
|
||||
# version and its crates.io checksum — no hardcoding required.
|
||||
@ -102,6 +110,9 @@
|
||||
pkgs.python3 # Required for correct builds now, as python is sandboxed in nix builds
|
||||
];
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
# Logos blockchain related env vars
|
||||
LBC_ROOT_DIR = logos-blockchain-circuits.packages.${system}.default;
|
||||
RAPIDSNARK_LIB_DIR = rust-rapidsnark.packages.${system}.rapidsnark;
|
||||
# Point the risc0-circuit-recursion build script to the pre-fetched zip
|
||||
# so it doesn't try to download it inside the sandbox.
|
||||
RECURSION_SRC_PATH = "${recursionZkr}";
|
||||
@ -115,7 +126,6 @@
|
||||
'' + pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
export PATH="$PATH:/usr/bin"
|
||||
'';
|
||||
LOGOS_BLOCKCHAIN_CIRCUITS = logos-blockchain-circuits.packages.${system}.default;
|
||||
};
|
||||
|
||||
walletFfiPackage = craneLib.buildPackage (
|
||||
@ -142,7 +152,7 @@
|
||||
cargoExtraArgs = "-p indexer_ffi";
|
||||
postInstall = ''
|
||||
mkdir -p $out/include
|
||||
cp indexer/ffi/indexer_ffi.h $out/include/
|
||||
cp lez/indexer/ffi/indexer_ffi.h $out/include/
|
||||
''
|
||||
+ pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
|
||||
install_name_tool -id @rpath/libindexer_ffi.dylib $out/lib/libindexer_ffi.dylib
|
||||
|
||||
@ -29,13 +29,17 @@ wallet-ffi.workspace = true
|
||||
indexer_ffi.workspace = true
|
||||
indexer_service_protocol.workspace = true
|
||||
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
logos-blockchain-zone-sdk.workspace = true
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
tempfile.workspace = true
|
||||
bytesize.workspace = true
|
||||
reqwest.workspace = true
|
||||
borsh.workspace = true
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
num-bigint.workspace = true
|
||||
|
||||
@ -11,7 +11,7 @@ use lee::{
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
};
|
||||
@ -665,9 +665,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let auth_transfer_program_id = Program::authenticated_transfer_program().id();
|
||||
let nsk: lee_core::NullifierSecretKey = [3; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let _vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let ssk = SharedSecretKey([55_u8; 32]);
|
||||
let _epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
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)
|
||||
@ -712,6 +712,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
npk,
|
||||
ssk,
|
||||
identifier: 1337,
|
||||
|
||||
@ -4,19 +4,22 @@
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{ops::Deref as _, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use borsh::BorshSerialize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext};
|
||||
use futures::StreamExt as _;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
|
||||
public_transaction,
|
||||
};
|
||||
use lee_core::{InputAccountIdentity, account::AccountWithMetadata};
|
||||
use log::info;
|
||||
use logos_blockchain_core::mantle::{Value, ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_core::mantle::{ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_http_api_common::bodies::{
|
||||
channel::ChannelDepositRequestBody,
|
||||
wallet::{
|
||||
@ -24,8 +27,14 @@ use logos_blockchain_http_api_common::bodies::{
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
};
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use num_bigint::BigUint;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::public_mention;
|
||||
use tokio::test;
|
||||
use wallet::cli::{Command, execute_subcommand, programs::bridge::BridgeSubcommand};
|
||||
|
||||
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
|
||||
|
||||
@ -150,7 +159,6 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_pre.account.nonce, vault_pre.account.nonce],
|
||||
vec![],
|
||||
output,
|
||||
)
|
||||
.context("Failed to build privacy-preserving bridge deposit message")?;
|
||||
@ -196,8 +204,9 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
|
||||
async fn submit_bedrock_deposit(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
bedrock_account_pk: &str,
|
||||
recipient_id: AccountId,
|
||||
amount: u128,
|
||||
amount: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(BorshSerialize)]
|
||||
struct DepositMetadata {
|
||||
@ -210,18 +219,13 @@ async fn submit_bedrock_deposit(
|
||||
.try_into()
|
||||
.context("Encoded metadata is too big")?;
|
||||
|
||||
let funding_key = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
|
||||
let amount: Value = amount
|
||||
.try_into()
|
||||
.context("Deposit amount does not fit Bedrock Value type")?;
|
||||
let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let query_balance = || async {
|
||||
let balance_response = client
|
||||
.get(format!(
|
||||
"http://{bedrock_addr}/wallet/{funding_key}/balance"
|
||||
"http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
@ -238,13 +242,13 @@ async fn submit_bedrock_deposit(
|
||||
let mut balance = query_balance().await?;
|
||||
|
||||
info!(
|
||||
"Queried Bedrock balance for key {funding_key}: {:?}",
|
||||
"Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
balance.balance
|
||||
);
|
||||
|
||||
if balance.balance < amount {
|
||||
anyhow::bail!(
|
||||
"Bedrock wallet with key {funding_key} has insufficient balance {:?} for deposit amount {:?}",
|
||||
"Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for deposit amount {:?}",
|
||||
balance.balance,
|
||||
amount
|
||||
);
|
||||
@ -370,11 +374,18 @@ async fn wait_for_vault_balance(
|
||||
})?
|
||||
}
|
||||
|
||||
/// Test deposit and withdraw round trip.
|
||||
///
|
||||
/// Implemented as one test instead of two separate tests for deposit and withdraw, because the
|
||||
/// withdraw test depends on the deposit to set up the necessary state (funds in vault) for testing
|
||||
/// withdraw functionality.
|
||||
#[test]
|
||||
async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
let recipient_id = ctx.existing_public_accounts()[0];
|
||||
let amount = 1_u64;
|
||||
let vault_program_id = Program::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id);
|
||||
|
||||
@ -388,10 +399,17 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
.await?;
|
||||
|
||||
// Submit deposit to Bedrock
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), recipient_id, 1).await?;
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
.await
|
||||
.context("Failed to submit Bedrock deposit for round-trip setup")?;
|
||||
|
||||
// Wait for vault to receive the deposit (minted from bridge to vault)
|
||||
wait_for_vault_balance(&ctx, recipient_vault_id, vault_balance_before + 1).await?;
|
||||
wait_for_vault_balance(
|
||||
&ctx,
|
||||
recipient_vault_id,
|
||||
vault_balance_before + u128::from(amount),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Now claim funds from vault back to recipient
|
||||
let nonces = ctx
|
||||
@ -411,7 +429,9 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
vault_program_id,
|
||||
vec![recipient_id, recipient_vault_id],
|
||||
nonces,
|
||||
vault_core::Instruction::Claim { amount: 1 },
|
||||
vault_core::Instruction::Claim {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.context("Failed to build vault claim message")?;
|
||||
|
||||
@ -446,9 +466,125 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after_claim,
|
||||
recipient_balance_before + 1,
|
||||
recipient_balance_before + u128::from(amount),
|
||||
"Recipient balance should increase by claimed amount"
|
||||
);
|
||||
|
||||
// The indexer must replay the deposit and claim blocks and reach the same
|
||||
// state as the sequencer — including the bridge system account the deposit
|
||||
// modifies, which is the case the hot fix unblocks.
|
||||
wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
let bridge_account_id = lee::system_bridge_account_id();
|
||||
for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
|
||||
let indexer_account = indexer_service_rpc::RpcClient::get_account(
|
||||
// `deref` is needed for correct trait resolution
|
||||
// of the async `get_account` method on `RpcClient`
|
||||
ctx.indexer_client().deref(),
|
||||
account_id.into(),
|
||||
)
|
||||
.await?;
|
||||
let sequencer_account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
assert_eq!(
|
||||
indexer_account,
|
||||
sequencer_account.into(),
|
||||
"Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
);
|
||||
}
|
||||
|
||||
// Withdraw back to Bedrock and wait for finalized withdraw event.
|
||||
let sender_id = recipient_id;
|
||||
|
||||
let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
|
||||
let observe_fut = wait_for_finalized_withdraw_op(&observer, amount, bedrock_account_pk);
|
||||
|
||||
let withdraw_fut = execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Bridge(BridgeSubcommand::Withdraw {
|
||||
from: public_mention(sender_id),
|
||||
amount,
|
||||
bedrock_account_pk: bedrock_account_pk.to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
|
||||
withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
|
||||
observe_result
|
||||
.context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
||||
|
||||
// Sleep to observe sequencer log about validated withdraw event
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_zone_indexer_observer(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
|
||||
let bedrock_url = integration_tests::config::addr_to_url(
|
||||
integration_tests::config::UrlProtocol::Http,
|
||||
bedrock_addr,
|
||||
)
|
||||
.context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
|
||||
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
|
||||
Ok(ZoneIndexer::new(
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
node,
|
||||
))
|
||||
}
|
||||
|
||||
async fn wait_for_finalized_withdraw_op(
|
||||
observer: &ZoneIndexer<NodeHttpClient>,
|
||||
expected_amount: u64,
|
||||
receiver_pk: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
|
||||
let bedrock_account_pk_bytes = hex::decode(receiver_pk)
|
||||
.context("Failed to decode expected receiver public key from hex")?;
|
||||
let expected_receiver_pk =
|
||||
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
||||
BigUint::from_bytes_le(&bedrock_account_pk_bytes),
|
||||
);
|
||||
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
let stream = observer
|
||||
.follow()
|
||||
.await
|
||||
.context("Failed to read zone indexer message batch")?;
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
|
||||
while let Some(message) = stream.next().await {
|
||||
info!("Observed zone message {message:?}");
|
||||
|
||||
let ZoneMessage::Withdraw(withdraw) = message else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut iter = withdraw.outputs.iter();
|
||||
let Some(note) = iter.next() else {
|
||||
continue;
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
// Withdraw op should only have one output
|
||||
continue;
|
||||
}
|
||||
|
||||
if note.value == expected_amount && note.pk == expected_receiver_pk {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
|
||||
})?
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ use lee::{
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::PdaSeed,
|
||||
@ -74,6 +74,8 @@ async fn fund_private_pda(
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
@ -89,13 +91,9 @@ async fn fund_private_pda(
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender],
|
||||
vec![sender_account.nonce],
|
||||
vec![(npk, vpk, epk)],
|
||||
output,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
@ -23,7 +23,7 @@ use lee::{
|
||||
public_transaction as putx,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
account::{AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
@ -301,12 +301,16 @@ 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,
|
||||
nsk: sender_nsk,
|
||||
membership_proof: proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: recipient_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
||||
npk: recipient_npk,
|
||||
ssk: recipient_ss,
|
||||
identifier: 0,
|
||||
@ -315,16 +319,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(sender_npk, sender_vpk, sender_epk),
|
||||
(recipient_npk, recipient_vpk, recipient_epk),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
|
||||
pptx::PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
208
integration_tests/tests/vault.rs
Normal file
208
integration_tests/tests/vault.rs
Normal file
@ -0,0 +1,208 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use integration_tests::{TestContext, private_mention, public_mention};
|
||||
use lee::program::Program;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::cli::{Command, SubcommandReturnValue, programs::vault::VaultSubcommand};
|
||||
|
||||
#[test]
|
||||
async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let amount: u128 = 100;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let recipient = ctx.existing_public_accounts()[1];
|
||||
|
||||
let vault_program_id = Program::vault().id();
|
||||
let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient);
|
||||
|
||||
let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let recipient_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let recipient_vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
|
||||
let transfer_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Transfer {
|
||||
from: public_mention(sender),
|
||||
to: public_mention(recipient),
|
||||
amount,
|
||||
}),
|
||||
)
|
||||
.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
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let recipient_vault_balance_after_transfer = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
sender_balance_after_transfer,
|
||||
sender_balance_before - amount
|
||||
);
|
||||
assert_eq!(recipient_balance_after_transfer, recipient_balance_before);
|
||||
assert_eq!(
|
||||
recipient_vault_balance_after_transfer,
|
||||
recipient_vault_balance_before + amount
|
||||
);
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Claim {
|
||||
account_id: public_mention(recipient),
|
||||
amount,
|
||||
}),
|
||||
)
|
||||
.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
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient)
|
||||
.await?;
|
||||
let recipient_vault_balance_after_claim = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
|
||||
assert_eq!(sender_balance_after_claim, sender_balance_before - amount);
|
||||
assert_eq!(
|
||||
recipient_balance_after_claim,
|
||||
recipient_balance_before + amount
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_vault_balance_after_claim,
|
||||
recipient_vault_balance_before
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn private_transfer_and_private_claim() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let amount: u128 = 100;
|
||||
let sender = ctx.existing_private_accounts()[0];
|
||||
let owner = ctx.existing_private_accounts()[1];
|
||||
|
||||
let vault_program_id = Program::vault().id();
|
||||
let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner);
|
||||
|
||||
let sender_balance_before = ctx
|
||||
.wallet()
|
||||
.get_account_private(sender)
|
||||
.context("Failed to load sender private account")?
|
||||
.balance;
|
||||
let owner_balance_before = ctx
|
||||
.wallet()
|
||||
.get_account_private(owner)
|
||||
.context("Failed to load owner private account")?
|
||||
.balance;
|
||||
let owner_vault_balance_before = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(owner_vault_id)
|
||||
.await?;
|
||||
|
||||
let transfer_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Transfer {
|
||||
from: private_mention(sender),
|
||||
to: private_mention(owner),
|
||||
amount,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(
|
||||
transfer_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault transfer"
|
||||
);
|
||||
|
||||
let sender_balance_after_transfer = ctx
|
||||
.wallet()
|
||||
.get_account_private(sender)
|
||||
.context("Failed to load sender private account after transfer")?
|
||||
.balance;
|
||||
let owner_balance_after_transfer = ctx
|
||||
.wallet()
|
||||
.get_account_private(owner)
|
||||
.context("Failed to load owner private account after transfer")?
|
||||
.balance;
|
||||
let owner_vault_balance_after_transfer = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(owner_vault_id)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
sender_balance_after_transfer,
|
||||
sender_balance_before - amount
|
||||
);
|
||||
assert_eq!(owner_balance_after_transfer, owner_balance_before);
|
||||
assert_eq!(
|
||||
owner_vault_balance_after_transfer,
|
||||
owner_vault_balance_before + amount
|
||||
);
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Claim {
|
||||
account_id: private_mention(owner),
|
||||
amount,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(
|
||||
claim_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault claim"
|
||||
);
|
||||
|
||||
let sender_balance_after_claim = ctx
|
||||
.wallet()
|
||||
.get_account_private(sender)
|
||||
.context("Failed to load sender private account after claim")?
|
||||
.balance;
|
||||
let owner_balance_after_claim = ctx
|
||||
.wallet()
|
||||
.get_account_private(owner)
|
||||
.context("Failed to load owner private account after claim")?
|
||||
.balance;
|
||||
let owner_vault_balance_after_claim = ctx
|
||||
.sequencer_client()
|
||||
.get_account_balance(owner_vault_id)
|
||||
.await?;
|
||||
|
||||
assert_eq!(sender_balance_after_claim, sender_balance_before - amount);
|
||||
assert_eq!(owner_balance_after_claim, owner_balance_before + amount);
|
||||
assert_eq!(owner_vault_balance_after_claim, owner_vault_balance_before);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -7,6 +7,7 @@
|
||||
clippy::undocumented_unsafe_blocks,
|
||||
clippy::multiple_unsafe_ops_per_block,
|
||||
clippy::shadow_unrelated,
|
||||
clippy::as_conversions,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
@ -20,14 +21,19 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use integration_tests::{BlockingTestContext, TIME_TO_WAIT_FOR_BLOCK_SECONDS};
|
||||
use lee::{Account, AccountId, PrivateKey, PublicKey, program::Program};
|
||||
use lee::{
|
||||
Account, AccountId, PrivateKey, PublicKey,
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use lee_core::program::DEFAULT_PROGRAM_ID;
|
||||
use log::info;
|
||||
use tempfile::tempdir;
|
||||
use wallet::account::HumanReadableAccount;
|
||||
use wallet_ffi::{
|
||||
FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey,
|
||||
FfiTransferResult, FfiU128, WalletHandle, error, wallet::FfiCreateWalletResult,
|
||||
FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys,
|
||||
FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error,
|
||||
generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult},
|
||||
wallet::FfiCreateWalletResult,
|
||||
};
|
||||
|
||||
unsafe extern "C" {
|
||||
@ -186,6 +192,42 @@ unsafe extern "C" {
|
||||
mnemonic: *const c_char,
|
||||
password: *const c_char,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_resolve_public_account(
|
||||
account_id: FfiBytes32,
|
||||
needs_sign: bool,
|
||||
out_account_identity: *mut FfiAccountIdentity,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_send_generic_public_transaction(
|
||||
handle: *mut WalletHandle,
|
||||
account_identities: *const FfiAccountIdentity,
|
||||
account_identities_size: usize,
|
||||
instruction_words: *const u32,
|
||||
instruction_words_size: usize,
|
||||
program_with_dependencies: *const FfiProgramWithDependencies,
|
||||
out_result: *mut FfiTransactionResult,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_resolve_private_account(
|
||||
handle: *mut WalletHandle,
|
||||
account_id: FfiBytes32,
|
||||
out_account_identity: *mut FfiAccountIdentity,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_send_generic_private_transaction(
|
||||
handle: *mut WalletHandle,
|
||||
account_identities: *const FfiAccountIdentity,
|
||||
account_identities_size: usize,
|
||||
instruction_words: *const u32,
|
||||
instruction_words_size: usize,
|
||||
program_with_dependencies: *const FfiProgramWithDependencies,
|
||||
out_result: *mut FfiTransactionResult,
|
||||
) -> error::WalletFfiError;
|
||||
|
||||
fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult);
|
||||
|
||||
fn wallet_ffi_free_account_identity(account_identity: *mut FfiAccountIdentity);
|
||||
}
|
||||
|
||||
fn new_wallet_ffi_with_test_context_config(
|
||||
@ -1400,34 +1442,6 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
|
||||
// Get the account list with FFI method
|
||||
let wallet_ffi_account_list = unsafe {
|
||||
let mut out_list = FfiAccountList::default();
|
||||
wallet_ffi_list_accounts(wallet_ffi_handle, &raw mut out_list).unwrap();
|
||||
out_list
|
||||
};
|
||||
|
||||
let wallet_ffi_account_list_slice = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
wallet_ffi_account_list.entries,
|
||||
wallet_ffi_account_list.count,
|
||||
)
|
||||
};
|
||||
|
||||
// All created accounts must appear in the list
|
||||
let listed_public_ids: HashSet<_> = wallet_ffi_account_list_slice
|
||||
.iter()
|
||||
.filter(|e| e.is_public)
|
||||
.map(|e| hex::encode(e.account_id.data))
|
||||
.collect();
|
||||
|
||||
info!("Current list of accounts: {listed_public_ids:?}");
|
||||
|
||||
info!("Private acc to be restored 1 {:?}", hex::encode(private_account_id_1.data));
|
||||
info!("Private acc to be restored 2 {:?}", hex::encode(private_account_id_2.data));
|
||||
info!("Pub acc to be restored 1 {:?}", hex::encode(public_account_id_1.data));
|
||||
info!("Pub acc to be restored 2 {:?}", hex::encode(public_account_id_2.data));
|
||||
|
||||
assert_eq!(private_account_id_1_balance, 100);
|
||||
assert_eq!(private_account_id_2_balance, 101);
|
||||
assert_eq!(public_account_id_1_balance, 102);
|
||||
@ -1437,3 +1451,207 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletResult {
|
||||
wallet: wallet_ffi_handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into();
|
||||
let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into();
|
||||
let amount = 100_u128;
|
||||
|
||||
let mut transaction_result = FfiTransactionResult::default();
|
||||
|
||||
let mut from_account_identity = FfiAccountIdentity::default();
|
||||
let mut to_account_identity = FfiAccountIdentity::default();
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_resolve_public_account(from, true, &raw mut from_account_identity).unwrap();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_resolve_public_account(to, true, &raw mut to_account_identity).unwrap();
|
||||
}
|
||||
|
||||
let ffi_accs = vec![from_account_identity, to_account_identity];
|
||||
let account_identities_size = ffi_accs.len();
|
||||
let account_identities =
|
||||
Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity;
|
||||
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
|
||||
amount,
|
||||
})
|
||||
.unwrap();
|
||||
let instruction_words_size = instruction_data.len();
|
||||
let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32;
|
||||
|
||||
let program: ProgramWithDependencies = Program::authenticated_transfer_program().into();
|
||||
let program_with_dependencies: FfiProgramWithDependencies = program.into();
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_send_generic_public_transaction(
|
||||
wallet_ffi_handle,
|
||||
account_identities,
|
||||
account_identities_size,
|
||||
instruction_words,
|
||||
instruction_words_size,
|
||||
&raw const program_with_dependencies,
|
||||
&raw mut transaction_result,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
let from_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const from,
|
||||
true,
|
||||
&raw mut out_balance,
|
||||
)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
|
||||
let to_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
wallet_ffi_get_balance(wallet_ffi_handle, &raw const to, true, &raw mut out_balance)
|
||||
.unwrap();
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
|
||||
assert_eq!(from_balance, 9900);
|
||||
assert_eq!(to_balance, 20100);
|
||||
|
||||
unsafe {
|
||||
let account_identities_mut = account_identities.cast_mut();
|
||||
wallet_ffi_free_account_identity(account_identities_mut);
|
||||
wallet_ffi_free_account_identity(account_identities_mut.add(1));
|
||||
|
||||
let instruction_data =
|
||||
std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size);
|
||||
drop(Box::from_raw(std::ptr::from_mut(instruction_data)));
|
||||
|
||||
wallet_ffi_free_transaction_result(&raw mut transaction_result);
|
||||
wallet_ffi_destroy(wallet_ffi_handle);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletResult {
|
||||
wallet: wallet_ffi_handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into();
|
||||
let to: FfiBytes32 = ctx.ctx().existing_private_accounts()[1].into();
|
||||
let amount = 100_u128;
|
||||
|
||||
let mut transaction_result = FfiTransactionResult::default();
|
||||
|
||||
let mut from_account_identity = FfiAccountIdentity::default();
|
||||
let mut to_account_identity = FfiAccountIdentity::default();
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_resolve_private_account(wallet_ffi_handle, from, &raw mut from_account_identity)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_resolve_private_account(wallet_ffi_handle, to, &raw mut to_account_identity)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let ffi_accs = vec![from_account_identity, to_account_identity];
|
||||
let account_identities_size = ffi_accs.len();
|
||||
let account_identities =
|
||||
Box::into_raw(ffi_accs.into_boxed_slice()) as *const FfiAccountIdentity;
|
||||
|
||||
let instruction_data =
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
|
||||
amount,
|
||||
})
|
||||
.unwrap();
|
||||
let instruction_words_size = instruction_data.len();
|
||||
let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32;
|
||||
|
||||
let program: ProgramWithDependencies = Program::authenticated_transfer_program().into();
|
||||
let program_with_dependencies: FfiProgramWithDependencies = program.into();
|
||||
|
||||
unsafe {
|
||||
wallet_ffi_send_generic_private_transaction(
|
||||
wallet_ffi_handle,
|
||||
account_identities,
|
||||
account_identities_size,
|
||||
instruction_words,
|
||||
instruction_words_size,
|
||||
&raw const program_with_dependencies,
|
||||
&raw mut transaction_result,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(transaction_result.secrets_size, 2);
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
|
||||
|
||||
// Sync private account local storage with onchain encrypted state
|
||||
unsafe {
|
||||
let mut current_height = 0;
|
||||
wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap();
|
||||
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
|
||||
};
|
||||
|
||||
let from_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
let _result = wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const from,
|
||||
false,
|
||||
&raw mut out_balance,
|
||||
);
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
|
||||
let to_balance = unsafe {
|
||||
let mut out_balance: [u8; 16] = [0; 16];
|
||||
let _result = wallet_ffi_get_balance(
|
||||
wallet_ffi_handle,
|
||||
&raw const to,
|
||||
false,
|
||||
&raw mut out_balance,
|
||||
);
|
||||
u128::from_le_bytes(out_balance)
|
||||
};
|
||||
|
||||
assert_eq!(from_balance, 9900);
|
||||
assert_eq!(to_balance, 20100);
|
||||
|
||||
unsafe {
|
||||
let account_identities_mut = account_identities.cast_mut();
|
||||
wallet_ffi_free_account_identity(account_identities_mut);
|
||||
wallet_ffi_free_account_identity(account_identities_mut.add(1));
|
||||
|
||||
let instruction_data =
|
||||
std::slice::from_raw_parts_mut(instruction_words.cast_mut(), instruction_words_size);
|
||||
drop(Box::from_raw(std::ptr::from_mut(instruction_data)));
|
||||
|
||||
wallet_ffi_free_transaction_result(&raw mut transaction_result);
|
||||
wallet_ffi_destroy(wallet_ffi_handle);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
|
||||
use k256::elliptic_curve::PrimeField as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyTreeNode;
|
||||
@ -6,9 +6,13 @@ use crate::key_management::key_tree::traits::KeyTreeNode;
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
|
||||
pub struct ChildKeysPublic {
|
||||
pub csk: lee::PrivateKey,
|
||||
pub cpk: lee::PublicKey,
|
||||
pub ccc: [u8; 32],
|
||||
/// Secret key for public account.
|
||||
pub sk: lee::PrivateKey,
|
||||
/// Schnorr secret key.
|
||||
pub ssk: lee::PrivateKey,
|
||||
/// Schnorr public key.
|
||||
pub pk: lee::PublicKey,
|
||||
pub cc: [u8; 32],
|
||||
/// Can be [`None`] if root.
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
@ -18,19 +22,24 @@ impl ChildKeysPublic {
|
||||
pub fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
|
||||
|
||||
let csk = lee::PrivateKey::try_new(
|
||||
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 ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||
let cpk = lee::PublicKey::new_from_private_key(&csk);
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: 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");
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
sk,
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
@ -39,61 +48,53 @@ impl ChildKeysPublic {
|
||||
pub fn nth_child(&self, cci: u32) -> Self {
|
||||
let hash_value = self.compute_hash_value(cci);
|
||||
|
||||
let csk = lee::PrivateKey::try_new({
|
||||
let hash_value = 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");
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"))
|
||||
.into(),
|
||||
)
|
||||
.expect("Expect a valid k256 scalar");
|
||||
let rhs =
|
||||
k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar");
|
||||
|
||||
let value_1 =
|
||||
k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar");
|
||||
let value_2 = k256::Scalar::from_repr((*self.csk.value()).into())
|
||||
.expect("Expect a valid k256 scalar");
|
||||
let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into())
|
||||
.expect("Expect a valid private key");
|
||||
|
||||
let sum = value_1.add(&value_2);
|
||||
sum.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 ccc = *hash_value
|
||||
let cc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let cpk = lee::PublicKey::new_from_private_key(&csk);
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
sk,
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn account_id(&self) -> lee::AccountId {
|
||||
lee::AccountId::from(&self.cpk)
|
||||
lee::AccountId::from(&self.pk)
|
||||
}
|
||||
|
||||
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
|
||||
let mut hash_input = vec![];
|
||||
|
||||
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
|
||||
// Non-harden.
|
||||
// BIP-032 compatibility requires 1-byte header from the public_key;
|
||||
// Not stored in `self.cpk.value()`.
|
||||
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
|
||||
.expect("32 bytes, within curve order");
|
||||
let pk = sk.public_key();
|
||||
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
|
||||
} else {
|
||||
// Harden.
|
||||
hash_input.extend_from_slice(&[0_u8]);
|
||||
hash_input.extend_from_slice(self.csk.value());
|
||||
}
|
||||
// Simplified key logic by only supporting harden keys.
|
||||
// Non-harden keys would require access to untweaked public keys associated to `sk`s.
|
||||
// Thus, not PQ secure.
|
||||
hash_input.extend_from_slice(&[0_u8]);
|
||||
hash_input.extend_from_slice(self.sk.value());
|
||||
|
||||
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
|
||||
hash_input.extend_from_slice(&cci.to_be_bytes());
|
||||
|
||||
hmac_sha512::HMAC::mac(hash_input, self.ccc)
|
||||
hmac_sha512::HMAC::mac(hash_input, self.cc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +104,7 @@ impl ChildKeysPublic {
|
||||
)]
|
||||
impl<'a> From<&'a ChildKeysPublic> for &'a lee::PrivateKey {
|
||||
fn from(value: &'a ChildKeysPublic) -> Self {
|
||||
&value.csk
|
||||
&value.ssk
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,30 +138,37 @@ mod tests {
|
||||
];
|
||||
let keys = ChildKeysPublic::root(seed);
|
||||
|
||||
let expected_ccc = [
|
||||
let expected_cc = [
|
||||
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
|
||||
13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
let expected_sk: PrivateKey = PrivateKey::try_new([
|
||||
40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36,
|
||||
202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247,
|
||||
244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245,
|
||||
let expected_ssk: PrivateKey = PrivateKey::try_new([
|
||||
207, 4, 246, 223, 104, 72, 19, 85, 14, 122, 194, 82, 32, 163, 60, 57, 8, 25, 209, 91,
|
||||
254, 107, 76, 238, 31, 68, 236, 192, 154, 78, 105, 118,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == keys.ccc);
|
||||
assert!(expected_csk == keys.csk);
|
||||
assert!(expected_cpk == keys.cpk);
|
||||
let expected_pk: PublicKey = PublicKey::try_new([
|
||||
188, 163, 203, 45, 151, 154, 230, 254, 123, 114, 158, 130, 19, 182, 164, 143, 150, 131,
|
||||
176, 7, 27, 58, 204, 116, 5, 247, 0, 255, 111, 160, 52, 201,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_cc == keys.cc);
|
||||
assert!(expected_ssk == keys.ssk);
|
||||
assert!(expected_sk == keys.sk);
|
||||
assert!(expected_pk == keys.pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harden_child_keys_generation() {
|
||||
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,
|
||||
@ -171,93 +179,32 @@ mod tests {
|
||||
let cci = (2_u32).pow(31) + 13;
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
let expected_cc = [
|
||||
149, 226, 13, 4, 194, 12, 69, 29, 9, 234, 209, 119, 98, 4, 128, 91, 37, 103, 192, 31,
|
||||
130, 126, 123, 20, 90, 34, 173, 209, 101, 248, 155, 36,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
let expected_sk: PrivateKey = PrivateKey::try_new([
|
||||
9, 65, 33, 228, 25, 82, 219, 117, 91, 217, 11, 223, 144, 85, 246, 26, 123, 216, 107,
|
||||
213, 33, 52, 188, 22, 198, 246, 71, 46, 245, 174, 16, 47,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
142, 143, 238, 159, 105, 165, 224, 252, 108, 62, 53, 209, 176, 219, 249, 38, 90, 241,
|
||||
201, 81, 194, 146, 236, 5, 83, 152, 238, 243, 138, 16, 229, 15,
|
||||
let expected_ssk: PrivateKey = PrivateKey::try_new([
|
||||
100, 37, 212, 81, 40, 233, 72, 156, 177, 139, 50, 114, 136, 157, 202, 132, 203, 246,
|
||||
252, 242, 13, 81, 42, 100, 159, 240, 187, 252, 202, 108, 25, 105,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonharden_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 cci = 13;
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
79, 228, 242, 119, 211, 203, 198, 175, 95, 36, 4, 234, 139, 45, 137, 138, 54, 211, 187,
|
||||
16, 28, 79, 80, 232, 216, 101, 145, 19, 101, 220, 217, 141,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
185, 147, 32, 242, 145, 91, 123, 77, 42, 33, 134, 84, 12, 165, 117, 70, 158, 201, 95,
|
||||
153, 14, 12, 92, 235, 128, 156, 194, 169, 68, 35, 165, 127,
|
||||
let expected_pk: PublicKey = PublicKey::try_new([
|
||||
210, 59, 119, 137, 21, 153, 82, 22, 195, 82, 12, 16, 80, 156, 125, 199, 19, 173, 46,
|
||||
224, 213, 144, 165, 126, 70, 129, 171, 141, 77, 212, 108, 233,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
119, 16, 145, 121, 97, 244, 186, 35, 136, 34, 140, 171, 206, 139, 11, 208, 207, 121,
|
||||
158, 45, 28, 22, 140, 98, 161, 179, 212, 173, 238, 220, 2, 34,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_case_child_keys_generation_2_power_31() {
|
||||
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 cci = (2_u32).pow(31); //equivant to 0, thus non-harden.
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
221, 208, 47, 189, 174, 152, 33, 25, 151, 114, 233, 191, 57, 15, 40, 140, 46, 87, 126,
|
||||
58, 215, 40, 246, 111, 166, 113, 183, 145, 173, 11, 27, 182,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
223, 29, 87, 189, 126, 24, 117, 225, 190, 57, 0, 143, 207, 168, 231, 139, 170, 192, 81,
|
||||
254, 126, 10, 115, 42, 141, 157, 70, 171, 199, 231, 198, 132,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
96, 123, 245, 51, 214, 216, 215, 205, 70, 145, 105, 221, 166, 169, 122, 27, 94, 112,
|
||||
228, 110, 249, 177, 85, 173, 180, 248, 185, 199, 112, 246, 83, 33,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
assert!(expected_cc == child_keys.cc);
|
||||
assert!(expected_ssk == child_keys.ssk);
|
||||
assert!(expected_sk == child_keys.sk);
|
||||
assert!(expected_pk == child_keys.pk);
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,8 +347,8 @@ mod tests {
|
||||
|
||||
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
||||
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
||||
172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178,
|
||||
38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247
|
||||
10, 231, 159, 65, 236, 46, 205, 5, 172, 89, 250, 29, 123, 195, 212, 137, 155, 111, 40,
|
||||
120, 53, 28, 124, 54, 224, 170, 119, 208, 2, 72, 75, 50
|
||||
])));
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ use crate::{
|
||||
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::Ciphertext,
|
||||
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
@ -33,6 +33,8 @@ pub enum InputAccountIdentity {
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
|
||||
/// matched against `pre_state.account_id`.
|
||||
PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -40,6 +42,8 @@ pub enum InputAccountIdentity {
|
||||
/// Update of an authorized standalone private account: existing on-chain commitment, with
|
||||
/// membership proof.
|
||||
PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
@ -48,6 +52,8 @@ 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,
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -57,6 +63,8 @@ pub enum InputAccountIdentity {
|
||||
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
|
||||
/// as the 4th input.
|
||||
PrivatePdaInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -72,6 +80,8 @@ 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,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
@ -123,7 +133,7 @@ impl InputAccountIdentity {
|
||||
pub struct PrivacyPreservingCircuitOutput {
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
pub ciphertexts: Vec<Ciphertext>,
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
@ -148,6 +158,7 @@ mod tests {
|
||||
use crate::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
encryption::Ciphertext,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -181,7 +192,11 @@ mod tests {
|
||||
data: b"post state data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
|
||||
}],
|
||||
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],
|
||||
encrypted_private_post_states: vec![EncryptedAccountData {
|
||||
ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]),
|
||||
epk: EphemeralPublicKey(vec![9, 9, 9]),
|
||||
view_tag: 42,
|
||||
}],
|
||||
new_commitments: vec![Commitment::new(
|
||||
&AccountId::new([1; 32]),
|
||||
&Account::default(),
|
||||
|
||||
@ -52,6 +52,7 @@ impl std::fmt::Debug for Commitment {
|
||||
impl Commitment {
|
||||
/// Generates the commitment to a private account owned by user for `account_id`:
|
||||
/// SHA256( `Comm_DS` || `account_id` || `program_owner` || balance || nonce || SHA256(data)).
|
||||
// TODO: Accept account_id by value as it's Copy
|
||||
#[must_use]
|
||||
pub fn new(account_id: &AccountId, account: &Account) -> Self {
|
||||
const COMMITMENT_PREFIX: &[u8; 32] =
|
||||
|
||||
@ -6,7 +6,7 @@ use chacha20::{
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
@ -17,6 +17,11 @@ pub type Scalar = [u8; 32];
|
||||
#[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.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
|
||||
pub struct EncryptionScheme;
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
@ -36,6 +41,45 @@ impl std::fmt::Debug for Ciphertext {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ViewTag = u8;
|
||||
|
||||
/// Encrypted private-account note for one output.
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
ciphertext: Ciphertext,
|
||||
npk: &crate::NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
epk: EphemeralPublicKey,
|
||||
) -> Self {
|
||||
let view_tag = Self::compute_view_tag(npk, vpk);
|
||||
Self {
|
||||
ciphertext,
|
||||
epk,
|
||||
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 {
|
||||
#[must_use]
|
||||
pub fn encrypt(
|
||||
|
||||
@ -2,12 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::SharedSecretKey;
|
||||
|
||||
/// 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.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
use crate::{EphemeralPublicKey, SharedSecretKey};
|
||||
|
||||
/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate).
|
||||
#[derive(
|
||||
|
||||
@ -10,7 +10,9 @@ pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
compute_digest_for_path,
|
||||
};
|
||||
pub use encryption::{EncryptionScheme, SharedSecretKey};
|
||||
pub use encryption::{
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, SharedSecretKey, ViewTag,
|
||||
};
|
||||
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
|
||||
@ -97,6 +97,7 @@ impl Nullifier {
|
||||
}
|
||||
|
||||
/// Computes a nullifier for an account initialization.
|
||||
// TODO: Accept account_id by value as it's Copy
|
||||
#[must_use]
|
||||
pub fn for_account_initialization(account_id: &AccountId) -> Self {
|
||||
const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00";
|
||||
|
||||
@ -178,8 +178,8 @@ mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
@ -201,7 +201,7 @@ mod tests {
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[idx],
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
@ -210,6 +210,17 @@ mod tests {
|
||||
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();
|
||||
@ -257,6 +268,11 @@ mod tests {
|
||||
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,
|
||||
@ -274,10 +290,10 @@ mod tests {
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[0],
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
@ -356,6 +372,11 @@ mod tests {
|
||||
.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
|
||||
@ -364,6 +385,11 @@ mod tests {
|
||||
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,
|
||||
@ -378,10 +404,10 @@ mod tests {
|
||||
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.ciphertexts.len(), 2);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[0],
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
@ -390,7 +416,7 @@ mod tests {
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[1],
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
@ -432,6 +458,11 @@ mod tests {
|
||||
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,
|
||||
@ -461,6 +492,8 @@ mod tests {
|
||||
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,
|
||||
@ -508,6 +541,8 @@ mod tests {
|
||||
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,
|
||||
@ -561,6 +596,8 @@ mod tests {
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
@ -618,6 +655,11 @@ mod tests {
|
||||
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,
|
||||
@ -647,6 +689,8 @@ mod tests {
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize)
|
||||
.unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
@ -691,6 +735,8 @@ mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
@ -735,6 +781,8 @@ mod tests {
|
||||
.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(),
|
||||
@ -789,6 +837,8 @@ mod tests {
|
||||
Program::serialize_instruction((seed, 1_u128, auth_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(),
|
||||
@ -827,6 +877,8 @@ mod tests {
|
||||
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,
|
||||
@ -870,6 +922,8 @@ mod tests {
|
||||
Program::serialize_instruction((seed, 1_u128, auth_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(),
|
||||
|
||||
@ -1,52 +1,16 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, Nonce},
|
||||
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
pub use lee_core::{EncryptedAccountData, ViewTag};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::{AccountId, error::LeeError};
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
pub type ViewTag = u8;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
impl EncryptedAccountData {
|
||||
fn new(
|
||||
ciphertext: Ciphertext,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
epk: EphemeralPublicKey,
|
||||
) -> Self {
|
||||
let view_tag = Self::compute_view_tag(npk, vpk);
|
||||
Self {
|
||||
ciphertext,
|
||||
epk,
|
||||
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: &NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/ViewTag/");
|
||||
hasher.update(npk.to_byte_array());
|
||||
hasher.update(vpk.to_bytes());
|
||||
let digest: [u8; 32] = hasher.finalize().into();
|
||||
digest[0]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
@ -92,28 +56,13 @@ impl Message {
|
||||
pub fn try_from_circuit_output(
|
||||
public_account_ids: Vec<AccountId>,
|
||||
nonces: Vec<Nonce>,
|
||||
public_keys: Vec<(NullifierPublicKey, ViewingPublicKey, EphemeralPublicKey)>,
|
||||
output: PrivacyPreservingCircuitOutput,
|
||||
) -> Result<Self, LeeError> {
|
||||
if public_keys.len() != output.ciphertexts.len() {
|
||||
return Err(LeeError::InvalidInput(
|
||||
"Ephemeral public keys and ciphertexts length mismatch".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let encrypted_private_post_states = output
|
||||
.ciphertexts
|
||||
.into_iter()
|
||||
.zip(public_keys)
|
||||
.map(|(ciphertext, (npk, vpk, epk))| {
|
||||
EncryptedAccountData::new(ciphertext, &npk, &vpk, epk)
|
||||
})
|
||||
.collect();
|
||||
Ok(Self {
|
||||
public_account_ids,
|
||||
nonces,
|
||||
public_post_states: output.public_post_states,
|
||||
encrypted_private_post_states,
|
||||
encrypted_private_post_states: output.encrypted_private_post_states,
|
||||
new_commitments: output.new_commitments,
|
||||
new_nullifiers: output.new_nullifiers,
|
||||
block_validity_window: output.block_validity_window,
|
||||
|
||||
@ -498,6 +498,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elf_returns_the_program_bytecode_constant() {
|
||||
// `Program::elf` must return exactly the compile-time ELF, never an empty
|
||||
// or placeholder slice. Catches mutations returning `Vec::leak(Vec::new())`,
|
||||
// `Vec::leak(vec![0])`, or `Vec::leak(vec![1])`.
|
||||
let at = Program::authenticated_transfer_program();
|
||||
assert!(!at.elf().is_empty());
|
||||
assert_eq!(at.elf(), AUTHENTICATED_TRANSFER_ELF);
|
||||
|
||||
let token = Program::token();
|
||||
assert!(!token.elf().is_empty());
|
||||
assert_eq!(token.elf(), TOKEN_ELF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = Program::simple_balance_transfer();
|
||||
|
||||
@ -16,3 +16,18 @@ impl Message {
|
||||
self.bytecode
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Message;
|
||||
|
||||
#[test]
|
||||
fn bytecode_roundtrip() {
|
||||
// `Message::new(b).into_bytecode()` must return exactly `b`. Catches
|
||||
// mutations of `into_bytecode` returning `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic
|
||||
assert_eq!(Message::new(bytecode.clone()).into_bytecode(), bytecode);
|
||||
assert!(Message::new(vec![]).into_bytecode().is_empty());
|
||||
assert_eq!(Message::new(vec![0xAB]).into_bytecode(), vec![0xAB_u8]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use k256::ecdsa::signature::hazmat::PrehashVerifier as _;
|
||||
pub use private_key::PrivateKey;
|
||||
pub use public_key::PublicKey;
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
@ -72,7 +73,7 @@ impl Signature {
|
||||
return false;
|
||||
};
|
||||
|
||||
pk.verify_raw(bytes, &sig).is_ok()
|
||||
pk.verify_prehash(bytes, &sig).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
|
||||
use rand::{Rng as _, rngs::OsRng};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::error::LeeError;
|
||||
|
||||
@ -60,6 +62,29 @@ impl PrivateKey {
|
||||
pub const fn value(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// `tweak` produces the "tweaked secret key" (`sk`) given a public account's `ssk`.
|
||||
/// We use "tweaked keys" to shield the public accounts' `ssk` against quantum threats.
|
||||
/// The "tweaked keys" are used for Schnorr Signatures (BIP-340).
|
||||
/// The usage of these keys will be greatly reduced once LEE is upgraded to use a PQ signatures.
|
||||
pub fn tweak(value: &[u8; 32]) -> Result<Self, LeeError> {
|
||||
if !Self::is_valid_key(*value) {
|
||||
return Err(LeeError::InvalidPrivateKey);
|
||||
}
|
||||
|
||||
let sk = k256::SecretKey::from_slice(value).map_err(|_e| LeeError::InvalidPrivateKey)?;
|
||||
|
||||
let hashed: [u8; 32] =
|
||||
Sha256::digest(sk.public_key().to_encoded_point(true).as_bytes()).into();
|
||||
|
||||
let sk = sk.to_nonzero_scalar();
|
||||
|
||||
let scalar = k256::Scalar::from_repr(hashed.into())
|
||||
.into_option()
|
||||
.ok_or(LeeError::InvalidPrivateKey)?;
|
||||
|
||||
Self::try_new(sk.add(&scalar).to_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -75,4 +100,33 @@ mod tests {
|
||||
fn produce_key() {
|
||||
let _key = PrivateKey::new_os_random();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tweak_rejects_zero_key() {
|
||||
assert!(matches!(
|
||||
PrivateKey::tweak(&[0_u8; 32]),
|
||||
Err(LeeError::InvalidPrivateKey)
|
||||
));
|
||||
}
|
||||
|
||||
// tweak: 0xFF…FF exceeds the secp256k1 curve order
|
||||
#[test]
|
||||
fn tweak_rejects_out_of_range_key() {
|
||||
assert!(matches!(
|
||||
PrivateKey::tweak(&[0xFF; 32]),
|
||||
Err(LeeError::InvalidPrivateKey)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tweak_deterministic() {
|
||||
let tweaked = PrivateKey::tweak(&[1_u8; 32]).unwrap();
|
||||
assert_eq!(
|
||||
tweaked.value(),
|
||||
&[
|
||||
242, 210, 33, 19, 65, 108, 136, 176, 179, 128, 110, 210, 107, 193, 168, 112, 206,
|
||||
171, 86, 238, 131, 10, 39, 36, 44, 39, 246, 20, 46, 193, 204, 66
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,8 +418,8 @@ pub mod tests {
|
||||
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use lee_core::{
|
||||
BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey, Timestamp,
|
||||
BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
|
||||
NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{
|
||||
@ -613,6 +613,48 @@ pub mod tests {
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_system_accounts_have_expected_contents() {
|
||||
// System-account IDs must be distinct and non-default, and the genesis
|
||||
// faucet/bridge accounts must carry their expected field values. Catches
|
||||
// mutations that replace `system_faucet_account`/`system_bridge_account`
|
||||
// with `Default::default()`, delete their `balance`/`program_owner`
|
||||
// fields, or replace `system_bridge_account_id` with `Default::default()`.
|
||||
let faucet_id = system_faucet_account_id();
|
||||
let bridge_id = system_bridge_account_id();
|
||||
assert_ne!(bridge_id, AccountId::default());
|
||||
assert_ne!(faucet_id, bridge_id);
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
let default_owner = Account::default().program_owner;
|
||||
|
||||
let faucet = state.get_account_by_id(faucet_id);
|
||||
assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX");
|
||||
assert_ne!(
|
||||
faucet.program_owner, default_owner,
|
||||
"faucet must have a non-default program_owner"
|
||||
);
|
||||
|
||||
let bridge = state.get_account_by_id(bridge_id);
|
||||
assert_ne!(
|
||||
bridge.program_owner, default_owner,
|
||||
"bridge must have a non-default program_owner"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_commitment_set_digest_differs_from_empty_state() {
|
||||
// The genesis state inserts DUMMY_COMMITMENT, so its commitment-set digest
|
||||
// must differ from a freshly-created empty state's all-zero root. Catches
|
||||
// the mutation that replaces `commitment_set_digest` with `Default::default()`.
|
||||
let genesis = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
let empty = V03State::new();
|
||||
assert_ne!(
|
||||
genesis.commitment_set_digest(),
|
||||
empty.commitment_set_digest()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_genesis() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
@ -1376,6 +1418,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -1388,7 +1435,6 @@ pub mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender_keys.account_id()],
|
||||
vec![sender_nonce],
|
||||
vec![(recipient_keys.npk(), recipient_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1429,6 +1475,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: epk_1,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret_1,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -1437,6 +1488,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: epk_2,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret_2,
|
||||
identifier: 0,
|
||||
@ -1446,16 +1502,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(sender_keys.npk(), sender_keys.vpk(), epk_1),
|
||||
(recipient_keys.npk(), recipient_keys.vpk(), epk_2),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -1494,6 +1541,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -1507,13 +1559,8 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![*recipient_account_id],
|
||||
vec![],
|
||||
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -1630,6 +1677,79 @@ pub mod tests {
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
}
|
||||
|
||||
fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 100,
|
||||
nonce: Nonce(0xdead_beef),
|
||||
..Account::default()
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
let state = V03State::new_with_genesis_accounts(&[], vec![], 0)
|
||||
.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)
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@ -1992,6 +2112,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2003,6 +2128,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2048,6 +2178,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2059,6 +2194,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2104,6 +2244,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2115,6 +2260,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2160,6 +2310,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2171,6 +2326,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2216,6 +2376,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2227,6 +2392,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2270,6 +2440,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2281,6 +2456,11 @@ pub mod tests {
|
||||
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: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2326,6 +2506,8 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2359,6 +2541,8 @@ pub mod tests {
|
||||
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: u128::MAX,
|
||||
@ -2370,7 +2554,7 @@ pub mod tests {
|
||||
let (output, _proof) = result.expect("private PDA claim should succeed");
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
}
|
||||
@ -2400,6 +2584,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()),
|
||||
npk: npk_b,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2437,6 +2623,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2477,6 +2665,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2516,12 +2706,16 @@ pub mod tests {
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()),
|
||||
npk: keys_a.npk(),
|
||||
ssk: shared_a,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()),
|
||||
npk: keys_b.npk(),
|
||||
ssk: shared_b,
|
||||
identifier: u128::MAX,
|
||||
@ -2564,6 +2758,8 @@ pub mod tests {
|
||||
vec![owned_pre_state],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2652,12 +2848,22 @@ pub mod tests {
|
||||
Program::serialize_instruction(100_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (1, vec![]),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (1, vec![]),
|
||||
@ -3003,6 +3209,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3016,13 +3227,9 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_account_id],
|
||||
vec![Nonce(0)],
|
||||
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let 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);
|
||||
@ -3129,6 +3336,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: to_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&to_keys.npk(),
|
||||
&to_keys.vpk(),
|
||||
),
|
||||
ssk: to_ss,
|
||||
nsk: from_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3137,6 +3349,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: from_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&from_keys.npk(),
|
||||
&from_keys.vpk(),
|
||||
),
|
||||
ssk: from_ss,
|
||||
nsk: to_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3149,16 +3366,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(to_keys.npk(), to_keys.vpk(), to_epk),
|
||||
(from_keys.npk(), from_keys.vpk(), from_epk),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.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);
|
||||
|
||||
@ -3406,6 +3614,11 @@ pub mod tests {
|
||||
vec![authorized_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3415,13 +3628,7 @@ pub mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create message from circuit output
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -3454,6 +3661,11 @@ pub mod tests {
|
||||
vec![unauthorized_account],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
npk: private_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3462,13 +3674,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -3506,6 +3712,11 @@ pub mod tests {
|
||||
vec![authorized_account.clone()],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3514,13 +3725,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -3553,6 +3758,11 @@ pub mod tests {
|
||||
vec![account_metadata],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret2,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3630,6 +3840,11 @@ pub mod tests {
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
|
||||
.0,
|
||||
nsk: sender_keys.nsk,
|
||||
@ -3657,6 +3872,11 @@ pub mod tests {
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
|
||||
.0,
|
||||
nsk: sender_keys.nsk,
|
||||
@ -3718,6 +3938,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
ssk: recipient,
|
||||
nsk: recipient_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3872,6 +4097,11 @@ pub mod tests {
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3880,13 +4110,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(account_keys.npk(), account_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.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)
|
||||
@ -3941,6 +4165,11 @@ pub mod tests {
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3949,13 +4178,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(account_keys.npk(), account_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.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)
|
||||
@ -4504,6 +4727,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: alice_epk_0.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_0,
|
||||
identifier: 0,
|
||||
@ -4513,13 +4741,9 @@ pub mod tests {
|
||||
&auth_transfer.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4544,6 +4768,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: alice_epk_1.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_1,
|
||||
identifier: 1,
|
||||
@ -4553,13 +4782,9 @@ pub mod tests {
|
||||
&auth_transfer.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4587,6 +4812,11 @@ pub mod tests {
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: alice_epk_0,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
ssk: alice_shared_0,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -4600,13 +4830,9 @@ pub mod tests {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![Nonce(0)],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_0)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4628,6 +4854,11 @@ pub mod tests {
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: alice_epk_1,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
ssk: alice_shared_1,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -4641,13 +4872,8 @@ pub mod tests {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_1)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4690,6 +4916,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
nsk: alice_keys.nsk,
|
||||
ssk: alice_shared_1_refund,
|
||||
membership_proof: state
|
||||
@ -4702,17 +4933,9 @@ pub mod tests {
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![recipient_nonce],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
|
||||
@ -492,12 +492,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
ciphertexts: message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|value| value.ciphertext)
|
||||
.collect(),
|
||||
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,
|
||||
@ -526,6 +521,44 @@ mod tests {
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
#[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).
|
||||
use authenticated_transfer_core::Instruction as AtInstruction;
|
||||
|
||||
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_genesis_accounts(&[(from, 100)], vec![], 0);
|
||||
let program_id = Program::authenticated_transfer_program().id();
|
||||
let message = Message::try_new(
|
||||
program_id,
|
||||
vec![from, to],
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
AtInstruction::Transfer { amount: 5 },
|
||||
)
|
||||
.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.
|
||||
///
|
||||
@ -542,7 +575,7 @@ mod tests {
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity, SharedSecretKey,
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -626,6 +659,11 @@ mod tests {
|
||||
// [2] recipient — first seen in authenticated_transfer's program_output.pre_states
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: attacker_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&attacker_keys.npk(),
|
||||
&attacker_keys.vpk(),
|
||||
),
|
||||
ssk: attacker_ssk,
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
@ -650,7 +688,6 @@ mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
@ -690,7 +727,7 @@ mod tests {
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity, SharedSecretKey,
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -782,6 +819,11 @@ mod tests {
|
||||
// so PrivateAuthorizedUpdate is not an option.
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: attacker_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&attacker_keys.npk(),
|
||||
&attacker_keys.vpk(),
|
||||
),
|
||||
ssk: attacker_ssk,
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
@ -807,7 +849,6 @@ mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -5,14 +5,12 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256, digest::FixedOutput as _};
|
||||
|
||||
use crate::{HashType, transaction::LeeTransaction};
|
||||
pub type MantleMsgId = [u8; 32];
|
||||
pub type BlockHash = HashType;
|
||||
|
||||
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
|
||||
pub struct BlockMeta {
|
||||
pub id: BlockId,
|
||||
pub hash: BlockHash,
|
||||
pub msg_id: MantleMsgId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -55,7 +53,6 @@ pub struct Block {
|
||||
pub header: BlockHeader,
|
||||
pub body: BlockBody,
|
||||
pub bedrock_status: BedrockStatus,
|
||||
pub bedrock_parent_id: MantleMsgId,
|
||||
}
|
||||
|
||||
impl Serialize for Block {
|
||||
@ -80,11 +77,7 @@ pub struct HashableBlockData {
|
||||
|
||||
impl HashableBlockData {
|
||||
#[must_use]
|
||||
pub fn into_pending_block(
|
||||
self,
|
||||
signing_key: &lee::PrivateKey,
|
||||
bedrock_parent_id: MantleMsgId,
|
||||
) -> Block {
|
||||
pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block {
|
||||
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();
|
||||
@ -111,7 +104,6 @@ impl HashableBlockData {
|
||||
transactions: self.transactions,
|
||||
},
|
||||
bedrock_status: BedrockStatus::Pending,
|
||||
bedrock_parent_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,3 +53,33 @@ impl From<BasicAuth> for BasicAuthCredentials {
|
||||
Self::new(value.username, value.password)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use super::BasicAuth;
|
||||
|
||||
#[test]
|
||||
fn parse_preserves_non_empty_password() {
|
||||
let auth = BasicAuth::from_str("user:secret").expect("must parse");
|
||||
assert_eq!(auth.username, "user");
|
||||
assert_eq!(auth.password.as_deref(), Some("secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_password_is_none() {
|
||||
// A trailing colon means an empty password, which must become `None`.
|
||||
// Catches deletion of `!` in `.filter(|p| !p.is_empty())`, which would
|
||||
// instead yield `Some("")`.
|
||||
let auth = BasicAuth::from_str("user:").expect("must parse");
|
||||
assert_eq!(auth.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_username_only_has_no_password() {
|
||||
let auth = BasicAuth::from_str("alice").expect("must parse");
|
||||
assert_eq!(auth.username, "alice");
|
||||
assert_eq!(auth.password, None);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,4 +93,16 @@ mod tests {
|
||||
let deserialized = HashType::from_str(&serialized).unwrap();
|
||||
assert_eq!(original, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_ref_returns_exact_inner_bytes() {
|
||||
// `HashType::as_ref` must return exactly the inner `[u8; 32]` — not an
|
||||
// empty slice or a placeholder. Catches mutations of `as_ref` that return
|
||||
// `Vec::leak(Vec::new())`, `vec![0]`, or `vec![1]`.
|
||||
let known = [0x42_u8; 32];
|
||||
let hash = HashType(known);
|
||||
assert_eq!(hash.as_ref(), &known);
|
||||
assert_eq!(hash.as_ref().len(), 32);
|
||||
assert_eq!(HashType([0_u8; 32]).as_ref().len(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ pub fn produce_dummy_block(
|
||||
transactions,
|
||||
};
|
||||
|
||||
block_data.into_pending_block(&sequencer_sign_key_for_testing(), [0; 32])
|
||||
block_data.into_pending_block(&sequencer_sign_key_for_testing())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
||||
@ -78,7 +78,31 @@ impl LeeTransaction {
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<ValidatedStateDiff, lee::error::LeeError> {
|
||||
let diff = match self {
|
||||
let diff = self.compute_state_diff(state, block_id, timestamp)?;
|
||||
|
||||
let restricted_modification_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(lee::system_faucet_account_id()));
|
||||
for account_id in restricted_modification_accounts {
|
||||
validate_doesnt_modify_account(state, &diff, account_id)?;
|
||||
}
|
||||
|
||||
self.validate_bridge_account_modification(state, &diff)?;
|
||||
|
||||
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`].
|
||||
fn compute_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<ValidatedStateDiff, lee::error::LeeError> {
|
||||
match self {
|
||||
Self::Public(tx) => {
|
||||
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
|
||||
}
|
||||
@ -88,17 +112,7 @@ impl LeeTransaction {
|
||||
Self::ProgramDeployment(tx) => {
|
||||
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
|
||||
}
|
||||
}?;
|
||||
|
||||
let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([
|
||||
lee::system_faucet_account_id(),
|
||||
lee::system_bridge_account_id(),
|
||||
]);
|
||||
for account_id in system_accounts {
|
||||
validate_doesnt_modify_account(state, &diff, account_id)?;
|
||||
}
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Validates the transaction against the current state, rejects modifications to clock
|
||||
@ -115,6 +129,62 @@ impl LeeTransaction {
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Similar to [`Self::execute_check_on_state`], but skips the system-account guard.
|
||||
///
|
||||
/// 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(
|
||||
self,
|
||||
state: &mut V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, lee::error::LeeError> {
|
||||
let diff = self
|
||||
.compute_state_diff(state, block_id, timestamp)
|
||||
.inspect_err(|err| warn!("Error at transition {err:#?}"))?;
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn validate_bridge_account_modification(
|
||||
&self,
|
||||
state: &V03State,
|
||||
diff: &ValidatedStateDiff,
|
||||
) -> Result<(), lee::error::LeeError> {
|
||||
let bridge_account_id = lee::system_bridge_account_id();
|
||||
let pre = state.get_account_by_id(bridge_account_id);
|
||||
let Some(post) = diff.public_diff().get(&bridge_account_id).cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Self::Public(_) = self else {
|
||||
return Err(lee::error::LeeError::InvalidInput(format!(
|
||||
"Non-public transaction cannot modify system bridge account {bridge_account_id}"
|
||||
)));
|
||||
};
|
||||
|
||||
let only_balance_increased = {
|
||||
let expected_pre = lee::Account {
|
||||
balance: pre.balance,
|
||||
..post.clone()
|
||||
};
|
||||
(expected_pre == pre) && (pre.balance <= post.balance)
|
||||
};
|
||||
|
||||
if only_balance_increased {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(lee::error::LeeError::InvalidInput(format!(
|
||||
"Transaction modifies restricted system bridge account {bridge_account_id}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lee::PublicTransaction> for LeeTransaction {
|
||||
@ -188,3 +258,47 @@ fn validate_doesnt_modify_account(
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee::{
|
||||
AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PrivateKey, PublicKey, V03State,
|
||||
system_bridge_account_id, system_faucet_account_id,
|
||||
};
|
||||
|
||||
use crate::test_utils::create_transaction_native_token_transfer;
|
||||
|
||||
#[test]
|
||||
fn system_account_ids_are_distinct_and_non_default() {
|
||||
let faucet = system_faucet_account_id();
|
||||
let bridge = system_bridge_account_id();
|
||||
assert_ne!(faucet, AccountId::default());
|
||||
assert_ne!(bridge, AccountId::default());
|
||||
assert_ne!(faucet, bridge);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_on_state_rejects_modifying_a_system_account() {
|
||||
// A native transfer that credits a clock system account *changes* that
|
||||
// account, so `validate_doesnt_modify_account` must reject it. Catches
|
||||
// the `!=` → `==` inversion at `validate_doesnt_modify_account` (a changed
|
||||
// account would no longer be flagged) and `public_diff → HashMap::new()`
|
||||
// (an empty diff hides the modification).
|
||||
let sender_key = PrivateKey::try_new([5_u8; 32]).expect("valid key");
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000)], vec![], 0);
|
||||
|
||||
let tx = create_transaction_native_token_transfer(
|
||||
sender_id,
|
||||
0,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
100,
|
||||
&sender_key,
|
||||
);
|
||||
|
||||
assert!(
|
||||
tx.validate_on_state(&state, 1, 0).is_err(),
|
||||
"validate_on_state must reject a transfer that credits a clock system account",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ pub fn BlockPreview(block: Block) -> impl IntoView {
|
||||
},
|
||||
body: BlockBody { transactions },
|
||||
bedrock_status,
|
||||
bedrock_parent_id: _,
|
||||
} = block;
|
||||
|
||||
let tx_count = transactions.len();
|
||||
|
||||
@ -64,7 +64,6 @@ pub fn BlockPage() -> impl IntoView {
|
||||
transactions,
|
||||
},
|
||||
bedrock_status,
|
||||
bedrock_parent_id: _,
|
||||
} = blk;
|
||||
|
||||
let hash_str = hash.to_string();
|
||||
|
||||
@ -171,7 +171,10 @@ impl IndexerStore {
|
||||
transaction
|
||||
.clone()
|
||||
.transaction_stateless_check()?
|
||||
.execute_check_on_state(
|
||||
// FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to
|
||||
// sequencer-generated deposit tx'es;
|
||||
// CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions
|
||||
.execute_without_system_accounts_check_on_state(
|
||||
&mut state_guard,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
@ -238,10 +241,8 @@ mod tests {
|
||||
timestamp: 0,
|
||||
transactions: vec![clock_tx],
|
||||
};
|
||||
let genesis_block = genesis_block_data.into_pending_block(
|
||||
&common::test_utils::sequencer_sign_key_for_testing(),
|
||||
[0; 32],
|
||||
);
|
||||
let genesis_block = genesis_block_data
|
||||
.into_pending_block(&common::test_utils::sequencer_sign_key_for_testing());
|
||||
let mut prev_hash = Some(genesis_block.header.hash);
|
||||
storage
|
||||
.put_block(genesis_block, HeaderId::from([0_u8; 32]))
|
||||
|
||||
@ -320,13 +320,10 @@ typedef struct FfiVec_FfiTransaction {
|
||||
|
||||
typedef struct FfiVec_FfiTransaction FfiBlockBody;
|
||||
|
||||
typedef struct FfiBytes32 FfiMsgId;
|
||||
|
||||
typedef struct FfiBlock {
|
||||
struct FfiBlockHeader header;
|
||||
FfiBlockBody body;
|
||||
enum FfiBedrockStatus bedrock_status;
|
||||
FfiMsgId bedrock_parent_id;
|
||||
} FfiBlock;
|
||||
|
||||
typedef struct FfiOption_FfiBlock {
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
use indexer_service_protocol::{
|
||||
BedrockStatus, Block, BlockHeader, HashType, MantleMsgId, Signature,
|
||||
};
|
||||
use indexer_service_protocol::{BedrockStatus, Block, BlockHeader, HashType, Signature};
|
||||
|
||||
use crate::api::types::{
|
||||
FfiBlockId, FfiHashType, FfiMsgId, FfiOption, FfiSignature, FfiTimestamp, FfiVec,
|
||||
FfiBlockId, FfiHashType, FfiOption, FfiSignature, FfiTimestamp, FfiVec,
|
||||
transaction::free_ffi_transaction_vec, vectors::FfiBlockBody,
|
||||
};
|
||||
|
||||
@ -12,7 +10,6 @@ pub struct FfiBlock {
|
||||
pub header: FfiBlockHeader,
|
||||
pub body: FfiBlockBody,
|
||||
pub bedrock_status: FfiBedrockStatus,
|
||||
pub bedrock_parent_id: FfiMsgId,
|
||||
}
|
||||
|
||||
impl From<Block> for FfiBlock {
|
||||
@ -21,7 +18,6 @@ impl From<Block> for FfiBlock {
|
||||
header,
|
||||
body,
|
||||
bedrock_status,
|
||||
bedrock_parent_id,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
@ -33,7 +29,6 @@ impl From<Block> for FfiBlock {
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
bedrock_status: bedrock_status.into(),
|
||||
bedrock_parent_id: bedrock_parent_id.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,8 +121,6 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) {
|
||||
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
|
||||
let _: BedrockStatus = val.bedrock_status.into();
|
||||
|
||||
let _ = MantleMsgId(val.bedrock_parent_id.data);
|
||||
|
||||
unsafe {
|
||||
free_ffi_transaction_vec(ffi_tx_ffi_vec);
|
||||
};
|
||||
@ -166,8 +159,6 @@ pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) {
|
||||
#[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")]
|
||||
let _: BedrockStatus = value.bedrock_status.into();
|
||||
|
||||
let _ = MantleMsgId(value.bedrock_parent_id.data);
|
||||
|
||||
unsafe {
|
||||
free_ffi_transaction_vec(ffi_tx_ffi_vec);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexer_service_protocol::{AccountId, HashType, MantleMsgId, ProgramId, PublicKey, Signature};
|
||||
use indexer_service_protocol::{AccountId, HashType, ProgramId, PublicKey, Signature};
|
||||
|
||||
pub mod account;
|
||||
pub mod block;
|
||||
@ -68,7 +68,6 @@ impl From<FfiU128> for u128 {
|
||||
}
|
||||
|
||||
pub type FfiHashType = FfiBytes32;
|
||||
pub type FfiMsgId = FfiBytes32;
|
||||
pub type FfiBlockId = u64;
|
||||
pub type FfiTimestamp = u64;
|
||||
pub type FfiSignature = FfiBytes64;
|
||||
@ -82,12 +81,6 @@ impl From<HashType> for FfiHashType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MantleMsgId> for FfiMsgId {
|
||||
fn from(value: MantleMsgId) -> Self {
|
||||
Self { data: value.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Signature> for FfiSignature {
|
||||
fn from(value: Signature) -> Self {
|
||||
Self { data: value.0 }
|
||||
|
||||
@ -37,9 +37,6 @@ RUN cp "$(which r0vm)" /usr/local/bin/r0vm
|
||||
RUN test -x /usr/local/bin/r0vm
|
||||
RUN r0vm --version
|
||||
|
||||
# Install logos blockchain circuits
|
||||
RUN curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/main/scripts/setup-logos-blockchain-circuits.sh | bash
|
||||
|
||||
WORKDIR /indexer_service
|
||||
|
||||
# Planner stage - generates dependency recipe
|
||||
@ -83,9 +80,6 @@ COPY --from=builder --chown=indexer_service_user:indexer_service_user /usr/local
|
||||
# Copy r0vm binary from builder
|
||||
COPY --from=builder --chown=indexer_service_user:indexer_service_user /usr/local/bin/r0vm /usr/local/bin/r0vm
|
||||
|
||||
# Copy logos blockchain circuits from builder
|
||||
COPY --from=builder --chown=indexer_service_user:indexer_service_user /root/.logos-blockchain-circuits /home/indexer_service_user/.logos-blockchain-circuits
|
||||
|
||||
VOLUME /var/lib/indexer_service
|
||||
|
||||
# Expose default port
|
||||
|
||||
@ -4,8 +4,8 @@ use lee_core::account::Nonce;
|
||||
|
||||
use crate::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, Ciphertext, Commitment,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId,
|
||||
Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, Nullifier,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction,
|
||||
Signature, Transaction, ValidityWindow, WitnessSet,
|
||||
};
|
||||
@ -630,14 +630,12 @@ impl From<common::block::Block> for Block {
|
||||
header,
|
||||
body,
|
||||
bedrock_status,
|
||||
bedrock_parent_id,
|
||||
} = value;
|
||||
|
||||
Self {
|
||||
header: header.into(),
|
||||
body: body.into(),
|
||||
bedrock_status: bedrock_status.into(),
|
||||
bedrock_parent_id: MantleMsgId(bedrock_parent_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -650,14 +648,12 @@ impl TryFrom<Block> for common::block::Block {
|
||||
header,
|
||||
body,
|
||||
bedrock_status,
|
||||
bedrock_parent_id,
|
||||
} = value;
|
||||
|
||||
Ok(Self {
|
||||
header: header.try_into()?,
|
||||
body: body.try_into()?,
|
||||
bedrock_status: bedrock_status.into(),
|
||||
bedrock_parent_id: bedrock_parent_id.0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +145,6 @@ pub struct Block {
|
||||
pub header: BlockHeader,
|
||||
pub body: BlockBody,
|
||||
pub bedrock_status: BedrockStatus,
|
||||
pub bedrock_parent_id: MantleMsgId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
@ -358,13 +357,6 @@ impl FromStr for HashType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MantleMsgId(
|
||||
#[serde(with = "base64::arr")]
|
||||
#[schemars(with = "String", description = "base64-encoded Bedrock message id")]
|
||||
pub [u8; 32],
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum BedrockStatus {
|
||||
Pending,
|
||||
|
||||
@ -10,10 +10,10 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
|
||||
use indexer_service_protocol::{
|
||||
Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId,
|
||||
PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage,
|
||||
ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature,
|
||||
Transaction, ValidityWindow, WitnessSet,
|
||||
CommitmentSetDigest, Data, EncryptedAccountData, HashType, PrivacyPreservingMessage,
|
||||
PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction,
|
||||
ProgramId, PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow,
|
||||
WitnessSet,
|
||||
};
|
||||
use jsonrpsee::{
|
||||
core::{SubscriptionResult, async_trait},
|
||||
@ -432,7 +432,6 @@ fn build_mock_block(
|
||||
transactions: block_transactions,
|
||||
},
|
||||
bedrock_status,
|
||||
bedrock_parent_id: MantleMsgId([0; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@ -7,8 +7,9 @@ use zeroize::Zeroizing;
|
||||
|
||||
pub mod python_path;
|
||||
|
||||
/// NSK and VSK as fixed-length zeroizing byte arrays.
|
||||
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>);
|
||||
/// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte
|
||||
/// arrays.
|
||||
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 64]>);
|
||||
|
||||
// TODO: encrypt at rest alongside broader wallet storage encryption work.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -123,7 +124,7 @@ impl KeycardWallet {
|
||||
}
|
||||
|
||||
pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult<PublicKey> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
@ -190,7 +191,7 @@ impl KeycardWallet {
|
||||
path: &str,
|
||||
message: &[u8; 32],
|
||||
) -> PyResult<(Signature, PublicKey)> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
@ -239,13 +240,13 @@ impl KeycardWallet {
|
||||
};
|
||||
|
||||
let vsk = {
|
||||
if raw_vsk.len() != 32 {
|
||||
if raw_vsk.len() != 64 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
|
||||
"expected 32-byte VSK from keycard, got {} bytes",
|
||||
"expected 64-byte VSK from keycard, got {} bytes",
|
||||
raw_vsk.len()
|
||||
)));
|
||||
}
|
||||
let mut arr = Zeroizing::new([0_u8; 32]);
|
||||
let mut arr = Zeroizing::new([0_u8; 64]);
|
||||
arr.copy_from_slice(&raw_vsk);
|
||||
arr
|
||||
};
|
||||
@ -257,7 +258,7 @@ impl KeycardWallet {
|
||||
pin: &str,
|
||||
path: &str,
|
||||
) -> PyResult<PrivateKeyPair> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
|
||||
@ -48,7 +48,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> {
|
||||
|
||||
let sys = PyModule::import(py, "sys")?;
|
||||
let binding = sys.getattr("path")?;
|
||||
let sys_path = binding.downcast::<PyList>()?;
|
||||
let sys_path = binding.cast::<PyList>()?;
|
||||
|
||||
for path in &paths_to_add {
|
||||
let path_str = path.to_str().expect("Invalid path");
|
||||
|
||||
@ -19,6 +19,8 @@ faucet_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
@ -27,14 +29,14 @@ tempfile.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
rand.workspace = true
|
||||
borsh.workspace = true
|
||||
bytesize.workspace = true
|
||||
hex.workspace = true
|
||||
url.workspace = true
|
||||
num-bigint.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@ -46,3 +48,4 @@ mock = []
|
||||
futures.workspace = true
|
||||
test_program_methods.workspace = true
|
||||
lee = { workspace = true, features = ["test-utils"] }
|
||||
key_protocol.workspace = true
|
||||
|
||||
@ -1,21 +1,29 @@
|
||||
use std::{pin::Pin, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use common::block::Block;
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
pub use logos_blockchain_core::mantle::ops::channel::MsgId;
|
||||
pub use logos_blockchain_key_management_system_service::keys::Ed25519Key;
|
||||
use logos_blockchain_core::mantle::ops::channel::inscribe::Inscription;
|
||||
pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey};
|
||||
pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient,
|
||||
adapter::NodeHttpClient,
|
||||
sequencer::{Event, SequencerConfig as ZoneSdkSequencerConfig, SequencerHandle, ZoneSequencer},
|
||||
state::{DepositInfo, FinalizedOp, InscriptionInfo},
|
||||
sequencer::{
|
||||
DepositInfo, Event, FinalizedOp, InscriptionInfo,
|
||||
SequencerConfig as ZoneSdkSequencerConfig, WithdrawArg, WithdrawInfo, ZoneSequencer,
|
||||
},
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::{sync::mpsc, task::JoinHandle};
|
||||
|
||||
use crate::config::BedrockConfig;
|
||||
|
||||
/// Channel capacity for the publish inbox. One publish per produced block, drained
|
||||
/// in microseconds by the drive task — 32 is huge headroom and just provides
|
||||
/// backpressure if the drive task stalls (reconnect, long backfill).
|
||||
const PUBLISH_INBOX_CAPACITY: usize = 32;
|
||||
|
||||
/// Sink for `Event::Published` checkpoints emitted by the drive task.
|
||||
/// Caller is responsible for persistence (e.g. writing to rocksdb).
|
||||
pub type CheckpointSink = Box<dyn Fn(SequencerCheckpoint) + Send + 'static>;
|
||||
@ -29,8 +37,16 @@ pub type FinalizedBlockSink = Box<dyn Fn(u64) + Send + 'static>;
|
||||
pub type OnDepositEventSink =
|
||||
Box<dyn Fn(DepositInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||
|
||||
/// Sink for finalized Bedrock withdraw events.
|
||||
pub type OnWithdrawEventSink =
|
||||
Box<dyn Fn(WithdrawInfo) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + 'static>;
|
||||
|
||||
#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")]
|
||||
pub trait BlockPublisherTrait: Clone {
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "Looks better than bundling all those callbacks into a struct"
|
||||
)]
|
||||
async fn new(
|
||||
config: &BedrockConfig,
|
||||
bedrock_signing_key: Ed25519Key,
|
||||
@ -39,17 +55,18 @@ pub trait BlockPublisherTrait: Clone {
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
) -> Result<Self>;
|
||||
|
||||
/// Fire-and-forget publish. Zone-sdk drives the actual submission and
|
||||
/// retries internally; this just hands the payload off.
|
||||
async fn publish_block(&self, block: &Block) -> Result<()>;
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Real block publisher backed by zone-sdk's `ZoneSequencer`.
|
||||
#[derive(Clone)]
|
||||
pub struct ZoneSdkPublisher {
|
||||
handle: SequencerHandle<NodeHttpClient>,
|
||||
publish_tx: mpsc::Sender<(Inscription, Vec<WithdrawArg>)>,
|
||||
// Aborts the drive task when the last clone is dropped.
|
||||
_drive_task: Arc<DriveTaskGuard>,
|
||||
}
|
||||
@ -71,6 +88,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
on_checkpoint: CheckpointSink,
|
||||
on_finalized_block: FinalizedBlockSink,
|
||||
on_deposit_event: OnDepositEventSink,
|
||||
on_withdraw_event: OnWithdrawEventSink,
|
||||
) -> Result<Self> {
|
||||
let basic_auth = config.auth.clone().map(Into::into);
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), config.node_url.clone());
|
||||
@ -80,7 +98,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
..ZoneSdkSequencerConfig::default()
|
||||
};
|
||||
|
||||
let (mut sequencer, mut handle) = ZoneSequencer::init_with_config(
|
||||
let mut sequencer = ZoneSequencer::init_with_config(
|
||||
config.channel_id,
|
||||
bedrock_signing_key,
|
||||
node,
|
||||
@ -88,55 +106,107 @@ impl BlockPublisherTrait for ZoneSdkPublisher {
|
||||
initial_checkpoint,
|
||||
);
|
||||
|
||||
// Grab readiness receiver before moving the sequencer into the drive
|
||||
// task so we can await cold-start completion below.
|
||||
let mut ready_rx = sequencer.subscribe_ready();
|
||||
|
||||
let (publish_tx, mut publish_rx) =
|
||||
mpsc::channel::<(Inscription, Vec<WithdrawArg>)>(PUBLISH_INBOX_CAPACITY);
|
||||
|
||||
let drive_task = tokio::spawn(async move {
|
||||
loop {
|
||||
let Some(event) = sequencer.next_event().await else {
|
||||
continue;
|
||||
};
|
||||
match event {
|
||||
Event::Checkpoint { checkpoint } => on_checkpoint(checkpoint),
|
||||
Event::TxsFinalized { items } => {
|
||||
for op in items.into_iter().flat_map(|item| item.ops) {
|
||||
match op {
|
||||
FinalizedOp::Inscription(inscription) => {
|
||||
if let Some(block_id) = block_id_from_inscription(&inscription)
|
||||
{
|
||||
on_finalized_block(block_id);
|
||||
#[expect(
|
||||
clippy::integer_division_remainder_used,
|
||||
reason = "tokio::select! expansion uses `%` for random branch selection"
|
||||
)]
|
||||
{
|
||||
tokio::select! {
|
||||
// Drain external publish requests by calling the
|
||||
// borrowing handle — `&mut sequencer` is only
|
||||
// available here.
|
||||
Some((data_bounded, withdrawals)) = publish_rx.recv() => {
|
||||
let data_byte_size = data_bounded.len();
|
||||
if withdrawals.is_empty() {
|
||||
if let Err(e) = sequencer.handle()
|
||||
.publish(data_bounded)
|
||||
.context("Failed to publish block") {
|
||||
warn!("zone-sdk publish failed: {e:?}");
|
||||
}
|
||||
|
||||
info!("Published block with the size of {data_byte_size} bytes");
|
||||
} else {
|
||||
let withdraw_count = withdrawals.len();
|
||||
if let Err(e) = sequencer.handle()
|
||||
.publish_atomic_withdraw(data_bounded, withdrawals)
|
||||
.context("Failed to publish block with withdrawals") {
|
||||
warn!("zone-sdk publish failed: {e:?}");
|
||||
}
|
||||
|
||||
info!(
|
||||
"Published block with the size of {data_byte_size} bytes and {withdraw_count} bridge withdrawals",
|
||||
);
|
||||
}
|
||||
}
|
||||
event = sequencer.next_event() => {
|
||||
let Some(event) = event else {
|
||||
continue;
|
||||
};
|
||||
match event {
|
||||
Event::BlocksProcessed {
|
||||
checkpoint,
|
||||
channel_update: _,
|
||||
finalized,
|
||||
} => {
|
||||
on_checkpoint(checkpoint);
|
||||
for op in finalized.into_iter().flat_map(|item| item.ops) {
|
||||
match op {
|
||||
FinalizedOp::Inscription(inscription) => {
|
||||
if let Some(block_id) =
|
||||
block_id_from_inscription(&inscription)
|
||||
{
|
||||
on_finalized_block(block_id);
|
||||
}
|
||||
}
|
||||
FinalizedOp::Deposit(deposit) => {
|
||||
on_deposit_event(deposit).await;
|
||||
}
|
||||
FinalizedOp::Withdraw(withdraw) => {
|
||||
on_withdraw_event(withdraw).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FinalizedOp::Deposit(deposit) => {
|
||||
on_deposit_event(deposit).await;
|
||||
}
|
||||
FinalizedOp::Withdraw(_) => {}
|
||||
Event::Ready | Event::TurnNotification { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::ChannelUpdate { .. }
|
||||
| Event::Published { .. }
|
||||
| Event::Readiness { .. }
|
||||
| Event::TurnNotification { .. } => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handle.wait_ready().await;
|
||||
// Wait for cold-start backfill to complete before returning so callers
|
||||
// can publish immediately (e.g. genesis block) without racing readiness.
|
||||
ready_rx
|
||||
.wait_for(|v| *v)
|
||||
.await
|
||||
.context("Zone-sdk readiness channel closed before becoming ready")?;
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
publish_tx,
|
||||
_drive_task: Arc::new(DriveTaskGuard(drive_task)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn publish_block(&self, block: &Block) -> Result<()> {
|
||||
async fn publish_block(&self, block: &Block, withdrawals: Vec<WithdrawArg>) -> Result<()> {
|
||||
let data = borsh::to_vec(block).context("Failed to serialize block")?;
|
||||
let data_bounded = data
|
||||
let data_bounded: Inscription = data
|
||||
.try_into()
|
||||
.context("Block data exceeds maximum allowed size")?;
|
||||
|
||||
self.handle
|
||||
.publish_message(data_bounded)
|
||||
self.publish_tx
|
||||
.send((data_bounded, withdrawals))
|
||||
.await
|
||||
.context("Failed to publish block")?;
|
||||
.map_err(|_closed| anyhow!("Drive task is no longer running"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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