Merge remote-tracking branch 'origin/dev' into moudy/cross-zone-target-auth

# Conflicts:
#	Cargo.lock
#	artifacts/lez/programs/amm.bin
#	artifacts/lez/programs/associated_token_account.bin
#	artifacts/lez/programs/authenticated_transfer.bin
#	artifacts/lez/programs/bridge.bin
#	artifacts/lez/programs/bridge_lock.bin
#	artifacts/lez/programs/clock.bin
#	artifacts/lez/programs/cross_zone_inbox.bin
#	artifacts/lez/programs/cross_zone_outbox.bin
#	artifacts/lez/programs/faucet.bin
#	artifacts/lez/programs/pinata.bin
#	artifacts/lez/programs/pinata_token.bin
#	artifacts/lez/programs/ping_receiver.bin
#	artifacts/lez/programs/ping_sender.bin
#	artifacts/lez/programs/token.bin
#	artifacts/lez/programs/vault.bin
#	artifacts/lez/programs/wrapped_token.bin
#	test_fixtures/fixtures/prebuilt_sequencer_db.dump
This commit is contained in:
moudyellaz
2026-08-19 23:40:53 +02:00
226 changed files with 11894 additions and 3592 deletions
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -uo pipefail
with_retry() {
local command="$1"
local max_attempts="${2:-3}"
local attempt=1
while (( attempt <= max_attempts )); do
if eval "$command"; then
return 0
fi
if (( attempt < max_attempts )); then
echo "::warning:: Attempt $attempt failed, cleaning up and retrying..." >&2
rm -rf target/debug/deps/*.o target/debug/incremental 2>/dev/null || true
cargo clean -p integration_tests 2>/dev/null || true
sleep 5
fi
(( attempt++ ))
done
echo "::error:: Command failed after $max_attempts attempts: $command" >&2
return 1
}
+8 -9
View File
@@ -215,13 +215,8 @@ jobs:
image: ${{ needs.ci-image.outputs.image }}
env: RISC0_DEV_MODE=1
run: |
for i in 1 2 3; do
cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager && break
echo "::warning:: Attempt $i failed, cleaning up and retrying..." >&2
rm -rf target/debug/deps/*.o target/debug/incremental 2>/dev/null || true
cargo clean -p integration_tests 2>/dev/null || true
sleep 5
done
source .github/scripts/with_retry.sh
with_retry 'cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager' 5
- name: Upload integration test archive
uses: actions/upload-artifact@v4
@@ -288,7 +283,9 @@ jobs:
env: |
RISC0_DEV_MODE=1
RUST_LOG=info
run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"
run: |
source .github/scripts/with_retry.sh
with_retry 'cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"' 5
valid-proof-test:
needs: ci-image
@@ -318,7 +315,9 @@ jobs:
with:
image: ${{ needs.ci-image.outputs.image }}
env: RUST_LOG=info
run: cargo test -p integration_tests -- --exact private::private_transfer_to_owned_account
run: |
source .github/scripts/with_retry.sh
with_retry 'cargo test -p integration_tests -- --exact private::private_transfer_to_owned_account' 5
# `just build-artifacts` drives the host's Docker daemon via `cargo risczero
# build`, so it goes through the image rather than `container:`.
Generated
+311 -232
View File
File diff suppressed because it is too large Load Diff
+31 -10
View File
@@ -22,7 +22,9 @@ members = [
"lez/sequencer/service",
"lez/sequencer/service/protocol",
"lez/sequencer/service/rpc",
"lez/sequencer/service/metrics",
"lez/sequencer/actors/executor",
"lez/sequencer/actors/rpc_server",
"lez/sequencer/actors/rpc_server/metrics",
"lez/indexer/core",
"lez/indexer/service",
"lez/indexer/service/protocol",
@@ -53,6 +55,7 @@ members = [
"lez/programs/wrapped_token",
"lez/programs/ping_sender",
"lez/programs/ping_receiver",
"lez/programs/sequencer_stake",
"lez/cross_zone",
"test_programs",
@@ -85,7 +88,9 @@ sequencer_core = { path = "lez/sequencer/core" }
sequencer_core_metrics = { path = "lez/sequencer/core/metrics" }
sequencer_service_protocol = { path = "lez/sequencer/service/protocol" }
sequencer_service_rpc = { path = "lez/sequencer/service/rpc" }
sequencer_service_metrics = { path = "lez/sequencer/service/metrics" }
sequencer_executor_actor = { path = "lez/sequencer/actors/executor" }
sequencer_rpc_server_actor = { path = "lez/sequencer/actors/rpc_server" }
sequencer_rpc_server_actor_metrics = { path = "lez/sequencer/actors/rpc_server/metrics" }
sequencer_service = { path = "lez/sequencer/service" }
indexer_core = { path = "lez/indexer/core" }
indexer_service = { path = "lez/indexer/service" }
@@ -116,6 +121,7 @@ cross_zone_outbox_core = { path = "lez/programs/cross_zone_outbox/core" }
bridge_lock_core = { path = "lez/programs/bridge_lock/core" }
wrapped_token_core = { path = "lez/programs/wrapped_token/core" }
ping_core = { path = "lez/programs/ping_core" }
sequencer_stake_core = { path = "lez/programs/sequencer_stake/core" }
cross_zone = { path = "lez/cross_zone" }
build_utils = { path = "build_utils" }
test_programs = { path = "test_programs" }
@@ -132,6 +138,8 @@ tokio = { version = "1.50", features = [
tokio-util = "0.7.18"
risc0-zkvm = { version = "3.0.5", default-features = false, features = ['std'] }
risc0-build = "3.0.5"
kameo = "0.22.2"
kameo_actors = "0.8.1"
anyhow = "1.0.98"
derive_more = "2.1.1"
num_cpus = "1.13.1"
@@ -154,12 +162,23 @@ metrics-exporter-prometheus = "0.18.3"
lru = "0.16.3"
thiserror = "2.0"
sha2 = "0.10.8"
ed25519-dalek = { version = "2.2.0", default-features = false }
hex = "0.4.3"
bytemuck = "1.24.0"
bytesize = { version = "2.3.1", features = ["serde"] }
humantime-serde = "1.1"
arc-swap = "1.7"
humantime = "2.1"
libp2p = { version = "0.56", features = [
"ed25519",
"gossipsub",
"identify",
"kad",
"macros",
"mdns",
"quic",
"tokio",
] }
aes-gcm = "0.10.3"
toml = "0.9.8"
bincode = "1.3.3"
@@ -184,14 +203,14 @@ schemars = "1.2"
async-stream = "0.3.6"
strum = { version = "0.28.0", features = ["derive"] }
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-codec = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "e2a1c3b7ef2191c224f998b94332c5926c789f9d" }
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-codec = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "97d5e104fc20ce6e2fb404f8a9364afcb181a3e5" }
keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "9535a657ba04b1e6916de51777e22b4837c1a84d" }
@@ -345,6 +364,8 @@ clippy.let-underscore-untyped = "allow"
# Reason: this lint is actually bad as it forces to use wildcard `..` instead of
# field-by-field `_` which may lead to subtle bugs when new fields are added to the struct.
clippy.unneeded-field-pattern = "allow"
# Reason: this lint makes no sense for us.
clippy.error_impl_error = "allow"
# Nursery
clippy.nursery = { level = "deny", priority = -1 }
+1 -1
View File
@@ -155,7 +155,7 @@ cross-zone-chat:
clean:
@echo "🧹 Cleaning run artifacts"
rm -rf lez/sequencer/service/bedrock_signing_key
rm -rf lez/sequencer/service/rocksdb
rm -rf lez/sequencer/service/rocksdb*
rm -rf lez/indexer/service/rocksdb*
rm -rf lez/wallet/configs/debug/storage.json
rm -rf lez/wallet/configs/debug/statistics.json
+2 -2
View File
@@ -169,9 +169,9 @@ The sequencer and logos blockchain node can be run locally:
After stopping services above you need to remove 3 folders to start cleanly:
1. In the `logos-blockchain/logos-blockchain` folder `state` (not needed in case of docker setup)
2. In the `logos-execution-zone` folder `lez/sequencer/service/rocksdb`
2. In the `logos-execution-zone` folder `lez/sequencer/service/rocksdb-<channel id>`
3. In the `logos-execution-zone` file `lez/sequencer/service/bedrock_signing_key`
4. In the `logos-execution-zone` folder `lez/indexer/service/rocksdb`
4. In the `logos-execution-zone` folder `lez/indexer/service/rocksdb-<channel id>`
### Normal mode (`just` commands)
We provide a `Justfile` for developer and user needs, you can run the whole setup with it. The only difference will be that logos-blockchain (bedrock) will be started from docker.
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.
+4 -2
View File
@@ -41,7 +41,7 @@ cryptarchia:
version: Bedrock
parent_block: '0000000000000000000000000000000000000000000000000000000000000000'
slot: 0
block_root: cb5951ac1ffa1aa5d0e585fb54e784bd9c025b28d752324e98b3837f34648692
block_root: '379bace32490b92ea47583b9aad4a32b059312a920d45b3b7386cffbc2737e18'
proof_of_leadership:
proof: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
entropy_contribution: '0000000000000000000000000000000000000000000000000000000000000000'
@@ -153,6 +153,8 @@ cryptarchia:
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
- value: 100000
pk: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
- value: 1000000000000
pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717
- value: 18446744073709551615
pk: c2a6a4a0981d5bdcf8ddeb8d7934fd8c5510efeb1053f613b45871670b6f7b19
- opcode: 17
@@ -168,7 +170,7 @@ cryptarchia:
- /ip4/65.109.51.37/udp/3400/quic-v1
provider_id: '59c662860b737f4e2515599adb3434856db8070b373a449ff66955ad3da6b473'
zk_id: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
locked_note_id: '7e449a14172fc90679f6fca7b49a2d58c305ebf7ac42ef20202e533c31115222'
locked_note_id: '67e2ec02c536a82f0eb80800fcfb8f2d66c5423d57cd081dfb0f237234f6ac26'
ops_proofs:
- !ZkSig
pi_a: '0000000000000000000000000000000000000000000000000000000000000000'
+10 -2
View File
@@ -17,11 +17,19 @@ use anyhow::{Context as _, Result, bail};
/// }
/// ```
pub fn include_artifacts(artifacts_sub_dir: &str) -> Result<()> {
let manifest_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
// Resolved at build-script runtime from the invoking crate, not at compile
// time: `env!` would bake in the path of whichever checkout compiled this
// rlib first, and with a shared cargo target dir every other worktree then
// embeds that checkout's artifacts instead of its own.
let invoking_manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let workspace_root = invoking_manifest_dir
.ancestors()
.find(|dir| dir.join("artifacts").is_dir())
.context("no artifacts/ directory above the invoking crate")?;
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let mod_dir = out_dir.join(artifacts_sub_dir);
let mod_file = mod_dir.join("mod.rs");
let artifacts_dir = manifest_dir.join(format!("../artifacts/{artifacts_sub_dir}/"));
let artifacts_dir = workspace_root.join(format!("artifacts/{artifacts_sub_dir}/"));
println!("cargo:rerun-if-changed={}", artifacts_dir.display());
+3 -3
View File
@@ -348,9 +348,9 @@ Check the `run_hello_world_private.rs` file to see how it is used.
# 8. Account authorization mechanism
The Hello world example does not enforce any authorization on the input account. This means any user can execute it on any account, regardless of ownership.
LEE provides a mechanism for programs to enforce proper authorization before an execution can succeed. The meaning of authorization differs between public and private accounts:
- Public accounts: authorization requires that the transaction is signed with the accounts signing key.
- Private accounts: authorization requires that the circuit verifies knowledge of the accounts nullifier secret key.
LEE provides a mechanism for programs to enforce proper authorization before an execution can succeed. For both private and public accounts, the authorization is checked against knowledge of a secret key, yet the check is different:
- Public accounts: the transaction is signed with the accounts signing key.
- Private accounts: the circuit verifies knowledge of the accounts authorization secret key (`ask`), the key from which the accounts nullifier secret key is derived.
From the program development perspective it is very simple: input accounts come with a flag indicating whether they has been properly authorized. And so, the only difference between the program `hello_world.rs` and `hello_world_with_authorization.rs` is in the lines
+2
View File
@@ -33,11 +33,13 @@ wallet-ffi.workspace = true
indexer_ffi.workspace = true
indexer_service_protocol.workspace = true
system_accounts.workspace = true
sequencer_stake_core.workspace = true
programs.workspace = true
test_programs.workspace = true
testnet_initial_state.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
+6 -6
View File
@@ -8,7 +8,6 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use lee::AccountId;
use log::info;
use sequencer_service_rpc::RpcClient as _;
pub use test_fixtures::*;
use wallet::{
@@ -93,7 +92,7 @@ pub async fn send_claiming_new_account(
amount,
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
@@ -113,7 +112,7 @@ pub async fn create_token(
total_supply,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
@@ -135,7 +134,7 @@ pub async fn token_send(
amount,
};
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
@@ -155,7 +154,7 @@ pub async fn token_send_claiming_new_account(
amount,
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
Ok(())
}
@@ -198,6 +197,7 @@ pub async fn sync_private(ctx: &mut TestContext) -> Result<()> {
}
/// Look up a restored private account for `account_id`, panicking with `label` if absent.
#[must_use]
pub fn restored_private_account<'ctx>(
ctx: &'ctx TestContext,
account_id: AccountId,
@@ -240,7 +240,7 @@ pub async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> Result<u64> {
let last_seq =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client())
.await?;
info!(
log::info!(
"Indexer caught up. Indexer last block id: {ind}. Current sequencer last block id: {last_seq}"
);
return Ok(ind);
+8 -9
View File
@@ -8,7 +8,6 @@ use integration_tests::{TestContext, get_account, new_account, private_mention};
use key_protocol::key_management::KeyChain;
use lee::Data;
use lee_core::account::Nonce;
use log::info;
use tokio::test;
use wallet::{
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
@@ -27,13 +26,13 @@ async fn get_existing_account() -> Result<()> {
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
assert_eq!(account.nonce.0, 1);
info!("Successfully retrieved account with correct details");
log::info!("Successfully retrieved account with correct details");
Ok(())
}
@@ -60,7 +59,7 @@ async fn new_public_account_with_label() -> Result<()> {
assert_eq!(resolved, Some(AccountIdWithPrivacy::Public(account_id)));
info!("Successfully created public account with label");
log::info!("Successfully created public account with label");
Ok(())
}
@@ -82,7 +81,7 @@ async fn add_label_to_existing_account() -> Result<()> {
assert_eq!(resolved, Some(AccountIdWithPrivacy::Private(account_id)));
info!("Successfully set label on existing private account");
log::info!("Successfully set label on existing private account");
Ok(())
}
@@ -103,7 +102,7 @@ async fn new_public_account_without_label() -> Result<()> {
"No label should be stored when not provided"
);
info!("Successfully created public account without label");
log::info!("Successfully created public account without label");
Ok(())
}
@@ -147,7 +146,7 @@ async fn import_private_account() -> Result<()> {
0,
));
let account = lee::Account {
program_owner: programs::authenticated_transfer().id(),
program_owner: programs::authenticated_transfer().id().into(),
balance: 777,
data: Data::default(),
nonce: Nonce::default(),
@@ -211,7 +210,7 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
serde_json::to_string(&key_chain).context("Failed to serialize key chain")?;
let initial_account = lee::Account {
program_owner: programs::authenticated_transfer().id(),
program_owner: programs::authenticated_transfer().id().into(),
balance: 100,
data: Data::default(),
nonce: Nonce::default(),
@@ -230,7 +229,7 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
.await?;
let updated_account = lee::Account {
program_owner: programs::authenticated_transfer().id(),
program_owner: programs::authenticated_transfer().id().into(),
balance: 999,
data: Data::default(),
nonce: Nonce::default(),
@@ -17,7 +17,6 @@ use lee_core::{
account::{Account, AccountWithMetadata},
encryption::ViewingPublicKey,
};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
@@ -38,13 +37,13 @@ async fn private_transfer_to_owned_account() -> Result<()> {
send(&mut ctx, private_mention(from), private_mention(to), 100).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
assert_private_commitment_in_state(&ctx, from, "sender").await?;
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
info!("Successfully transferred privately to owned account");
log::info!("Successfully transferred privately to owned account");
Ok(())
}
@@ -73,7 +72,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let new_commitment1 = ctx
@@ -88,7 +87,7 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
}
info!("Successfully transferred privately to foreign account");
log::info!("Successfully transferred privately to foreign account");
Ok(())
}
@@ -109,7 +108,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
send(&mut ctx, private_mention(from), public_mention(to), 100).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let from_acc = ctx
@@ -123,7 +122,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
assert_eq!(from_acc.balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Successfully deshielded transfer to public account");
log::info!("Successfully deshielded transfer to public account");
Ok(())
}
@@ -154,7 +153,7 @@ async fn deshielded_transfer_does_not_sign_with_recipient_key() -> Result<()> {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
@@ -164,7 +163,7 @@ async fn deshielded_transfer_does_not_sign_with_recipient_key() -> Result<()> {
"deshielded transfer must not carry any signature, in particular not the recipient's"
);
info!("Deshielded transfer correctly did not sign with the recipient's key");
log::info!("Deshielded transfer correctly did not sign with the recipient's key");
Ok(())
}
@@ -223,7 +222,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
.context("Failed to get recipient's private account")?;
assert_eq!(to_res_acc.balance, 100);
info!("Successfully transferred using claiming path");
log::info!("Successfully transferred using claiming path");
Ok(())
}
@@ -237,7 +236,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
send(&mut ctx, public_mention(from), private_mention(to), 100).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let acc_to = ctx
@@ -251,7 +250,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> {
assert_eq!(acc_from_balance, 9900);
assert_eq!(acc_to.balance, 20100);
info!("Successfully shielded transfer to owned private account");
log::info!("Successfully shielded transfer to owned private account");
Ok(())
}
@@ -280,7 +279,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
@@ -293,7 +292,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
assert_eq!(acc_1_balance, 9900);
info!("Successfully shielded transfer to foreign account");
log::info!("Successfully shielded transfer to foreign account");
Ok(())
}
@@ -338,7 +337,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
info!("Waiting for next blocks to check if continuous run fetches account");
log::info!("Waiting for next blocks to check if continuous run fetches account");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
@@ -371,7 +370,7 @@ async fn initialize_private_account() -> Result<()> {
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Syncing private accounts");
log::info!("Syncing private accounts");
sync_private(&mut ctx).await?;
assert_private_commitment_in_state(&ctx, account_id, "account").await?;
@@ -383,12 +382,12 @@ async fn initialize_private_account() -> Result<()> {
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(account.balance, 0);
assert!(account.data.is_empty());
info!("Successfully initialized private account");
log::info!("Successfully initialized private account");
Ok(())
}
@@ -417,13 +416,13 @@ async fn private_transfer_using_from_label() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
assert_private_commitment_in_state(&ctx, from, "sender").await?;
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
info!("Successfully transferred privately using from_label");
log::info!("Successfully transferred privately using from_label");
Ok(())
}
@@ -462,10 +461,10 @@ async fn initialize_private_account_using_label() -> Result<()> {
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
info!("Successfully initialized private account using label");
log::info!("Successfully initialized private account using label");
Ok(())
}
@@ -526,7 +525,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
sync_private(&mut ctx).await?;
@@ -569,7 +568,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> {
"both accounts must resolve to the key node created at the start of the test"
);
info!("Successfully transferred to two distinct identifiers under the same NPK");
log::info!("Successfully transferred to two distinct identifiers under the same NPK");
Ok(())
}
@@ -584,7 +583,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
));
ctx.sequencer_client().send_transaction(deploy_tx).await?;
info!("Waiting for deploy block creation");
log::info!("Waiting for deploy block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_account_id = system_accounts::faucet_account_id();
@@ -592,7 +591,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
let faucet_program_id = programs::faucet().id();
let vault_program_id = programs::vault().id();
let auth_transfer_program_id = programs::authenticated_transfer().id();
let nsk: lee_core::NullifierSecretKey = [3; 32];
let ask = lee_core::AuthorizationSecretKey([3; 32]);
let nsk = lee_core::NullifierSecretKey::from(&ask);
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let attacker_vault_id = {
@@ -661,7 +661,8 @@ async fn prove_init_with_commitment_root(
sender_id,
);
let nsk: lee_core::NullifierSecretKey = [7; 32];
let ask = lee_core::AuthorizationSecretKey([7; 32]);
let nsk = lee_core::NullifierSecretKey::from(&ask);
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
@@ -678,7 +679,7 @@ async fn prove_init_with_commitment_root(
vpk,
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular { ask: Some(ask) },
nullifier: NullifierWitness::Init {
npk,
commitment_root,
@@ -697,7 +698,8 @@ async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> {
let (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?;
let nsk: lee_core::NullifierSecretKey = [7; 32];
let ask = lee_core::AuthorizationSecretKey([7; 32]);
let nsk = lee_core::NullifierSecretKey::from(&ask);
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
+34 -35
View File
@@ -7,7 +7,6 @@ use integration_tests::{
public_mention, send, send_claiming_new_account,
};
use lee::{PublicKey, public_transaction};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{
@@ -39,15 +38,15 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
anyhow::bail!("Expected TransactionExecuted return value");
};
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, receiver).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
@@ -94,12 +93,12 @@ pub async fn successful_transfer_to_new_account() -> Result<()> {
// requires it, so bypass the CLI for this one send.
send_claiming_new_account(&mut ctx, sender, new_persistent_account_id, 100).await?;
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, new_persistent_account_id).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 100);
@@ -124,15 +123,15 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> {
let failed_send = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await;
assert!(failed_send.is_err());
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking balances unchanged");
log::info!("Checking balances unchanged");
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 10000);
assert_eq!(acc_2_balance, 20000);
@@ -156,20 +155,20 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move after first transfer");
log::info!("Checking correct balance move after first transfer");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, receiver).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("First TX Success!");
log::info!("First TX Success!");
// Second transfer
send(
@@ -180,20 +179,20 @@ async fn two_consecutive_successful_transfers() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move after second transfer");
log::info!("Checking correct balance move after second transfer");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, receiver).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9800);
assert_eq!(acc_2_balance, 20200);
info!("Second TX Success!");
log::info!("Second TX Success!");
Ok(())
}
@@ -209,18 +208,18 @@ async fn initialize_public_account() -> Result<()> {
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Checking correct execution");
log::info!("Checking correct execution");
let account = get_account(&ctx, account_id).await?;
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(account.balance, 0);
assert_eq!(account.nonce.0, 1);
assert!(account.data.is_empty());
info!("Successfully initialized public account");
log::info!("Successfully initialized public account");
Ok(())
}
@@ -248,17 +247,17 @@ async fn successful_transfer_using_from_label() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, receiver).await?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Successfully transferred using from_label");
log::info!("Successfully transferred using from_label");
Ok(())
}
@@ -286,17 +285,17 @@ async fn successful_transfer_using_to_label() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, sender).await?;
let acc_2_balance = account_balance(&ctx, receiver).await?;
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Successfully transferred using to_label");
log::info!("Successfully transferred using to_label");
Ok(())
}
@@ -326,7 +325,7 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
.send_transaction(LeeTransaction::Public(tx))
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let recipient_balance_after = account_balance(&ctx, recipient).await?;
@@ -372,7 +371,7 @@ async fn cannot_execute_faucet_program() -> Result<()> {
.send_transaction(LeeTransaction::Public(tx))
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let recipient_balance_after = account_balance(&ctx, recipient).await?;
@@ -396,7 +395,7 @@ async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
));
ctx.sequencer_client().send_transaction(deploy_tx).await?;
info!("Waiting for deploy block creation");
log::info!("Waiting for deploy block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_account_id = system_accounts::faucet_account_id();
@@ -422,7 +421,7 @@ async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> {
let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?;
+34 -24
View File
@@ -9,22 +9,26 @@ use std::time::Duration;
use anyhow::Result;
use bytesize::ByteSize;
use common::transaction::LeeTransaction;
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, config::SequencerPartialConfig,
};
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, config::SequencerPartialConfig};
use lee::program::Program;
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
#[test]
async fn reject_oversized_transaction() -> Result<()> {
let ctx = TestContext::builder()
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: ByteSize::mib(1),
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
})
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: ByteSize::mib(1),
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
}),
)
.build()
.await?;
@@ -61,13 +65,16 @@ async fn reject_oversized_transaction() -> Result<()> {
#[test]
async fn accept_transaction_within_limit() -> Result<()> {
let ctx = TestContext::builder()
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: ByteSize::mib(1),
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
})
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: ByteSize::mib(1),
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
}),
)
.build()
.await?;
@@ -102,13 +109,16 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> {
let max_program_size = claimer.elf().len().max(chain_caller.elf().len());
let block_size = ByteSize::b((max_program_size + 10 * 1024) as u64);
let ctx = TestContext::builder()
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: block_size,
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
})
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_sequencer_partial_config(SequencerPartialConfig {
max_num_tx_in_block: 100,
max_block_size: block_size,
mempool_max_size: 1000,
block_create_timeout: Duration::from_secs(10),
}),
)
.build()
.await?;
+4 -4
View File
@@ -249,7 +249,7 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
// let mut balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
// info!(
// log::info!(
// "Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
// balance.balance
// );
@@ -291,7 +291,7 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
// .await
// .context("Failed to decode Bedrock transfer-funds response")?;
// info!(
// log::info!(
// "Submitted transfer-funds to create exact deposit note, tx hash {:?}",
// transfer.hash
// );
@@ -343,7 +343,7 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
// .text()
// .await
// .unwrap_or_else(|_| "<failed to decode>".to_owned());
// info!(
// log::info!(
// "Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount
// {amount}, response body: {body_text}", );
@@ -585,7 +585,7 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
// let mut stream = std::pin::pin!(stream);
// while let Some(message) = stream.next().await {
// info!("Observed zone message {message:?}");
// log::info!("Observed zone message {message:?}");
// if let ZoneMessage::Withdraw(withdraw) = message {
// released_notes.extend(withdraw.inputs.iter().copied());
+65 -3
View File
@@ -6,9 +6,12 @@
use anyhow::Result;
use integration_tests::TestContext;
use log::info;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder,
config::{MultiNodeTestContextConfig, bedrock_channel_id},
};
use tokio::test;
use wallet::cli::{Command, config::ConfigSubcommand};
use wallet::cli::{Command, config::ConfigSubcommand, statistics::StatisticsSubcommand};
#[test]
async fn modify_config_field() -> Result<()> {
@@ -33,7 +36,66 @@ async fn modify_config_field() -> Result<()> {
});
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
info!("Successfully modified and restored config field");
log::info!("Successfully modified and restored config field");
Ok(())
}
#[test]
async fn modify_config_field_multiseq() -> Result<()> {
let mut ctx = MultiZoneTestContextBuilder::default()
.with_zone(ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 2,
bedrock_channel: bedrock_channel_id(),
}))
.build()
.await?;
// Default config have callibration limit and distribution limit as 1
// Modifying them
let wallet_mut = ctx.wallet_mut();
let command = Command::Config(ConfigSubcommand::Set {
key: "distribution_limit".to_owned(),
value: "2".to_owned(),
});
wallet::cli::execute_subcommand(wallet_mut, command).await?;
let command = Command::Config(ConfigSubcommand::Set {
key: "calibration_limit".to_owned(),
value: "10".to_owned(),
});
wallet::cli::execute_subcommand(wallet_mut, command).await?;
// Check config correctness
assert_eq!(
wallet_mut
.config()
.multi_sequencer_client_config
.calibration_limit,
10
);
assert_eq!(
wallet_mut
.config()
.multi_sequencer_client_config
.distribution_limit,
2
);
// Rotate clients to callibrate the other one
let command = Command::Statistics(StatisticsSubcommand::ExecuteRotation);
wallet::cli::execute_subcommand(wallet_mut, command).await?;
// After that, there must be two leaders
let leaders = wallet_mut.leaders();
assert_eq!(leaders.len(), 2);
// And both of them must have similar statistics
let first_stat = wallet_mut.get_statistics(&leaders[0].1).unwrap();
let second_stat = wallet_mut.get_statistics(&leaders[1].1).unwrap();
assert_eq!(first_stat.latest_block_id, second_stat.latest_block_id);
Ok(())
}
+37 -30
View File
@@ -22,7 +22,6 @@ use cross_zone_outbox_core::outbox_pda;
use integration_tests::{
config::{self, SequencerPartialConfig},
indexer_client::IndexerClient,
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
};
use lee::{
AccountId, PrivateKey, PublicKey, PublicTransaction,
@@ -30,6 +29,9 @@ use lee::{
};
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute, GenesisAction};
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
@@ -39,11 +41,6 @@ const RECIPIENT: [u8; 32] = [9; 32];
#[test]
async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
// Declared first so it outlives both zones (drops run in reverse order).
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up shared Bedrock node")?;
let partial = SequencerPartialConfig::default();
let channel_a = config::bedrock_channel_id();
let channel_b = config::bedrock_channel_id_b();
@@ -72,37 +69,48 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
holder: holder_id,
amount: INITIAL_BALANCE,
}];
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_a)
.with_genesis(genesis_a)
.setup()
.await
.context("Failed to set up zone A sequencer")?;
let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_b)
.with_genesis(vec![])
.with_cross_zone(cross_zone.clone())
.setup()
.await
.context("Failed to set up zone B sequencer")?;
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
.await
.context("Failed to set up zone B indexer")?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_a,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(genesis_a),
)
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_b,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![])
.with_cross_zone(Some(cross_zone)),
)
.build()
.await?;
let seq_client_a = &ctx
.zone_default_sequencer_component(channel_a)
.sequencer_client;
let ind_client_b = ctx.indexer_client_zone(channel_b).unwrap();
// Lock LOCK_AMOUNT on zone A, addressed to the recipient on zone B.
let lock = build_lock_tx(&holder_key, holder_id, zone_b);
sequencer_client(seq_a.addr())?
seq_client_a
.send_transaction(lock)
.await
.context("Failed to submit lock on zone A")?;
// Wait until zone B's indexer reflects the verified mint.
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
let indexer = indexer_client(idx_b.addr())
.await
.context("Failed to build indexer client")?;
let minted = wait_for_mint(&indexer, holding_id).await?;
let minted = wait_for_mint(ind_client_b, holding_id).await?;
assert_eq!(
minted, LOCK_AMOUNT,
"zone B must mint exactly the locked amount"
@@ -111,14 +119,13 @@ async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
// Conservation: the mint on B must be backed by an equal lock on A. The lock
// has already landed (it preceded delivery), so zone A reflects the debit and
// escrow now.
let seq_a_client = sequencer_client(seq_a.addr())?;
let escrow_id = bridge_lock_core::escrow_account_id(programs::bridge_lock().id());
let escrowed = seq_a_client.get_account(escrow_id).await?.balance;
let escrowed = seq_client_a.get_account(escrow_id).await?.balance;
assert_eq!(
escrowed, LOCK_AMOUNT,
"zone A escrow must hold the locked amount"
);
let remaining = seq_a_client.get_account(holder_id).await?.balance;
let remaining = seq_client_a.get_account(holder_id).await?.balance;
assert_eq!(
remaining,
INITIAL_BALANCE - LOCK_AMOUNT,
@@ -9,35 +9,39 @@
//! inbox guest's caller-is-none assertion passes for a top-level user tx, so the
//! sequencer ingress guard is the only thing that stops this.
use anyhow::{Context as _, Result};
use anyhow::Result;
use common::transaction::LeeTransaction;
use cross_zone_inbox_core::{
CrossZoneMessage, Instruction, inbox_config_account_id, inbox_seen_shard_account_id,
};
use integration_tests::{
config::{self, SequencerPartialConfig},
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
};
use integration_tests::config::{self, SequencerPartialConfig};
use lee::{
PublicTransaction,
public_transaction::{Message, WitnessSet},
};
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
#[test]
async fn user_origin_inbox_call_rejected() -> Result<()> {
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up Bedrock node")?;
let partial = SequencerPartialConfig::default();
let channel = config::bedrock_channel_id();
let (seq, _seq_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel)
.with_genesis(vec![])
.setup()
.await
.context("Failed to set up sequencer")?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel,
})
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.build()
.await?;
// A user hand-builds a top-level inbox Dispatch and submits it via RPC.
let inbox_id = programs::cross_zone_inbox().id();
@@ -64,7 +68,11 @@ async fn user_origin_inbox_call_rejected() -> Result<()> {
WitnessSet::from_raw_parts(vec![]),
));
let result = sequencer_client(seq.addr())?.send_transaction(tx).await;
let result = ctx
.default_sequencer_component()
.sequencer_client
.send_transaction(tx)
.await;
let err = result.expect_err("the sequencer must reject a user-origin inbox call");
assert!(
err.to_string().contains("sequencer-only"),
+39 -24
View File
@@ -16,10 +16,7 @@ use std::time::Duration;
use anyhow::{Context as _, Result};
use common::transaction::LeeTransaction;
use cross_zone_outbox_core::outbox_pda;
use integration_tests::{
config::{self, SequencerPartialConfig},
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
};
use integration_tests::config::{self, SequencerPartialConfig};
use lee::{AccountId, PublicTransaction, public_transaction::Message};
use lee_core::program::ProgramId;
use ping_core::{
@@ -28,6 +25,9 @@ use ping_core::{
};
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
@@ -35,11 +35,6 @@ const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
#[test]
async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
// Declared first so it outlives both zones (drops run in reverse order).
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up shared Bedrock node")?;
let partial = SequencerPartialConfig::default();
let channel_a = config::bedrock_channel_id();
let channel_b = config::bedrock_channel_id_b();
@@ -62,30 +57,50 @@ async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
source_governance: None,
};
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_a)
.with_genesis(vec![])
.setup()
.await
.context("Failed to set up zone A sequencer")?;
let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_b)
.with_genesis(vec![])
.with_cross_zone(cross_zone)
.setup()
.await
.context("Failed to set up zone B sequencer")?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_a,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_b,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_genesis(vec![])
.with_cross_zone(Some(cross_zone)),
)
.build()
.await?;
// Submit the ping on zone A, addressed to ping_receiver on zone B.
let ping = build_ping_tx(zone_b, receiver_id);
sequencer_client(seq_a.addr())?
let seq_client_a = &ctx
.zone_default_sequencer_component(channel_a)
.sequencer_client;
let seq_client_b = &ctx
.zone_default_sequencer_component(channel_b)
.sequencer_client;
seq_client_a
.send_transaction(ping)
.await
.context("Failed to submit ping on zone A")?;
// Wait until zone B's sequencer records the delivered payload.
let record_id = ping_record_pda(receiver_id);
let delivered = wait_for_delivery(sequencer_client(seq_b.addr())?, record_id).await?;
let delivered = wait_for_delivery(seq_client_b.clone(), record_id).await?;
assert_eq!(
delivered, PING_PAYLOAD,
@@ -60,7 +60,7 @@ fn seed_inbox_config(state: &mut V03State, self_zone: [u8; 32]) {
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
inbox_config_account_id(inbox_id),
Account {
program_owner: inbox_id,
program_owner: inbox_id.into(),
balance: 0,
data: config
.to_bytes()
@@ -98,7 +98,7 @@ fn seed_wrapped_config_with_governance(
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
wrapped_token_core::config_account_id(wrapped_token_id),
Account {
program_owner: wrapped_token_id,
program_owner: wrapped_token_id.into(),
data: config
.to_bytes()
.try_into()
@@ -135,7 +135,7 @@ fn seed_receiver_config_with_governance(
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
receiver_config_account_id(receiver_id),
Account {
program_owner: receiver_id,
program_owner: receiver_id.into(),
data: config
.to_bytes()
.try_into()
@@ -152,7 +152,7 @@ fn seed_ping_sender_config(state: &mut V03State) {
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
sender_config_account_id(sender_id),
Account {
program_owner: sender_id,
program_owner: sender_id.into(),
data: outbox_bytes(programs::cross_zone_outbox().id())
.to_vec()
.try_into()
@@ -169,7 +169,7 @@ fn seed_bridge_lock_config(state: &mut V03State) {
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
bridge_lock_core::config_account_id(bridge_lock_id),
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
data: bridge_lock_core::config_bytes(
programs::cross_zone_outbox().id(),
programs::wrapped_token().id(),
@@ -457,7 +457,7 @@ fn lock_escrows_balance_and_emits_to_outbox() {
state = state.with_public_accounts([(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -580,7 +580,7 @@ fn a_second_emit_at_the_same_slot_is_rejected() {
let mut state = base_state().with_public_accounts([(
holder_id,
Account {
program_owner: programs::bridge_lock().id(),
program_owner: programs::bridge_lock().id().into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -628,7 +628,7 @@ fn two_emitters_share_an_ordinal_without_colliding() {
let mut state = base_state().with_public_accounts([(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -718,7 +718,7 @@ fn a_lock_naming_another_target_program_is_rejected() {
let mut state = base_state().with_public_accounts([(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -764,7 +764,7 @@ fn a_lock_naming_other_mint_accounts_is_rejected() {
let mut state = base_state().with_public_accounts([(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -823,7 +823,7 @@ fn a_lock_with_a_substituted_config_account_is_rejected() {
(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -831,7 +831,7 @@ fn a_lock_with_a_substituted_config_account_is_rejected() {
(
decoy_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
data: bridge_lock_core::config_bytes([3; 8], [4; 8])
.to_vec()
.try_into()
@@ -888,7 +888,7 @@ fn a_lock_before_the_pins_are_set_is_rejected() {
let state = base_state().with_public_accounts([(
holder_id,
Account {
program_owner: bridge_lock_id,
program_owner: bridge_lock_id.into(),
balance: INITIAL_BALANCE,
..Default::default()
},
@@ -1188,7 +1188,7 @@ fn the_token_authority_path_holds() {
assert_eq!(cfg.sources, bridge_source, "the new source is authorized");
assert_eq!(
state.get_account_by_id(authority).program_owner,
wrapped_token_id,
wrapped_token_id.into(),
"the first use claims the authority account for the target"
);
@@ -1564,7 +1564,7 @@ fn the_governance_path_holds() {
assert_eq!(cfg.sources, vec![(src_zone, programs::bridge_lock().id())]);
assert_eq!(
state.get_account_by_id(authority).program_owner,
wrapped_token_id,
wrapped_token_id.into(),
"the first use claims the delegated PDA for the target"
);
@@ -1731,7 +1731,7 @@ fn the_receiver_governance_path_holds() {
assert_eq!(cfg.sources, vec![(src_zone, programs::ping_sender().id())]);
assert_eq!(
state.get_account_by_id(authority).program_owner,
receiver_id,
receiver_id.into(),
"the first use claims the delegated PDA for the receiver"
);
}
@@ -1770,7 +1770,7 @@ fn a_shared_authority_survives_the_first_claim() {
state.apply_state_diff(first);
assert_eq!(
state.get_account_by_id(authority).program_owner,
wrapped_token_id,
wrapped_token_id.into(),
"the first target to be used owns the account"
);
@@ -1800,7 +1800,7 @@ fn a_shared_authority_survives_the_first_claim() {
);
assert_eq!(
state.get_account_by_id(authority).program_owner,
wrapped_token_id,
wrapped_token_id.into(),
"the receiver never takes the account over"
);
@@ -2205,7 +2205,7 @@ fn mint_replay_rejected() {
state = state.with_public_accounts([(
seen_id,
Account {
program_owner: inbox_id,
program_owner: inbox_id.into(),
balance: 0,
data: shard
.to_bytes()
@@ -2282,7 +2282,7 @@ fn a_delivery_from_a_second_block_at_the_same_id_is_refused() {
state = state.with_public_accounts([(
seen_id,
Account {
program_owner: inbox_id,
program_owner: inbox_id.into(),
balance: 0,
data: shard
.to_bytes()
+33 -30
View File
@@ -17,7 +17,6 @@ use cross_zone_outbox_core::outbox_pda;
use integration_tests::{
config::{self, SequencerPartialConfig},
indexer_client::IndexerClient,
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
};
use lee::{AccountId, PublicTransaction, public_transaction::Message};
use lee_core::program::ProgramId;
@@ -27,6 +26,9 @@ use ping_core::{
};
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
@@ -34,11 +36,6 @@ const PING_PAYLOAD: &[u8] = b"hello-verified-zone";
#[test]
async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
// Declared first so it outlives both zones (drops run in reverse order).
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up shared Bedrock node")?;
let partial = SequencerPartialConfig::default();
let channel_a = config::bedrock_channel_id();
let channel_b = config::bedrock_channel_id_b();
@@ -59,31 +56,40 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
source_governance: None,
};
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_a,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_b,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![])
.with_cross_zone(Some(cross_zone)),
)
.build()
.await?;
// Zone A: source. Zone B: destination, with the watcher on its sequencer and
// the verifier on its indexer.
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_a)
.with_genesis(vec![])
.setup()
.await
.context("Failed to set up zone A sequencer")?;
let (_idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
.await
.context("Failed to set up zone A indexer")?;
let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_b)
.with_genesis(vec![])
.with_cross_zone(cross_zone.clone())
.setup()
.await
.context("Failed to set up zone B sequencer")?;
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
.await
.context("Failed to set up zone B indexer")?;
let ind_client_b = ctx.indexer_client_zone(channel_b).unwrap();
let seq_client_a = &ctx
.zone_default_sequencer_component(channel_a)
.sequencer_client;
// Submit the ping on zone A, addressed to ping_receiver on zone B.
let ping = build_ping_tx(zone_b, receiver_id);
sequencer_client(seq_a.addr())?
seq_client_a
.send_transaction(ping)
.await
.context("Failed to submit ping on zone A")?;
@@ -91,11 +97,8 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
// Wait until zone B's indexer records the delivered payload. The indexer only
// applies the dispatch after re-deriving and verifying it.
let record_id = ping_record_pda(receiver_id);
let indexer = indexer_client(idx_b.addr())
.await
.context("Failed to build indexer client")?;
let delivered = wait_for_indexer_delivery(&indexer, record_id).await?;
let delivered = wait_for_indexer_delivery(ind_client_b, record_id).await?;
assert_eq!(
delivered, PING_PAYLOAD,
"Zone B's indexer must record the verified cross-zone payload"
+129
View File
@@ -0,0 +1,129 @@
#![expect(
clippy::tests_outside_test_module,
reason = "top-level test functions are conventional for integration tests"
)]
//! Gossip end-to-end: two sequencers share one channel with p2p gossip on,
//! and B's block production is disabled. A transfer submitted to B's RPC can
//! therefore only be included if gossip hands it to A — without gossip it
//! would sit in B's mempool forever and the test times out.
use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::config::{self, SequencerPartialConfig};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts};
use tokio::test;
const PHASE_TIMEOUT: Duration = Duration::from_secs(360);
const POLL_INTERVAL: Duration = Duration::from_secs(2);
const TRANSFER_AMOUNT: u128 = 10;
#[test]
async fn gossiped_transaction_reaches_producing_sequencer() -> Result<()> {
let bedrock_channel_id = config::bedrock_channel_id();
let partial = SequencerPartialConfig {
block_create_timeout: Duration::from_secs(5),
..SequencerPartialConfig::default()
};
// B never produces: its production timer is longer than the test, so its
// local mempool is a dead end and inclusion proves gossip delivery to A.
let follower_partial = SequencerPartialConfig {
block_create_timeout: Duration::from_secs(100_000),
..SequencerPartialConfig::default()
};
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 2,
bedrock_channel: bedrock_channel_id,
})
.disable_wallet()
.disable_indexer()
.with_sequencer_partial_config(partial)
.with_follower_sequencer_partial_config(follower_partial)
.with_gossip()
.with_genesis(vec![]),
)
.build()
.await?;
let mut seq_iterator = ctx.sequencer_components_iter(bedrock_channel_id).unwrap();
let seq_client_a = &(seq_iterator.next().unwrap().sequencer_client);
let seq_client_b = &(seq_iterator.next().unwrap().sequencer_client);
wait_for_height(seq_client_a, 2, "sequencer A to produce past genesis").await?;
// B follows the chain via L1 even though it never produces.
let sync_target = seq_client_a.get_last_block_id().await?;
wait_for_height(seq_client_b, sync_target, "B to sync to A's height").await?;
let accounts = initial_public_user_accounts();
let from = accounts[0].account_id;
let to = accounts[1].account_id;
let sign_key = initial_pub_accounts_private_keys()[0].pub_sign_key.clone();
let to_balance_before = seq_client_a.get_account_balance(to).await?;
let nonce = seq_client_b.get_accounts_nonces(vec![from]).await?[0];
let tx = common::test_utils::create_transaction_native_token_transfer(
from,
nonce.0,
to,
TRANSFER_AMOUNT,
&sign_key,
);
seq_client_b
.send_transaction(tx)
.await
.context("Failed to submit the transfer to B")?;
// Only A produces, so the balance changing on A proves the transaction
// crossed the gossip mesh from B.
wait_for_balance(seq_client_a, to, to_balance_before + TRANSFER_AMOUNT).await?;
Ok(())
}
/// Polls the sequencer until its chain height reaches `target`.
async fn wait_for_height(client: &SequencerClient, target: u64, what: &str) -> Result<()> {
log::info!("Waiting for {what:?}, target is {target}");
let wait = async {
loop {
if client.get_last_block_id().await? >= target {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.with_context(|| format!("Timed out waiting for {what} (target height {target})"))?
}
/// Polls the sequencer until `account`'s balance reaches `expected`.
async fn wait_for_balance(
client: &SequencerClient,
account: lee::AccountId,
expected: u128,
) -> Result<()> {
log::info!("Waiting for {account} to have {expected} tokens");
let wait = async {
loop {
if client.get_account_balance(account).await? == expected {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.context("Timed out waiting for the gossiped transfer to be included by A")?
}
@@ -6,16 +6,15 @@
use anyhow::Result;
use indexer_service_rpc::RpcClient as _;
use integration_tests::{TestContext, wait_for_indexer_to_catch_up};
use log::info;
#[tokio::test]
async fn indexer_block_batching() -> Result<()> {
let ctx = TestContext::new().await?;
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await?;
info!("Last block on ind now is {last_block_indexer}");
log::info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 0);
@@ -31,7 +30,7 @@ async fn indexer_block_batching() -> Result<()> {
for block in &block_batch[1..] {
assert_eq!(block.header.prev_block_hash, prev_block_hash);
info!("Block {} chain-consistent", block.header.block_id);
log::info!("Block {} chain-consistent", block.header.block_id);
prev_block_hash = block.header.hash;
}
@@ -6,7 +6,6 @@
use anyhow::Result;
use indexer_ffi::api::types::FfiOption;
use log::info;
#[path = "indexer_ffi_helpers/mod.rs"]
mod indexer_ffi_helpers;
@@ -20,10 +19,10 @@ fn indexer_ffi_block_batching() -> Result<()> {
// WAIT: poll until the indexer has finalized at least two blocks (so the
// chain-consistency check below verifies at least one block link), returning
// early instead of sleeping for the full timeout.
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
let last_block_indexer = indexer_ffi_helpers::wait_for_indexer_ffi_block(&indexer_ffi, 2)?;
info!("Last block on indexer FFI now is {last_block_indexer}");
log::info!("Last block on indexer FFI now is {last_block_indexer}");
assert!(last_block_indexer > 0);
@@ -44,7 +43,7 @@ fn indexer_ffi_block_batching() -> Result<()> {
assert_eq!(last_block_prev_hash, block.header.hash.data);
info!("Block {} chain-consistent", block.header.block_id);
log::info!("Block {} chain-consistent", block.header.block_id);
last_block_prev_hash = block.header.prev_block_hash.data;
}
@@ -17,8 +17,11 @@ use indexer_ffi::{
types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock},
},
};
use integration_tests::{BlockingTestContext, TestContext};
use integration_tests::BlockingTestContext;
use tempfile::TempDir;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
unsafe extern "C" {
pub unsafe fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult;
@@ -83,7 +86,12 @@ pub fn setup_indexer_ffi(bedrock_addr: SocketAddr) -> Result<(IndexerServiceFFI,
}
pub fn setup() -> Result<(BlockingTestContext, IndexerServiceFFI, TempDir)> {
let ctx = TestContext::builder().disable_indexer().build_blocking()?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default()).disable_indexer(),
)
.build_blocking()?;
// Don't borrow `ctx.runtime()`: `ctx` (and its by-value tokio runtime) is
// moved into the returned tuple, which would leave any pointer into it
// dangling. Pass a null runtime so the FFI owns its own — the same path the
@@ -14,7 +14,6 @@ use integration_tests::{
verify_commitment_is_in_state,
};
use lee::AccountId;
use log::info;
use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand};
#[path = "indexer_ffi_helpers/mod.rs"]
@@ -36,10 +35,10 @@ fn indexer_ffi_state_consistency() -> Result<()> {
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account_balance(
ctx.sequencer_client(),
@@ -53,8 +52,8 @@ fn indexer_ffi_state_consistency() -> Result<()> {
)
})?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
@@ -74,7 +73,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let new_commitment1 = ctx
@@ -95,10 +94,10 @@ fn indexer_ffi_state_consistency() -> Result<()> {
ctx.block_on(|ctx| verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()));
assert!(commitment_check2);
info!("Successfully transferred privately to owned account");
log::info!("Successfully transferred privately to owned account");
// WAIT
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
std::thread::sleep(L2_TO_L1_TIMEOUT);
let acc1_ind_state_ffi = unsafe {
@@ -125,7 +124,7 @@ fn indexer_ffi_state_consistency() -> Result<()> {
let acc2_ind_state_pre = unsafe { &*acc2_ind_state_ffi.value };
let acc2_ind_state: Account = acc2_ind_state_pre.into();
info!("Checking correct state transition");
log::info!("Checking correct state transition");
let acc1_seq_state = ctx.block_on(|ctx| {
sequencer_service_rpc::RpcClient::get_account(
ctx.sequencer_client(),
@@ -10,7 +10,6 @@ use std::time::Duration;
use anyhow::Result;
use indexer_service_protocol::Account;
use integration_tests::{L2_TO_L1_TIMEOUT, TIME_TO_WAIT_FOR_BLOCK_SECONDS, public_mention};
use log::info;
use wallet::{
account::Label,
cli::{Command, programs::native_token_transfer::AuthTransferSubcommand},
@@ -52,7 +51,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
ctx.block_on_mut(|ctx| wallet::cli::execute_subcommand(ctx.wallet_mut(), command))?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let acc_1_balance = ctx.block_on(|ctx| {
@@ -71,7 +70,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
std::thread::sleep(L2_TO_L1_TIMEOUT);
let acc1_ind_state_ffi = unsafe {
@@ -95,7 +94,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> {
assert_eq!(acc1_ind_state, acc1_seq_state.into());
info!("Indexer state is consistent after label-based transfer");
log::info!("Indexer state is consistent after label-based transfer");
Ok(())
}
+1 -2
View File
@@ -9,7 +9,6 @@ use anyhow::{Context as _, Result};
use indexer_service_protocol::IndexerSyncState;
use indexer_service_rpc::RpcClient as _;
use integration_tests::{TestContext, wait_for_indexer_to_catch_up};
use log::info;
const CAUGHT_UP_STATUS_TIMEOUT: Duration = Duration::from_secs(60);
@@ -32,7 +31,7 @@ async fn indexer_status_rpc_reports_caught_up_with_no_stall() -> Result<()> {
if status.state == IndexerSyncState::CaughtUp {
return anyhow::Ok(status);
}
info!("Waiting for caught-up indexer status, got {status:?}");
log::info!("Waiting for caught-up indexer status, got {status:?}");
tokio::time::sleep(Duration::from_millis(500)).await;
}
})
@@ -13,7 +13,6 @@ use integration_tests::{
wait_for_indexer_to_catch_up,
};
use lee::AccountId;
use log::info;
#[tokio::test]
async fn indexer_state_consistency() -> Result<()> {
@@ -25,15 +24,15 @@ async fn indexer_state_consistency() -> Result<()> {
);
send(&mut ctx, public_mention(acc0), public_mention(acc1), 100).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
info!("Checking correct balance move");
log::info!("Checking correct balance move");
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?;
info!("Balance of sender: {acc_1_balance:#?}");
info!("Balance of receiver: {acc_2_balance:#?}");
log::info!("Balance of sender: {acc_1_balance:#?}");
log::info!("Balance of receiver: {acc_2_balance:#?}");
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
@@ -43,15 +42,15 @@ async fn indexer_state_consistency() -> Result<()> {
send(&mut ctx, private_mention(from), private_mention(to), 100).await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
assert_private_commitment_in_state(&ctx, from, "sender").await?;
assert_private_commitment_in_state(&ctx, to, "receiver").await?;
info!("Successfully transferred privately to owned account");
log::info!("Successfully transferred privately to owned account");
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await?;
let acc1_ind_state = ctx
@@ -65,7 +64,7 @@ async fn indexer_state_consistency() -> Result<()> {
.await
.unwrap();
info!("Checking correct state transition");
log::info!("Checking correct state transition");
let acc1_seq_state = get_account(&ctx, ctx.existing_public_accounts()[0]).await?;
let acc2_seq_state = get_account(&ctx, ctx.existing_public_accounts()[1]).await?;
@@ -12,7 +12,6 @@ use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, public_mention,
send, wait_for_indexer_to_catch_up,
};
use log::info;
use wallet::{
account::Label,
cli::{CliAccountMention, Command},
@@ -47,7 +46,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
)
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?;
@@ -56,7 +55,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
assert_eq!(acc_1_balance, 9900);
assert_eq!(acc_2_balance, 20100);
info!("Waiting for indexer to parse blocks");
log::info!("Waiting for indexer to parse blocks");
wait_for_indexer_to_catch_up(&ctx).await?;
let acc1_ind_state = ctx
@@ -68,7 +67,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> {
assert_eq!(acc1_ind_state, acc1_seq_state.into());
info!("Indexer state is consistent after label-based transfer");
log::info!("Indexer state is consistent after label-based transfer");
Ok(())
}
+2 -3
View File
@@ -5,7 +5,6 @@
use anyhow::Result;
use integration_tests::{TestContext, wait_for_indexer_to_catch_up};
use log::info;
#[tokio::test]
async fn indexer_test_run() -> Result<()> {
@@ -16,8 +15,8 @@ async fn indexer_test_run() -> Result<()> {
let last_block_seq =
sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?;
info!("Last block on seq now is {last_block_seq}");
info!("Last block on ind now is {last_block_indexer}");
log::info!("Last block on seq now is {last_block_seq}");
log::info!("Last block on ind now is {last_block_indexer}");
assert!(last_block_indexer > 0);
@@ -4,7 +4,6 @@
)]
use anyhow::Result;
use log::info;
#[path = "indexer_ffi_helpers/mod.rs"]
mod indexer_ffi_helpers;
@@ -19,7 +18,7 @@ fn indexer_test_run_ffi() -> Result<()> {
// returning early instead of sleeping for the full timeout.
let last_block_indexer_ffi = indexer_ffi_helpers::wait_for_indexer_ffi_block(&indexer_ffi, 1)?;
info!("Last block on indexer FFI now is {last_block_indexer_ffi}");
log::info!("Last block on indexer FFI now is {last_block_indexer_ffi}");
assert!(last_block_indexer_ffi > 0);
+6 -7
View File
@@ -14,7 +14,6 @@ use integration_tests::{
};
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use lee::AccountId;
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::cli::{
@@ -83,7 +82,7 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
.context("Failed to get recipient's private account")?;
assert_eq!(to_res_acc.balance, 100);
info!("Successfully transferred using claiming path");
log::info!("Successfully transferred using claiming path");
Ok(())
}
@@ -125,7 +124,7 @@ async fn restore_keys_from_seed() -> Result<()> {
send_claiming_new_account(&mut ctx, from, to_account_id3, 102).await?;
send_claiming_new_account(&mut ctx, from, to_account_id4, 103).await?;
info!("Preparation complete, performing keys restoration");
log::info!("Preparation complete, performing keys restoration");
// Restore keys from seed
wallet::cli::execute_keys_restoration(ctx.wallet_mut(), 10).await?;
@@ -140,17 +139,17 @@ async fn restore_keys_from_seed() -> Result<()> {
assert_eq!(
acc1.account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(
acc2.account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(acc1.account.balance, 100);
assert_eq!(acc2.account.balance, 101);
info!("Tree checks passed, testing restored accounts can transact");
log::info!("Tree checks passed, testing restored accounts can transact");
// Test that restored accounts can send transactions
send(
@@ -196,7 +195,7 @@ async fn restore_keys_from_seed() -> Result<()> {
assert_eq!(acc3, 91); // 102 - 11
assert_eq!(acc4, 114); // 103 + 11
info!("Successfully restored keys and verified transactions");
log::info!("Successfully restored keys and verified transactions");
Ok(())
}
+107 -118
View File
@@ -3,8 +3,8 @@
reason = "top-level test functions are conventional for integration tests"
)]
//! Two sequencers share one channel: A starts solo as channel admin, live-
//! accredits `[A, B]` with round-robin rotation, B joins and syncs, both
//! Two sequencers share one channel: both are staked at genesis, so the channel
//! is created already accrediting `[A, B]`, B syncs the chain A began, both
//! produce on their turns, and A, B and an indexer converge on the same chain.
use std::time::Duration;
@@ -13,109 +13,97 @@ use anyhow::{Context as _, Result, ensure};
use indexer_service_rpc::RpcClient as _;
use integration_tests::{
config::{self, SequencerPartialConfig},
indexer_client::IndexerClient,
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
init_logger,
};
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
use sequencer_core::{
block_publisher::{Ed25519PublicKey, read_channel_state},
config::BedrockConfig,
};
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
use sequencer_core::{block_publisher::post_channel_config, config::BedrockConfig};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts};
use tokio::test;
/// 1 s bedrock slots: rotate the turn every ~20 s of tenure; steal a stalled
/// turn after ~30 s (bounds the stall while B is accredited but not started).
const POSTING_TIMEFRAME_SLOTS: u32 = 20;
const POSTING_TIMEOUT_SLOTS: u32 = 30;
const PHASE_TIMEOUT: Duration = Duration::from_secs(360);
const POLL_INTERVAL: Duration = Duration::from_secs(2);
const TRANSFER_AMOUNT: u128 = 10;
/// ≈4 turn windows past B's join (5 s blocks, ~20 s turns → ~4 blocks/window).
/// ≈4 turn windows, at the `system_accounts` posting timeframe and 5 s blocks.
const ROTATION_BLOCKS: u64 = 8;
#[test]
async fn multi_sequencer_committee_converges() -> Result<()> {
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up Bedrock node")?;
// Fixed seeds so A can accredit B's public key before B exists.
let key_a = [0xA1_u8; ED25519_SECRET_KEY_SIZE];
let key_b = [0xB2_u8; ED25519_SECRET_KEY_SIZE];
let pub_a = Ed25519Key::from_bytes(&key_a).public_key();
let pub_b = Ed25519Key::from_bytes(&key_b).public_key();
init_logger();
let channel = config::bedrock_channel_id();
let partial = SequencerPartialConfig {
block_create_timeout: Duration::from_secs(5),
..SequencerPartialConfig::default()
};
// Phase 1: A solo (its first inscription creates the channel), plus an indexer.
let (seq_a, _a_home) = SequencerSetup::new(partial, bedrock_addr)
.with_genesis(vec![])
.with_bedrock_signing_key(key_a)
.setup()
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 2,
bedrock_channel: channel,
})
.disable_wallet()
.with_sequencer_partial_config(partial),
)
.build()
.await
.context("Failed to set up sequencer A")?;
let a = sequencer_client(seq_a.addr())?;
let (idx, _idx_home) = setup_indexer(bedrock_addr, config::bedrock_channel_id(), None)
.await
.context("Failed to set up indexer")?;
let indexer = indexer_client(idx.addr()).await?;
.context("Failed to build the two-sequencer test context")?;
wait_for_height(&a, 2, "sequencer A to produce past genesis").await?;
let a = ctx
.sequencer_client_by_node_ids(channel, 0)
.context("Missing sequencer A")?;
let b = ctx
.sequencer_client_by_node_ids(channel, 1)
.context("Missing sequencer B")?;
let indexer = ctx.indexer_client();
// Phase 2: live roster change to [A, B] with rotation enabled, posted
// straight to bedrock with A's admin key (the operator one-shot path).
post_channel_config(
&BedrockConfig {
channel_id: config::bedrock_channel_id(),
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
funding_key: config::bedrock_funding_key(),
auth: None,
priority_fee: sequencer_core::config::default_priority_fee(),
},
&Ed25519Key::from_bytes(&key_a),
vec![pub_a, pub_b],
POSTING_TIMEFRAME_SLOTS,
POSTING_TIMEOUT_SLOTS,
1,
1,
)
.await
.context("Failed to configure the channel committee")?;
let pub_a = Ed25519Key::from_bytes(&config::SEQUENCER_SIGNING_KEY).public_key();
let pub_b = Ed25519Key::from_bytes(&config::sequencer_signing_key_from_seed(1)).public_key();
let height_at_config = a.get_last_block_id().await?;
wait_for_height(
&a,
height_at_config + 1,
"A to produce after the roster change",
)
let bedrock_config = BedrockConfig {
channel_id: channel,
node_url: config::addr_to_url(config::UrlProtocol::Http, ctx.bedrock_addr())?,
funding_key: config::bedrock_funding_key(),
auth: None,
priority_fee: sequencer_core::config::default_priority_fee(),
};
// Phase 1: both keys accredited from channel creation.
let mut want = vec![pub_a.to_bytes(), pub_b.to_bytes()];
want.sort_unstable();
wait_until("Bedrock to accredit both staked keys", || async {
Ok(committee(&bedrock_config).await?.0 == want)
})
.await?;
// Phase 3: B joins live and syncs the existing chain.
let (seq_b, _b_home) = SequencerSetup::new(partial, bedrock_addr)
.with_genesis(vec![])
.with_bedrock_signing_key(key_b)
.setup()
.await
.context("Failed to set up sequencer B")?;
let b = sequencer_client(seq_b.addr())?;
// Phase 2: B follows the chain A began.
let join_height = a.get_last_block_id().await?.max(1);
wait_for_height(b, join_height, "B to sync to A's height").await?;
let join_height = a.get_last_block_id().await?;
wait_for_height(&b, join_height, "B to sync to A's height at join").await?;
// Phase 4: rotation + convergence over ≈4 turn windows.
// Phase 3: rotation + convergence; without the turn check, a chain A
// produces alone satisfies everything below.
let rotation_target = join_height + ROTATION_BLOCKS;
wait_for_height(
&a,
a,
rotation_target,
"the chain to advance across turn windows",
)
.await?;
wait_for_height(&b, rotation_target, "B to follow across turn windows").await?;
assert_same_chain(&a, &b).await?;
wait_for_height(b, rotation_target, "B to follow across turn windows").await?;
wait_until("the round-robin turn to reach B", || async {
Ok(committee(&bedrock_config).await?.1 == Some(pub_b))
})
.await?;
assert_same_chain(a, b).await?;
// Phase 5: a tx submitted only to B is included by B and visible on A.
// Phase 4: a tx submitted only to B is included by B and visible on A.
let accounts = initial_public_user_accounts();
let from = accounts[0].account_id;
let to = accounts[1].account_id;
@@ -134,10 +122,17 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
.await
.context("Failed to submit the transfer to B")?;
wait_for_balance(&a, to, to_balance_before + TRANSFER_AMOUNT).await?;
let expected = to_balance_before + TRANSFER_AMOUNT;
wait_until("the cross-sequencer transfer to reach A", || async {
Ok(a.get_account_balance(to).await? == expected)
})
.await?;
// Phase 6: the indexer finalizes the same chain, with no stall.
wait_for_finalized(&indexer, join_height).await?;
// Phase 5: the indexer finalizes the same chain, with no stall.
wait_until("the indexer to finalize", || async {
Ok(indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= join_height)
})
.await?;
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
for id in 1..=finalized {
let block_i = indexer
@@ -163,53 +158,47 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
Ok(())
}
/// Polls `check` until it reports ready, failing with `what` on timeout.
async fn wait_until<F, Fut>(what: &str, mut check: F) -> Result<()>
where
F: FnMut() -> Fut,
Fut: Future<Output = Result<bool>>,
{
let wait = async {
while !check().await? {
tokio::time::sleep(POLL_INTERVAL).await;
}
Ok::<(), anyhow::Error>(())
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.with_context(|| format!("Timed out waiting for {what}"))?
}
/// Polls the sequencer until its chain height reaches `target`.
async fn wait_for_height(client: &SequencerClient, target: u64, what: &str) -> Result<()> {
let wait = async {
loop {
if client.get_last_block_id().await? >= target {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.with_context(|| format!("Timed out waiting for {what} (target height {target})"))?
wait_until(&format!("{what} (target height {target})"), || async {
Ok(client.get_last_block_id().await? >= target)
})
.await
}
/// Polls the sequencer until `account`'s balance reaches `expected`.
async fn wait_for_balance(
client: &SequencerClient,
account: lee::AccountId,
expected: u128,
) -> Result<()> {
let wait = async {
loop {
if client.get_account_balance(account).await? == expected {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
/// The channel's accredited keys, sorted, plus whose turn the tip was written on.
async fn committee(config: &BedrockConfig) -> Result<(Vec<[u8; 32]>, Option<Ed25519PublicKey>)> {
let Some(state) = read_channel_state(config).await? else {
return Ok((Vec::new(), None));
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.context("Timed out waiting for the cross-sequencer transfer to reach A")?
}
/// Polls the indexer until its finalized height reaches `target`.
async fn wait_for_finalized(indexer: &IndexerClient, target: u64) -> Result<()> {
let wait = async {
loop {
if indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= target {
return Ok::<(), anyhow::Error>(());
}
tokio::time::sleep(POLL_INTERVAL).await;
}
};
tokio::time::timeout(PHASE_TIMEOUT, wait)
.await
.context("Timed out waiting for the indexer to finalize")?
let turn = state
.accredited_keys
.get(usize::from(state.tip_sequencer))
.copied();
let mut keys: Vec<_> = state
.accredited_keys
.iter()
.map(Ed25519PublicKey::to_bytes)
.collect();
keys.sort_unstable();
Ok((keys, turn))
}
/// Asserts A and B hold byte-identical block hashes over their common prefix.
+7 -8
View File
@@ -27,7 +27,6 @@ use lee_core::{
encryption::ViewingPublicKey,
program::PdaSeed,
};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use tokio::test;
use wallet::{AccountIdentity, WalletCore};
@@ -181,7 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
// ── Receive ──────────────────────────────────────────────────────────────────────────────────
info!("Sending to alice_pda_0 (identifier=0)");
log::info!("Sending to alice_pda_0 (identifier=0)");
fund_private_pda(
ctx.wallet_mut(),
sender_0,
@@ -195,7 +194,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
)
.await?;
info!("Sending to alice_pda_1 (identifier=1)");
log::info!("Sending to alice_pda_1 (identifier=1)");
fund_private_pda(
ctx.wallet_mut(),
sender_1,
@@ -209,7 +208,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
)
.await?;
info!("Waiting for block");
log::info!("Waiting for block");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
// Sync so alice's wallet discovers and stores both PDAs.
@@ -263,7 +262,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
let amount_spend_0: u128 = 13;
let amount_spend_1: u128 = 37;
info!("Alice spending from alice_pda_0");
log::info!("Alice spending from alice_pda_0");
spend_private_pda(
ctx.wallet_mut(),
alice_pda_0_id,
@@ -276,7 +275,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
)
.await?;
info!("Alice spending from alice_pda_1");
log::info!("Alice spending from alice_pda_1");
spend_private_pda(
ctx.wallet_mut(),
alice_pda_1_id,
@@ -289,7 +288,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
)
.await?;
info!("Waiting for block");
log::info!("Waiting for block");
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
sync_private(&mut ctx).await?;
@@ -326,6 +325,6 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
"alice_pda_1 post-spend commitment not in state"
);
info!("Private PDA family member receive-and-spend test passed");
log::info!("Private PDA family member receive-and-spend test passed");
Ok(())
}
+18 -12
View File
@@ -8,8 +8,10 @@ use std::{io::Write as _, time::Duration};
use anyhow::Result;
use common::transaction::LeeTransaction;
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account};
use log::info;
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
use wallet::{cli::Command, config::WalletConfigOverrides};
@@ -45,7 +47,7 @@ async fn deploy_and_execute_program() -> Result<()> {
.send_transaction(LeeTransaction::Public(transaction))
.await?;
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
// Waiting for long time as it may take some time for such a big transaction to be included in a
// block
tokio::time::sleep(Duration::from_secs(2 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
@@ -53,12 +55,12 @@ async fn deploy_and_execute_program() -> Result<()> {
let post_state_account = get_account(&ctx, account_id).await?;
let expected_data: &[u8] = &[];
assert_eq!(post_state_account.program_owner, claimer.id());
assert_eq!(post_state_account.program_owner, claimer.id().into());
assert_eq!(post_state_account.balance, 0);
assert_eq!(post_state_account.data.as_ref(), expected_data);
assert_eq!(post_state_account.nonce.0, 1);
info!("Successfully deployed and executed program");
log::info!("Successfully deployed and executed program");
Ok(())
}
@@ -68,13 +70,17 @@ async fn deploy_invalid_program_fails() -> Result<()> {
// An invalid program bytecode is rejected by the sequencer during block production, so the
// deployment transaction is never included in a block. Shrink the wallet's polling window so
// the command gives up quickly instead of waiting for the full default timeout.
let mut ctx = TestContext::builder()
.with_wallet_config_overrides(WalletConfigOverrides {
seq_poll_timeout: Some(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)),
seq_tx_poll_max_blocks: Some(5),
seq_poll_max_retries: Some(2),
..WalletConfigOverrides::default()
})
let mut ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_wallet_config_overrides(WalletConfigOverrides {
seq_poll_timeout: Some(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)),
seq_tx_poll_max_blocks: Some(5),
seq_poll_max_retries: Some(2),
..WalletConfigOverrides::default()
}),
)
.build()
.await?;
@@ -92,7 +98,7 @@ async fn deploy_invalid_program_fails() -> Result<()> {
"Deploying an invalid program should fail, but got: {result:?}"
);
info!("Deploying an invalid program failed as expected");
log::info!("Deploying an invalid program failed as expected");
Ok(())
}
+23 -14
View File
@@ -13,7 +13,6 @@ use std::{path::Path, time::Duration};
use anyhow::{Context as _, Result, bail};
use indexer_service_rpc::RpcClient as _;
use lee::{AccountId, PrivateKey, PublicKey};
use logos_blockchain_core::mantle::ops::channel::ChannelId;
use sequencer_core::config::GenesisAction;
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
@@ -237,8 +236,11 @@ async fn empty_local_reconstructs_from_populated_bedrock() -> Result<()> {
// lost its local DB.
drop(handle_a);
tokio::time::sleep(Duration::from_secs(2)).await;
std::fs::remove_dir_all(home_a.path().join("rocksdb"))
.context("Failed to wipe sequencer L2 store")?;
std::fs::remove_dir_all(home_a.path().join(format!(
"rocksdb-{}",
test_fixtures::config::bedrock_channel_id()
)))
.context("Failed to wipe sequencer L2 store")?;
// Sequencer B restarts on the same home from that empty store and reconstructs.
let handle_b = SequencerSetup::new(slow_blocks(), bedrock_addr)
@@ -274,11 +276,13 @@ async fn empty_local_reconstructs_from_populated_bedrock() -> Result<()> {
/// Case 3: local store is not empty, but the Bedrock channel is empty.
///
/// A sequencer produces blocks (committing to a channel), is stopped, and is
/// restarted against a fresh/empty channel — i.e. the channel it committed to
/// was wiped or the node points at a different chain. Startup must fail rather
/// than silently resume onto a foreign channel. Crucially this must hold even
/// though the sequencer only ever *produced* (so it never recorded a per-block
/// anchor): the committed-but-missing-channel invariant catches it.
/// restarted with the same channel id against a Bedrock node where that channel
/// is empty — i.e. the channel it committed to was wiped. Startup must fail
/// rather than silently resume onto a foreign channel. Crucially this must hold
/// even though the sequencer only ever *produced* (so it never recorded a
/// per-block anchor): the committed-but-missing-channel invariant catches it.
/// A *different* channel id no longer exercises this, because the db path is
/// per-channel and a new id simply fresh-starts beside the old store.
#[test]
async fn nonempty_local_against_empty_channel_fails_startup() -> Result<()> {
const PRODUCED_TARGET: u64 = 3;
@@ -310,9 +314,12 @@ async fn nonempty_local_against_empty_channel_fails_startup() -> Result<()> {
drop(handle_a);
tokio::time::sleep(Duration::from_secs(2)).await;
// Restart on the SAME home (A's committed store: blocks + checkpoint) but
// pointed at a fresh, never-used channel — the channel it committed to is gone.
let empty_channel = ChannelId::from([0x5a_u8; 32]);
// Restart on the SAME home (A's committed store: blocks + checkpoint) and the
// SAME channel id, but against a fresh Bedrock node where that channel does
// not exist — the channel it committed to is gone.
let (_bedrock_b, bedrock_addr_b) = setup_bedrock_node()
.await
.context("Failed to setup second Bedrock")?;
// Startup aborts on the missing-channel invariant (a panic in
// `start_from_config`). Run it on a dedicated OS thread with its own runtime
@@ -325,8 +332,7 @@ async fn nonempty_local_against_empty_channel_fails_startup() -> Result<()> {
runtime.block_on(async {
tokio::time::timeout(
Duration::from_secs(90),
SequencerSetup::new(slow_blocks(), bedrock_addr)
.with_channel_id(empty_channel)
SequencerSetup::new(slow_blocks(), bedrock_addr_b)
.with_genesis(genesis)
.setup_at(&home_a_path),
)
@@ -473,7 +479,10 @@ async fn local_behind_channel_reconstructs_forward() -> Result<()> {
];
let home = tempfile::tempdir().context("Failed to create sequencer home")?;
let rocksdb = home.path().join("rocksdb");
let rocksdb = home.path().join(format!(
"rocksdb-{}",
test_fixtures::config::bedrock_channel_id()
));
// Bring the sequencer up to an early tip, then stop it so its store is at rest.
{
@@ -0,0 +1,392 @@
//! End-to-end demo of the sequencer self-join flow.
#![expect(
clippy::tests_outside_test_module,
reason = "Integration tests live at crate root and don't care about these lints"
)]
use std::time::Duration;
use anyhow::{Context as _, Result};
use integration_tests::{account_balance, get_account, new_account};
use lee::{AccountId, PrivateKey, PublicKey, program::Program};
use log::info;
use logos_blockchain_core::mantle::ops::channel::Ed25519PublicKey;
use logos_blockchain_key_management_system_service::keys::Ed25519Key;
use logos_blockchain_zone_sdk::{
CommonHttpClient,
adapter::{Node as _, NodeHttpClient},
};
use sequencer_core::config::GenesisAction;
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, TestContext, ZoneTestContextBuilder,
config::{
MultiNodeTestContextConfig, SequencerPartialConfig, UrlProtocol, addr_to_url,
bedrock_channel_id,
},
setup::{SequencerSetup, sequencer_client},
};
use tokio::test;
use wallet::AccountIdentity;
/// Comfortably above `system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE`.
const FUNDING_BALANCE: u128 = 2 * system_accounts::DEFAULT_MINIMUM_SEQUENCER_STAKE;
/// Bedrock signing key of the sequencer that stakes its way in.
const JOINER_SIGNING_KEY: [u8; 32] = [0x42; 32];
/// Short block cadence for the demo.
fn fast_blocks() -> SequencerPartialConfig {
SequencerPartialConfig {
block_create_timeout: Duration::from_secs(2),
..SequencerPartialConfig::default()
}
}
#[test]
async fn stake_transaction_joins_the_bedrock_committee() -> Result<()> {
let demo_sequencer_key = Ed25519Key::from_bytes(&JOINER_SIGNING_KEY).public_key();
let demo_stake_key = sequencer_stake_core::SequencerKey::new(demo_sequencer_key.to_bytes())
.expect("a Bedrock key is a valid Ed25519 public key");
let funding_private_key = PrivateKey::new_os_random();
let funding_id = AccountId::from(&PublicKey::new_from_private_key(&funding_private_key));
let mut ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_sequencer_partial_config(fast_blocks())
.with_genesis(vec![GenesisAction::SupplyAccount {
account_id: funding_id,
balance: FUNDING_BALANCE,
}]),
)
.build()
.await
.context("Failed to build test context")?;
// Import the funding key directly; it's not one of the wallet's default accounts.
ctx.wallet_mut()
.storage_mut()
.key_chain_mut()
.add_imported_public_account(funding_private_key);
// Claim the genesis supply out of its vault.
let owner_vault_id = vault_core::compute_vault_account_id(programs::vault().id(), funding_id);
let claim_instruction_data = Program::serialize_instruction(vault_core::Instruction::Claim {
amount: FUNDING_BALANCE,
})
.context("Failed to serialize vault Claim instruction")?;
ctx.wallet()
.send_pub_tx(
vec![
AccountIdentity::Public(funding_id),
AccountIdentity::PublicNoSign(owner_vault_id),
],
claim_instruction_data,
programs::vault().id(),
)
.await
.map_err(|err| {
anyhow::anyhow!(
"Failed to claim the demo funding account from its genesis vault: {err:?}"
)
})?;
info!("Waiting for the vault-claim transaction's block to land");
poll_until("vault claim to land", 30, || async {
Ok(account_balance(&ctx, funding_id).await? == FUNDING_BALANCE)
})
.await?;
info!("Funded demo account {funding_id} with {FUNDING_BALANCE} native balance");
let ownership_id = new_account(&mut ctx, false, None)
.await
.context("Failed to create a fresh stake ownership account")?;
info!("Fresh stake ownership account: {ownership_id}");
let mover_instruction_data =
Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer {
amount: FUNDING_BALANCE,
})
.context("Failed to serialize mover instruction")?;
let stake_instruction_data =
Program::serialize_instruction(sequencer_stake_core::Instruction::Stake {
sequencer_key: demo_stake_key,
amount: FUNDING_BALANCE,
mover_program_id: programs::authenticated_transfer().id(),
mover_instruction_data,
})
.context("Failed to serialize Stake instruction")?;
info!(
"Submitting Stake transaction for sequencer key {}",
hex::encode(demo_sequencer_key.to_bytes())
);
let config_id = system_accounts::sequencer_stake_config_account_id();
ctx.wallet()
.send_pub_tx(
vec![
AccountIdentity::Public(funding_id),
AccountIdentity::Public(ownership_id),
AccountIdentity::PublicNoSign(config_id),
],
stake_instruction_data,
programs::sequencer_stake().id(),
)
.await
.map_err(|err| anyhow::anyhow!("Failed to submit Stake transaction: {err:?}"))?;
info!("Waiting for the Stake transaction's block to land");
poll_until("stake to take ownership", 30, || async {
Ok(get_account(&ctx, ownership_id).await?.program_owner
== programs::sequencer_stake().id().into())
})
.await?;
let ownership_account = get_account(&ctx, ownership_id)
.await
.context("Failed to read the stake ownership account")?;
assert_eq!(
ownership_account.program_owner,
programs::sequencer_stake().id().into(),
"ownership account should now be owned by sequencer_stake"
);
assert_eq!(
ownership_account.balance, FUNDING_BALANCE,
"ownership account should hold the staked balance"
);
let record = sequencer_stake_core::StakeRecord::from_bytes(ownership_account.data.as_ref())
.context("ownership account data did not decode as a StakeRecord")?;
assert_eq!(record.sequencer_key, demo_stake_key);
info!(
"Ownership account confirmed: {} staked for sequencer key {}",
ownership_account.balance,
hex::encode(record.sequencer_key)
);
let bedrock_url = addr_to_url(UrlProtocol::Http, ctx.bedrock_addr())
.context("Failed to build the Bedrock node URL")?;
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
// The committee-config update is a separate tx from the block's own
// publish, so it may land a moment later — poll a few times before failing.
let mut channel_state = None;
for _ in 0..10 {
let state = node
.channel_state(bedrock_channel_id())
.await
.context("Failed to read Bedrock channel state")?
.context("Bedrock channel does not exist")?;
if state
.accredited_keys
.iter()
.any(|key: &Ed25519PublicKey| *key == demo_sequencer_key)
{
channel_state = Some(state);
break;
}
tokio::time::sleep(Duration::from_secs(3)).await;
}
let channel_state = channel_state.context(
"demo sequencer key should have been discovered and accredited after the Stake transaction",
)?;
info!(
"Bedrock channel now accredits {} key(s), including the demo sequencer key — self-join complete",
channel_state.accredited_keys.len()
);
// Only now start a node behind the key, against a channel that already has a chain.
let (joiner, _joiner_home) = SequencerSetup::new(fast_blocks(), ctx.bedrock_addr())
.with_channel_id(bedrock_channel_id())
.with_bedrock_signing_key(JOINER_SIGNING_KEY)
.joining_existing_channel()
.setup()
.await
.context("Failed to start the joining sequencer")?;
let joiner_client = sequencer_client(joiner.addr())?;
let joined_at = ctx.sequencer_client().get_last_block_id().await?;
poll_until("the joining sequencer to sync the existing chain", 120, {
let joiner_client = &joiner_client;
move || async move { Ok(joiner_client.get_last_block_id().await? >= joined_at) }
})
.await?;
info!("Joining sequencer synced to block {joined_at}");
// A tip past `joined_at` under the demo key is a block this node built.
poll_until("the joining sequencer to build a block on its turn", 180, {
let node = &node;
let ctx = &ctx;
move || async move {
let Some(state) = node.channel_state(bedrock_channel_id()).await? else {
return Ok(false);
};
let turn = state
.accredited_keys
.get(usize::from(state.tip_sequencer))
.copied();
Ok(turn == Some(demo_sequencer_key)
&& ctx.sequencer_client().get_last_block_id().await? > joined_at)
}
})
.await?;
info!("Joining sequencer produced a block on its round-robin turn");
// Both nodes agree, block for block, over everything they share.
let leader_client = ctx.sequencer_client();
let common = leader_client
.get_last_block_id()
.await?
.min(joiner_client.get_last_block_id().await?);
for id in 1..=common {
let leader_block = leader_client
.get_block(id)
.await?
.with_context(|| format!("Leader is missing block {id}"))?;
let joiner_block = joiner_client
.get_block(id)
.await?
.with_context(|| format!("Joining sequencer is missing block {id}"))?;
anyhow::ensure!(
leader_block.header.hash == joiner_block.header.hash,
"Chain divergence at block {id}: leader {:?} vs joiner {:?}",
leader_block.header.hash,
joiner_block.header.hash
);
}
info!("Leader and joining sequencer agree on all {common} shared blocks");
// Exit flow: full UnstakeRequest, wait for the committee removal to land on
// Bedrock, then check the sequencer's own FinalizeUnstake releases the stake.
//
// FinalizeUnstake is unsigned/permissionless, so it can't claim a fresh
// destination account (that needs the owner's own signature, same as
// authenticated_transfer's Transfer). Reuse funding_id: already
// authenticated_transfer-owned, drained to 0 by the Stake above.
let destination_id = funding_id;
let unstake_request_data =
Program::serialize_instruction(sequencer_stake_core::Instruction::UnstakeRequest {
amount: FUNDING_BALANCE,
destination: destination_id,
})
.context("Failed to serialize UnstakeRequest instruction")?;
ctx.wallet()
.send_pub_tx(
vec![
AccountIdentity::Public(ownership_id),
AccountIdentity::PublicNoSign(config_id),
],
unstake_request_data,
programs::sequencer_stake().id(),
)
.await
.map_err(|err| anyhow::anyhow!("Failed to submit UnstakeRequest transaction: {err:?}"))?;
info!("Submitted full UnstakeRequest for the demo sequencer key");
// A full drain crosses below the minimum, so discovery removes the key.
// Wide window: the removal has to wait for this sequencer to regain its
// round-robin turn (posting_timeframe/posting_timeout reclaim), on top of
// normal Bedrock confirmation latency.
let mut removed = false;
for _ in 0..30 {
let state = node
.channel_state(bedrock_channel_id())
.await
.context("Failed to read Bedrock channel state")?
.context("Bedrock channel does not exist")?;
if !state
.accredited_keys
.iter()
.any(|key: &Ed25519PublicKey| *key == demo_sequencer_key)
{
removed = true;
break;
}
tokio::time::sleep(Duration::from_secs(3)).await;
}
anyhow::ensure!(
removed,
"demo sequencer key should have been removed after the full UnstakeRequest"
);
info!("Demo sequencer key removed from the Bedrock committee");
// Once removed, the sequencer injects FinalizeUnstake itself; this test
// never submits one.
poll_until(
"FinalizeUnstake to drain the ownership account",
90,
|| async { Ok(get_account(&ctx, ownership_id).await?.balance == 0) },
)
.await?;
let drained_ownership_account = get_account(&ctx, ownership_id)
.await
.context("Failed to read the drained ownership account")?;
assert_eq!(
drained_ownership_account.balance, 0,
"ownership account should be fully drained"
);
let drained_record =
sequencer_stake_core::StakeRecord::from_bytes(drained_ownership_account.data.as_ref())
.context("drained ownership account data did not decode as a StakeRecord")?;
assert!(
drained_record.pending_unstake.is_none(),
"pending unstake should be cleared"
);
let destination_balance = account_balance(&ctx, destination_id).await?;
assert_eq!(
destination_balance, FUNDING_BALANCE,
"destination should receive the released stake"
);
// Nothing is at stake for this key any more: a fully drained account has
// its config entry removed outright.
assert!(
stake_entry(&ctx, config_id, demo_stake_key)
.await?
.is_none(),
"the config entry should be gone once the stake is fully released"
);
info!(
"FinalizeUnstake auto-included: {FUNDING_BALANCE} released to {destination_id}, nothing left at stake"
);
Ok(())
}
/// The `sequencer_stake` config entry for `sequencer_key`, or `None` if the key
/// has nothing at stake.
async fn stake_entry(
ctx: &TestContext,
config_id: AccountId,
sequencer_key: sequencer_stake_core::SequencerKey,
) -> Result<Option<sequencer_stake_core::SequencerEntry>> {
let config_account = get_account(ctx, config_id)
.await
.context("Failed to read the sequencer_stake config account")?;
let config =
sequencer_stake_core::SequencerStakeConfig::from_bytes(config_account.data.as_ref())
.context("config account data did not decode as a SequencerStakeConfig")?;
Ok(config.entries.get(&sequencer_key).copied())
}
/// Polls `check` once a second, up to `max_attempts` times, replacing fixed
/// block-wait sleeps: the accelerated devnet crosses an epoch boundary every
/// ~100 slots, so every second of wall-clock spent sleeping increases the
/// chance of straddling one.
async fn poll_until<F, Fut>(what: &str, max_attempts: u32, mut check: F) -> Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<bool>>,
{
for _ in 0..max_attempts {
if check().await.unwrap_or(false) {
return Ok(());
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
anyhow::bail!("timed out waiting for {what}")
}
+3 -4
View File
@@ -21,7 +21,6 @@ use anyhow::{Context as _, Result};
use integration_tests::{
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, sync_private,
};
use log::info;
use tokio::test;
use wallet::{
account::Label,
@@ -81,7 +80,7 @@ async fn group_create_and_shared_account_registration() -> Result<()> {
assert_eq!(entry.group_label, Label::new("test-group"));
assert!(entry.pda_seed.is_none());
info!("Shared account registered: {shared_account_id}");
log::info!("Shared account registered: {shared_account_id}");
Ok(())
}
@@ -156,7 +155,7 @@ async fn group_invite_join_key_agreement() -> Result<()> {
"Key agreement: same GMS produces same keys"
);
info!("Key agreement verified via invite/join");
log::info!("Key agreement verified via invite/join");
Ok(())
}
@@ -225,7 +224,7 @@ async fn fund_shared_account_from_public() -> Result<()> {
.shared_private_account(shared_id)
.context("Shared account not found after sync")?;
info!(
log::info!(
"Shared account balance after funding: {}",
entry.account.balance
);
+29 -17
View File
@@ -14,7 +14,7 @@ use std::time::{Duration, Instant};
use anyhow::{Context as _, Result};
use bytesize::ByteSize;
use common::transaction::LeeTransaction;
use integration_tests::{TestContext, config::SequencerPartialConfig};
use integration_tests::config::SequencerPartialConfig;
use lee::{
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
privacy_preserving_transaction::{self as pptx, circuit},
@@ -22,14 +22,16 @@ use lee::{
public_transaction as putx,
};
use lee_core::{
DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey,
NullifierWitness, PrivateWitness, WitnessKind,
AuthorizationSecretKey, DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof,
NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, WitnessKind,
account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
};
use log::info;
use sequencer_core::config::GenesisAction;
use sequencer_service_rpc::RpcClient as _;
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
pub(crate) struct TpsTestManager {
@@ -178,9 +180,13 @@ pub async fn tps_test() -> Result<()> {
let target_tps = 8;
let tps_test = TpsTestManager::new(target_tps, num_transactions);
let ctx = TestContext::builder()
.with_sequencer_partial_config(TpsTestManager::generate_sequencer_partial_config())
.with_genesis(tps_test.generate_genesis())
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig::default())
.with_sequencer_partial_config(TpsTestManager::generate_sequencer_partial_config())
.with_genesis(tps_test.generate_genesis()),
)
.build()
.await?;
@@ -191,7 +197,7 @@ pub async fn tps_test() -> Result<()> {
.context("Failed to claim vault funds for TPS accounts")?;
let target_time = tps_test.target_time();
info!(
log::info!(
"TPS test begin. Target time is {target_time:?} for {num_transactions} transactions ({target_tps} TPS)"
);
@@ -205,7 +211,7 @@ pub async fn tps_test() -> Result<()> {
.send_transaction(LeeTransaction::Public(tx))
.await
.unwrap();
info!("Sent tx {i}");
log::info!("Sent tx {i}");
tx_hashes.push(tx_hash);
}
@@ -225,7 +231,7 @@ pub async fn tps_test() -> Result<()> {
});
if tx_obj.is_ok_and(|opt| opt.is_some()) {
info!("Found tx {i} with hash {tx_hash}");
log::info!("Found tx {i} with hash {tx_hash}");
break;
}
}
@@ -234,7 +240,7 @@ pub async fn tps_test() -> Result<()> {
let tx_processed = tx_hashes.len();
let actual_tps = tx_processed as u64 / time_elapsed;
info!("Processed {tx_processed} transactions in {time_elapsed:?} ({actual_tps} TPS)",);
log::info!("Processed {tx_processed} transactions in {time_elapsed:?} ({actual_tps} TPS)",);
assert_eq!(tx_processed, num_transactions);
@@ -243,7 +249,7 @@ pub async fn tps_test() -> Result<()> {
"Elapsed time {time_elapsed:?} exceeded target time {target_time:?}"
);
info!("TPS test finished successfully");
log::info!("TPS test finished successfully");
Ok(())
}
@@ -255,20 +261,22 @@ pub async fn tps_test() -> Result<()> {
#[expect(dead_code, reason = "No idea if we need this, should we remove it?")]
fn build_privacy_transaction() -> PrivacyPreservingTransaction {
let program = programs::authenticated_transfer();
let sender_nsk = [1; 32];
let sender_ask = AuthorizationSecretKey([1; 32]);
let sender_nsk = NullifierSecretKey::from(&sender_ask);
let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]);
let sender_npk = NullifierPublicKey::from(&sender_nsk);
let sender_pre = AccountWithMetadata::new(
Account {
balance: 100,
nonce: Nonce(0xdead_beef),
program_owner: program.id(),
program_owner: program.id().into(),
data: Data::default(),
},
true,
AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0),
);
let recipient_nsk = [2; 32];
let recipient_ask = AuthorizationSecretKey([2; 32]);
let recipient_nsk = NullifierSecretKey::from(&recipient_ask);
let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]);
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
let recipient_pre = AccountWithMetadata::new(
@@ -296,7 +304,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
vpk: sender_vpk,
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_nsk,
@@ -307,7 +317,9 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
vpk: recipient_vpk,
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_ask),
},
nullifier: NullifierWitness::Init {
npk: recipient_npk,
commitment_root: DUMMY_COMMITMENT_HASH,
+47 -47
View File
@@ -6,16 +6,18 @@
//! Two zones (sequencer + indexer each, on separate channels) sharing one
//! Bedrock node, each producing and finalizing blocks independently.
use std::{net::SocketAddr, time::Duration};
use std::time::Duration;
use anyhow::{Context as _, Result};
use indexer_service_rpc::RpcClient as _;
use integration_tests::{
config::{self, SequencerPartialConfig},
indexer_client::IndexerClient,
setup::{SequencerSetup, setup_bedrock_node, setup_indexer},
};
use sequencer_service_rpc::{RpcClient as _, SequencerClientBuilder};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use test_fixtures::{
MultiZoneTestContextBuilder, ZoneTestContextBuilder, config::MultiNodeTestContextConfig,
};
use tokio::test;
const ZONE_LIVE_TIMEOUT: Duration = Duration::from_secs(360);
@@ -25,38 +27,45 @@ const MIN_BLOCK_ID: u64 = 2;
#[test]
async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> {
// Declared first so it outlives both zones (drops run in reverse order).
let (_bedrock, bedrock_addr) = setup_bedrock_node()
.await
.context("Failed to set up shared Bedrock node")?;
let partial = SequencerPartialConfig::default();
let channel_a = config::bedrock_channel_id();
let channel_b = config::bedrock_channel_id_b();
let partial = SequencerPartialConfig::default();
// Empty genesis is enough: the clock transaction drives block production.
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_a)
.with_genesis(vec![])
.setup()
.await
.context("Failed to set up zone A sequencer")?;
let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
.await
.context("Failed to set up zone A indexer")?;
let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
.with_channel_id(channel_b)
.with_genesis(vec![])
.setup()
.await
.context("Failed to set up zone B sequencer")?;
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None)
.await
.context("Failed to set up zone B indexer")?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_a,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 1,
bedrock_channel: channel_b,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.build()
.await?;
let ind_client_a = ctx.indexer_client_zone(channel_a).unwrap();
let ind_client_b = ctx.indexer_client_zone(channel_b).unwrap();
let seq_client_a = &ctx
.zone_default_sequencer_component(channel_a)
.sequencer_client;
let seq_client_b = &ctx
.zone_default_sequencer_component(channel_b)
.sequencer_client;
let (height_a, height_b) = tokio::try_join!(
wait_until_zone_live("A", seq_a.addr(), idx_a.addr()),
wait_until_zone_live("B", seq_b.addr(), idx_b.addr()),
wait_until_zone_live("A", seq_client_a, ind_client_a),
wait_until_zone_live("B", seq_client_b, ind_client_b),
)?;
assert!(
@@ -75,31 +84,22 @@ async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> {
/// to it. Returns the indexer's finalized block id.
async fn wait_until_zone_live(
label: &str,
sequencer_addr: SocketAddr,
indexer_addr: SocketAddr,
sequencer_client: &SequencerClient,
indexer_client: &IndexerClient,
) -> Result<u64> {
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_addr)
.context("Failed to build sequencer URL")?;
let sequencer = SequencerClientBuilder::default()
.build(sequencer_url)
.context("Failed to build sequencer client")?;
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_addr)
.context("Failed to build indexer URL")?;
let indexer = IndexerClient::new(&indexer_url)
.await
.context("Failed to build indexer client")?;
let wait = async {
loop {
if sequencer.get_last_block_id().await? >= MIN_BLOCK_ID {
if sequencer_client.get_last_block_id().await? >= MIN_BLOCK_ID {
break;
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
let target = sequencer.get_last_block_id().await?;
let target = sequencer_client.get_last_block_id().await?;
loop {
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
let finalized = indexer_client
.get_last_finalized_block_id()
.await?
.unwrap_or(0);
if finalized >= target {
log::info!(
"Zone {label} live: sequencer at {target}, indexer finalized {finalized}"
+75 -60
View File
@@ -26,8 +26,7 @@ use lee::{
Account, AccountId, PrivateKey, PublicKey,
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
};
use lee_core::program::DEFAULT_PROGRAM_ID;
use log::info;
use lee_core::program::DEFAULT_PROGRAM_OWNER;
use wallet::{account::HumanReadableAccount, program_facades::vault::Vault};
use wallet_ffi::{
FfiAccount, FfiAccountIdWithPrivacy, FfiAccountIdentity, FfiAccountList, FfiBytes32,
@@ -284,6 +283,12 @@ unsafe extern "C" {
) -> LabelList;
fn wallet_ffi_free_label_list(label_list: *mut LabelList) -> error::WalletFfiError;
fn wallet_ffi_poll_transaction_status(
handle: *mut WalletHandle,
tx_hash: FfiBytes32,
transaction_status: *mut bool,
) -> error::WalletFfiError;
}
fn new_wallet_ffi_with_test_context_config(
@@ -389,7 +394,7 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> {
#[test]
fn wallet_ffi_create_public_accounts() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let n_accounts = 10;
// Create `n_accounts` public accounts with wallet FFI
@@ -430,7 +435,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
#[test]
fn wallet_ffi_create_private_accounts() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let n_accounts = 10;
// Create `n_accounts` receiving keys with wallet FFI
let new_npks_ffi = unsafe {
@@ -465,7 +470,7 @@ fn wallet_ffi_create_private_accounts() -> Result<()> {
#[test]
fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
// Create a receiving key and save
let first_npk = unsafe {
@@ -505,7 +510,7 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
#[test]
fn test_wallet_ffi_list_accounts() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
// Create the wallet FFI and track which account IDs were created as public/private
let (wallet_ffi_handle, created_public_ids) = unsafe {
let home = tempfile::tempdir()?;
@@ -574,7 +579,7 @@ fn test_wallet_ffi_list_accounts() -> Result<()> {
#[test]
fn test_wallet_ffi_get_balance_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
@@ -596,7 +601,7 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> {
};
assert_eq!(balance, 10000);
info!("Successfully retrieved account balance");
log::info!("Successfully retrieved account balance");
unsafe {
wallet_ffi_destroy(wallet_ffi_handle);
@@ -607,7 +612,7 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> {
#[test]
fn test_wallet_ffi_get_account_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
@@ -629,7 +634,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> {
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
@@ -640,14 +645,14 @@ fn test_wallet_ffi_get_account_public() -> Result<()> {
wallet_ffi_destroy(wallet_ffi_handle);
}
info!("Successfully retrieved account with correct details");
log::info!("Successfully retrieved account with correct details");
Ok(())
}
#[test]
fn test_wallet_ffi_get_account_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let account_id: AccountId = ctx.ctx().existing_private_accounts()[0];
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
@@ -669,7 +674,7 @@ fn test_wallet_ffi_get_account_private() -> Result<()> {
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
assert_eq!(account.balance, 10000);
assert!(account.data.is_empty());
@@ -679,14 +684,14 @@ fn test_wallet_ffi_get_account_private() -> Result<()> {
wallet_ffi_destroy(wallet_ffi_handle);
}
info!("Successfully retrieved account with correct details");
log::info!("Successfully retrieved account with correct details");
Ok(())
}
#[test]
fn test_wallet_ffi_get_public_account_keys() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let account_id: AccountId = ctx.ctx().existing_public_accounts()[0];
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
@@ -717,7 +722,7 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> {
assert_eq!(key, expected_key);
info!("Successfully retrieved account key");
log::info!("Successfully retrieved account key");
unsafe {
wallet_ffi_destroy(wallet_ffi_handle);
@@ -728,7 +733,7 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> {
#[test]
fn test_wallet_ffi_get_private_account_keys() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let account_id: AccountId = ctx.ctx().existing_private_accounts()[0];
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
@@ -767,7 +772,7 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> {
wallet_ffi_destroy(wallet_ffi_handle);
}
info!("Successfully retrieved account keys");
log::info!("Successfully retrieved account keys");
Ok(())
}
@@ -814,7 +819,7 @@ fn wallet_ffi_base58_to_account_id() -> Result<()> {
#[test]
fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -838,7 +843,7 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
.unwrap();
(&out_account).try_into().unwrap()
};
assert_eq!(account.program_owner, DEFAULT_PROGRAM_ID);
assert_eq!(account.program_owner, DEFAULT_PROGRAM_OWNER);
// Call the init funciton
let mut transfer_result = FfiTransferResult::default();
@@ -851,7 +856,7 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// Check that the program owner is now the authenticated transfer program
@@ -867,7 +872,7 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
};
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
unsafe {
@@ -880,7 +885,7 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> {
#[test]
fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -904,7 +909,7 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -927,7 +932,7 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
};
assert_eq!(
account.program_owner,
programs::authenticated_transfer().id()
programs::authenticated_transfer().id().into()
);
unsafe {
@@ -940,7 +945,7 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -962,7 +967,7 @@ fn test_wallet_ffi_transfer_public() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let from_balance = unsafe {
@@ -987,8 +992,18 @@ fn test_wallet_ffi_transfer_public() -> Result<()> {
assert_eq!(from_balance, 9900);
assert_eq!(to_balance, 20100);
// Also check for transaction inclusion
let hash_bytes = unsafe { transfer_result.tx_hash_bytes() };
let mut is_included = false;
unsafe {
wallet_ffi_poll_transaction_status(wallet_ffi_handle, hash_bytes, &raw mut is_included)
.unwrap();
}
assert!(is_included);
unsafe {
wallet_ffi_free_transfer_result(&raw mut transfer_result);
wallet_ffi_destroy(wallet_ffi_handle);
}
@@ -997,7 +1012,7 @@ fn test_wallet_ffi_transfer_public() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_shielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1034,7 +1049,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1080,7 +1095,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1102,7 +1117,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
}
.unwrap();
info!("Waiting for next block creation");
log::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
@@ -1143,7 +1158,7 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1181,7 +1196,7 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1226,7 +1241,7 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
#[test]
fn restore_keys_from_seed_ffi() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1271,9 +1286,9 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut public_account_id_2).unwrap();
}
info!("Accounts created");
log::info!("Accounts created");
info!("Waiting for next block creation");
log::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
@@ -1305,7 +1320,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1333,7 +1348,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1357,7 +1372,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1381,7 +1396,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1398,11 +1413,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
wallet_ffi_free_transfer_result(&raw mut transfer_result_4);
}
info!("Preparation complete, performing keys restoration");
log::info!("Preparation complete, performing keys restoration");
let password = CString::new(ctx.ctx().wallet_password())?;
info!("Checking balance correctness before restoration");
log::info!("Checking balance correctness before restoration");
let private_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
@@ -1465,7 +1480,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap();
};
info!("Checking balance correctness after restoration");
log::info!("Checking balance correctness after restoration");
let private_account_id_1_balance = unsafe {
let mut out_balance: [u8; 16] = [0; 16];
@@ -1516,7 +1531,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
assert_eq!(public_account_id_1_balance, 102);
assert_eq!(public_account_id_2_balance, 103);
info!("Accounts restored");
log::info!("Accounts restored");
Ok(())
}
@@ -1546,7 +1561,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
// .unwrap();
// }
// info!("Waiting for next block creation");
// log::info!("Waiting for next block creation");
// std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
// let from_balance = unsafe {
@@ -1586,7 +1601,7 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1637,7 +1652,7 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let from_balance = unsafe {
@@ -1680,7 +1695,7 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
#[test]
fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1736,7 +1751,7 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
assert_eq!(transaction_result.secrets_size, 2);
info!("Waiting for next block creation");
log::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
@@ -1789,7 +1804,7 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> {
#[test]
fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1809,7 +1824,7 @@ fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
})
.unwrap();
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let vault_balance = unsafe {
@@ -1836,7 +1851,7 @@ fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let vault_balance_after_claim = unsafe {
@@ -1874,7 +1889,7 @@ fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> {
#[test]
fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1895,7 +1910,7 @@ fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
})
.unwrap();
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let vault_balance = unsafe {
@@ -1922,7 +1937,7 @@ fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
.unwrap();
}
info!("Waiting for next block creation");
log::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
@@ -1966,7 +1981,7 @@ fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
#[test]
fn test_wallet_ffi_single_label() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -1978,7 +1993,7 @@ fn test_wallet_ffi_single_label() -> Result<()> {
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id_1).unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let lab_1 = CString::from_str("LABEL1").unwrap().into_raw();
@@ -2015,7 +2030,7 @@ fn test_wallet_ffi_single_label() -> Result<()> {
#[test]
fn test_wallet_ffi_more_labels() -> Result<()> {
let ctx = BlockingTestContext::new()?;
let ctx = BlockingTestContext::new_default()?;
let home = tempfile::tempdir()?;
let FfiCreateWalletOutput {
wallet: wallet_ffi_handle,
@@ -2027,7 +2042,7 @@ fn test_wallet_ffi_more_labels() -> Result<()> {
wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id_1).unwrap();
}
info!("Waiting for next block creation");
log::info!("Waiting for next block creation");
std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS));
let lab_1 = CString::from_str("LABEL1").unwrap().into_raw();
@@ -339,7 +339,7 @@ mod tests {
}
/// Pins the end-to-end derivation for a fixed (GMS, `ProgramId`, `PdaSeed`). Any change
/// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` nsk/npk chain, or the
/// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` ask/nsk/npk chain, or the
/// `AccountId::for_private_pda` formula breaks this test. Mirrors the pinned-value
/// pattern from `for_private_pda_matches_pinned_value` in `lee_core`.
#[test]
@@ -357,8 +357,8 @@ mod tests {
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
let expected_npk = NullifierPublicKey([
136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112,
59, 136, 7, 185, 56, 46, 38, 4, 195, 155, 85, 32, 161, 24, 119, 14, 148, 100, 26, 152,
239, 255, 145, 142, 122, 166, 219, 75, 200, 9, 168, 7,
]);
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
// We verify npk is pinned, and AccountId is deterministically derived from it.
@@ -1,6 +1,8 @@
use std::collections::BTreeMap;
use lee_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey};
use lee_core::{
NullifierPublicKey, NullifierSecretKey, PrivateAccountKind, encryption::ViewingPublicKey,
};
use serde::{Deserialize, Serialize};
use sha2::Digest as _;
@@ -22,7 +24,7 @@ pub struct ChildKeysPrivate {
impl ChildKeysPrivate {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, b"LEE_master_priv");
let hash_value = hmac_sha512::HMAC::mac(seed, b"/LEE-Keys/v1/Master/Private");
let (first, ccc) = split_hash(&hash_value);
let ssk = SecretSpendingKey(first);
@@ -32,11 +34,13 @@ impl ChildKeysPrivate {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
const DOMAIN: &[u8; 27] = b"/LEE-Keys/v1/Parent/Private";
// `parent_hash`` is used to incorporate entropy based on the parent node's keys
// to generate the `ssk` and `ccc` values.
let mut parent_hash = sha2::Sha256::new();
parent_hash.update(b"LEE/keys");
parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key);
parent_hash.update(DOMAIN);
parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key());
parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d);
parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.z);
let parent_pt = parent_hash.finalize();
@@ -44,7 +48,7 @@ impl ChildKeysPrivate {
// Each child (of the same parent node) share the same `parent_pt`.
// To ensure that each child generates unique keys, we include the child index.
let mut input = vec![];
input.extend_from_slice(b"LEE_seed_priv");
input.extend_from_slice(b"/LEE-Keys/v1/Seed/Private");
input.extend_from_slice(&parent_pt);
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
input.extend_from_slice(&cci.to_be_bytes());
@@ -58,10 +62,10 @@ impl ChildKeysPrivate {
}
fn from_ssk_and_ccc(ssk: SecretSpendingKey, ccc: [u8; 32], cci: Option<u32>) -> Self {
let nsk = ssk.generate_nullifier_secret_key(cci);
let ask = ssk.generate_authorization_secret_key(cci);
let vsk = ssk.generate_viewing_secret_seed_key(cci);
let npk = NullifierPublicKey::from(&nsk);
let npk = NullifierPublicKey::from(&NullifierSecretKey::from(&ask));
let vpk = ViewingPublicKey::from(&vsk);
Self {
@@ -71,7 +75,7 @@ impl ChildKeysPrivate {
nullifier_public_key: npk,
viewing_public_key: vpk,
private_key_holder: PrivateKeyHolder {
nullifier_secret_key: nsk,
authorization_secret_key: ask,
viewing_secret_key: vsk,
},
},
@@ -121,104 +125,112 @@ mod tests {
let keys = ChildKeysPrivate::root(SEED);
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139,
222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191,
89, 83, 219, 205, 176, 131, 82, 74, 181, 52, 131, 240, 254, 155, 208, 126, 153, 157,
15, 191, 143, 255, 97, 11, 215, 173, 222, 16, 183, 7, 202, 121,
]);
let expected_ccc = [
56, 114, 70, 249, 67, 169, 206, 9, 192, 11, 180, 168, 149, 129, 42, 95, 43, 157, 130,
111, 13, 5, 195, 75, 20, 255, 162, 85, 40, 251, 8, 168,
186, 27, 116, 2, 28, 54, 110, 48, 7, 169, 72, 98, 9, 24, 12, 67, 109, 226, 251, 203,
24, 253, 128, 147, 135, 32, 56, 255, 46, 230, 70, 10,
];
let expected_ask = lee_core::AuthorizationSecretKey([
101, 181, 188, 132, 20, 141, 124, 205, 63, 12, 23, 238, 2, 74, 87, 224, 188, 155, 99,
63, 14, 60, 27, 126, 47, 106, 97, 114, 103, 158, 103, 60,
]);
let expected_nsk: NullifierSecretKey = [
154, 102, 103, 5, 34, 235, 227, 13, 22, 182, 226, 11, 7, 67, 110, 162, 99, 193, 174,
34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163,
46, 160, 51, 114, 77, 129, 41, 210, 140, 244, 36, 67, 95, 17, 96, 147, 113, 144, 67,
94, 198, 153, 220, 206, 59, 61, 18, 52, 63, 0, 51, 102,
];
let expected_npk = lee_core::NullifierPublicKey([
7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97,
247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2,
31, 162, 180, 178, 183, 165, 42, 194, 236, 199, 174, 146, 241, 104, 255, 171, 249, 138,
18, 132, 96, 10, 57, 210, 159, 169, 66, 238, 104, 132, 226, 13,
]);
let expected_vsk = ViewingSecretKey::new(
[
187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194,
159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36,
239, 105, 105, 155, 13, 89, 203, 100, 162, 108, 87, 128, 65, 88, 101, 217, 214,
126, 200, 51, 35, 146, 251, 207, 76, 44, 218, 2, 51, 113, 171, 167,
],
[
80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19,
223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82,
107, 52, 23, 158, 102, 144, 107, 47, 137, 52, 56, 143, 52, 225, 107, 228, 152, 23,
24, 172, 164, 21, 92, 89, 87, 175, 51, 190, 34, 219, 52, 47,
],
);
// Length matches MlKem768EncapsulationKey::LEN.
// Length matches MlKem768EncapsulationKey::LEN. Oracle-sourced from the ML-KEM-768
// implementation, unlike every other vector here; its trailing 32 bytes are
// rho = SHA3-512(d || 3)[..32] per FIPS-203, checked against the independent `d`.
let expected_vpk: [u8; 1184] = [
127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220,
150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181,
118, 237, 25, 129, 51, 160, 189, 51, 99, 184, 57, 28, 121, 240, 236, 2, 170, 198, 26,
91, 172, 110, 52, 32, 186, 35, 179, 202, 234, 249, 15, 242, 100, 198, 168, 163, 120,
205, 118, 85, 195, 210, 187, 95, 150, 154, 8, 68, 165, 237, 87, 166, 101, 57, 4, 18,
11, 122, 235, 180, 199, 154, 165, 158, 55, 136, 30, 237, 43, 167, 215, 68, 80, 102, 0,
71, 90, 130, 206, 240, 215, 69, 199, 83, 7, 60, 184, 128, 230, 184, 61, 93, 201, 204,
165, 104, 9, 127, 220, 52, 246, 217, 131, 251, 2, 170, 133, 6, 51, 40, 224, 101, 61,
16, 135, 32, 182, 201, 68, 58, 171, 54, 161, 184, 243, 38, 106, 200, 251, 17, 172, 8,
24, 73, 230, 55, 85, 20, 147, 222, 165, 200, 116, 135, 47, 20, 227, 56, 220, 64, 120,
215, 245, 58, 86, 102, 149, 252, 193, 163, 160, 59, 82, 138, 249, 171, 1, 54, 199, 193,
171, 85, 38, 64, 56, 121, 106, 84, 57, 252, 94, 147, 16, 191, 196, 104, 47, 129, 84,
21, 252, 160, 81, 207, 184, 199, 3, 177, 74, 117, 115, 175, 138, 108, 36, 198, 5, 32,
15, 218, 3, 20, 19, 15, 251, 209, 86, 128, 139, 148, 78, 10, 34, 144, 149, 74, 102, 48,
59, 70, 124, 47, 193, 100, 26, 9, 104, 178, 102, 156, 199, 242, 101, 147, 161, 87, 27,
234, 192, 204, 41, 36, 43, 83, 219, 15, 211, 66, 91, 76, 73, 13, 113, 155, 203, 193,
160, 130, 84, 103, 47, 70, 100, 147, 169, 65, 119, 84, 121, 122, 161, 76, 203, 144,
248, 145, 22, 8, 46, 121, 44, 77, 20, 149, 66, 179, 56, 149, 231, 98, 184, 9, 64, 14,
67, 196, 34, 8, 123, 21, 80, 169, 168, 223, 230, 133, 0, 66, 159, 230, 69, 201, 205,
169, 105, 196, 21, 71, 84, 70, 58, 165, 165, 134, 186, 232, 60, 70, 51, 57, 239, 74,
174, 116, 234, 36, 178, 49, 42, 168, 250, 104, 141, 106, 0, 109, 52, 86, 104, 243, 62,
214, 137, 48, 107, 2, 152, 206, 227, 175, 147, 236, 19, 113, 27, 191, 231, 235, 167,
114, 104, 23, 126, 203, 94, 242, 149, 171, 115, 170, 89, 244, 58, 29, 176, 73, 203, 44,
8, 32, 9, 226, 32, 78, 246, 38, 235, 149, 133, 25, 243, 47, 124, 180, 200, 211, 165,
137, 56, 169, 117, 31, 244, 65, 91, 135, 146, 158, 20, 75, 102, 32, 65, 250, 103, 199,
36, 48, 31, 155, 164, 191, 222, 85, 37, 66, 243, 17, 120, 104, 0, 228, 83, 200, 116, 6,
199, 106, 236, 139, 246, 216, 152, 241, 211, 85, 106, 200, 44, 231, 240, 66, 3, 193,
147, 16, 145, 65, 49, 33, 53, 247, 69, 47, 44, 113, 86, 117, 6, 20, 193, 183, 128, 178,
181, 21, 251, 99, 39, 149, 210, 146, 106, 181, 186, 7, 36, 63, 186, 234, 191, 164, 193,
162, 127, 250, 122, 189, 219, 21, 92, 48, 86, 209, 184, 99, 160, 201, 162, 145, 20,
138, 154, 18, 37, 180, 209, 165, 165, 51, 187, 78, 193, 175, 135, 6, 55, 216, 178, 10,
40, 246, 98, 128, 80, 14, 38, 69, 113, 123, 54, 94, 43, 50, 106, 167, 17, 77, 163, 148,
117, 225, 9, 7, 253, 240, 157, 96, 103, 33, 100, 37, 37, 20, 53, 138, 234, 55, 45, 232,
154, 9, 150, 192, 116, 36, 119, 106, 95, 119, 34, 220, 84, 174, 19, 227, 33, 209, 96,
197, 148, 230, 197, 59, 117, 130, 7, 116, 11, 0, 197, 16, 249, 151, 31, 4, 64, 29, 165,
247, 110, 176, 166, 4, 112, 136, 101, 208, 7, 179, 38, 183, 134, 58, 107, 207, 160, 38,
159, 67, 112, 20, 225, 199, 179, 133, 117, 144, 54, 199, 15, 204, 80, 154, 116, 84, 88,
109, 113, 5, 207, 226, 21, 62, 247, 122, 14, 156, 9, 8, 76, 26, 148, 67, 196, 128, 176,
78, 51, 161, 151, 75, 248, 154, 31, 168, 9, 4, 3, 107, 222, 245, 178, 21, 84, 7, 25,
155, 118, 97, 135, 63, 89, 233, 11, 207, 148, 155, 38, 106, 104, 102, 140, 104, 67,
149, 20, 30, 196, 44, 197, 128, 34, 182, 80, 30, 32, 137, 34, 212, 164, 177, 164, 12,
115, 41, 156, 111, 71, 230, 120, 111, 218, 25, 117, 218, 75, 167, 32, 37, 57, 50, 99,
181, 203, 40, 105, 248, 150, 114, 121, 73, 127, 198, 191, 161, 44, 56, 213, 243, 71, 2,
56, 192, 243, 107, 179, 27, 96, 21, 116, 169, 64, 15, 97, 166, 151, 200, 11, 40, 204,
71, 168, 220, 9, 55, 43, 146, 244, 212, 166, 192, 180, 189, 237, 162, 42, 29, 33, 52,
193, 4, 178, 157, 244, 28, 209, 44, 26, 36, 147, 126, 94, 164, 37, 47, 115, 38, 23,
165, 96, 106, 140, 42, 69, 146, 194, 93, 71, 175, 49, 147, 32, 246, 97, 94, 41, 116,
127, 174, 18, 16, 14, 163, 17, 180, 213, 203, 166, 33, 139, 214, 18, 170, 27, 41, 59,
175, 200, 101, 14, 128, 45, 179, 167, 136, 232, 138, 56, 124, 145, 75, 233, 132, 161,
196, 164, 72, 80, 60, 187, 38, 90, 90, 17, 66, 134, 59, 2, 165, 29, 76, 24, 38, 211,
177, 83, 119, 20, 239, 59, 77, 34, 3, 42, 47, 60, 89, 46, 103, 168, 120, 17, 199, 50,
17, 103, 107, 48, 8, 53, 220, 159, 212, 65, 198, 80, 8, 11, 235, 97, 203, 196, 240, 44,
56, 121, 77, 91, 196, 160, 129, 242, 149, 226, 57, 106, 180, 76, 161, 203, 18, 37, 166,
153, 44, 40, 28, 74, 8, 11, 6, 166, 54, 10, 103, 247, 23, 35, 7, 47, 173, 133, 71, 85,
3, 168, 250, 120, 126, 174, 37, 80, 128, 107, 7, 161, 130, 155, 136, 92, 48, 215, 119,
196, 124, 85, 157, 234, 2, 166, 137, 65, 121, 222, 112, 47, 17, 43, 23, 111, 88, 5,
195, 41, 8, 191, 227, 21, 173, 35, 199, 196, 188, 162, 191, 195, 204, 137, 54, 16, 73,
178, 150, 249, 234, 22, 216, 123, 157, 144, 218, 118, 53, 193, 67, 65, 84, 162, 244,
165, 24, 110, 246, 146, 228, 212, 180, 150, 116, 201, 37, 128, 76, 41, 188, 42, 79,
148, 52, 196, 176, 178, 224, 48, 168, 13, 129, 193, 131, 185, 131, 93, 40, 145, 56,
180, 29, 153, 83, 39, 69, 232, 96, 238, 137, 104, 150, 2, 202, 239, 149, 248, 154, 115,
115, 127, 3, 8, 32, 61, 96, 66, 25, 181, 14, 72, 73, 97, 186, 134, 140, 33, 69, 33, 74,
9, 112, 59, 246, 41, 55, 116, 67, 166, 192, 83, 50, 213, 177, 26, 20, 243, 125, 9, 35,
156, 82, 73, 55, 138, 66, 88, 141, 179, 140, 141, 50, 149, 79, 181, 43, 174, 200, 31,
111, 9, 154, 216, 90, 4, 89, 136, 130, 6, 139, 36, 142, 27, 168, 149, 210, 2, 105, 25,
39, 94, 244, 7, 242, 108, 19, 64, 72, 25, 237, 105, 3, 215, 75, 21, 211, 196, 58, 39,
116, 34, 24, 215, 25, 152, 56, 62, 70, 136, 98, 188, 21, 138, 18, 214, 75, 140, 185,
77, 4, 192, 71, 202, 76, 158, 212, 161, 37, 48, 171, 156, 254, 192, 68, 19, 89, 83, 83,
131, 194, 135, 250, 59, 239, 40, 22, 15, 211, 140, 112, 7, 154, 36, 196, 152, 144, 72,
64, 117, 249, 134, 46, 21, 36, 101, 242, 103, 182, 226, 93, 207, 243, 0, 214, 3, 117,
129, 65, 45, 181, 1, 44, 82, 71, 145, 188, 128, 21, 205, 56, 125, 31, 64, 16, 245, 247,
55, 10, 32, 112, 67, 164, 57, 63, 124, 10, 111, 251, 15, 21, 23, 132, 206, 122, 29, 16,
137, 206, 109, 193, 192, 132, 98, 140, 125, 41, 22, 41, 7, 208, 33, 160, 121, 143, 0,
24, 224, 149, 107, 141, 196, 129, 187, 181, 23, 21, 86, 62, 160, 185, 134, 185, 151,
165, 11, 72, 118, 163, 49, 182, 67, 0, 142, 101, 42, 2, 99, 102, 200, 92, 105, 53, 53,
144, 9, 84, 32, 119, 14, 12, 129, 26, 247, 173, 118, 193, 24, 252, 131, 99, 22, 97,
207, 213, 75, 199, 238, 197, 143, 153, 60, 13, 156, 236, 11, 218, 171, 106, 58, 52, 99,
236, 149, 153, 249, 147, 49, 179, 57, 20, 100, 38, 78, 62, 235, 110, 113, 72, 77, 177,
225, 51, 114, 172, 28, 150, 246, 190, 164, 43, 60, 3, 225, 85, 160, 38, 205, 189, 163,
72, 231, 56, 111, 2, 163, 74, 114, 218, 10, 45, 181, 35, 59, 119, 124, 4, 92, 45, 44,
53, 154, 122, 161, 65, 244, 130, 78, 83, 9, 198, 204, 134, 11, 214, 75, 156, 178, 151,
19, 212, 10, 140, 133, 128, 45, 93, 178, 121, 169, 108, 170, 134, 200, 0, 215, 172, 43,
111, 105, 182, 82, 235, 132, 236, 7, 37, 239, 235, 142, 173, 246, 150, 61, 8, 140, 159,
41, 166, 23, 218, 153, 65, 151, 91, 137, 132, 180, 141, 163, 113, 16, 209, 201, 63, 65,
121, 236, 66, 47, 74, 177, 74, 80, 117, 34, 44, 27, 22, 238, 154, 140, 90, 12, 50, 47,
5, 64, 121, 227, 36, 156, 233, 19, 85, 89, 1, 96, 137, 77, 187, 235, 124, 226, 3, 6,
219, 70, 126, 89, 247, 70, 85, 208, 200, 99, 37, 89, 18, 249, 128, 233, 245, 123, 18,
49, 32, 227, 163, 20, 184, 131, 203, 153, 4, 60, 188, 243, 157, 172, 120, 176, 72, 210,
146, 177, 54, 4, 125, 33, 82, 99, 184, 93, 186, 106, 54, 57, 156, 27, 96, 177, 89, 81,
12, 11, 83, 161, 153, 253, 160, 132, 28, 192, 34, 221, 184, 46, 51, 171, 203, 36, 121,
83, 229, 80, 39, 168, 115, 36, 126, 86, 153, 228, 115, 137, 127, 162, 189, 222, 200,
67, 231, 38, 3, 186, 66, 62, 69, 26, 166, 9, 99, 149, 134, 178, 0, 87, 120, 150, 11,
216, 124, 191, 136, 129, 130, 115, 101, 143, 195, 134, 79, 118, 197, 177, 165, 179,
117, 19, 50, 215, 51, 171, 123, 236, 193, 172, 24, 9, 5, 74, 28, 237, 43, 187, 114, 90,
187, 41, 188, 204, 64, 19, 147, 114, 0, 23, 16, 53, 89, 32, 36, 184, 145, 165, 6, 57,
134, 80, 102, 66, 59, 16, 60, 43, 132, 101, 73, 84, 161, 11, 192, 229, 163, 160, 117,
26, 11, 113, 50, 51, 98, 34, 220, 98, 143, 102, 105, 97, 156, 241, 34, 138, 136, 160,
129, 216, 89, 171, 42, 92, 178, 90, 5, 168, 84, 189, 223, 52, 33, 218, 203, 27, 89,
188, 184, 138, 172, 34, 166, 171, 156, 180, 161, 130, 94, 252, 166, 148, 114, 39, 138,
156, 66, 222, 146, 171, 40, 118, 29, 14, 5, 147, 53, 226, 24, 85, 1, 68, 28, 98, 99,
236, 65, 91, 131, 145, 152, 159, 198, 202, 144, 6, 125, 93, 247, 26, 149, 51, 159, 164,
100, 142, 239, 66, 105, 106, 155, 177, 96, 216, 160, 20, 198, 10, 186, 113, 83, 85, 51,
7, 246, 227, 69, 80, 22, 38, 129, 198, 7, 66, 130, 95, 32, 224, 110, 62, 231, 70, 123,
55, 175, 115, 219, 199, 234, 43, 73, 93, 198, 188, 196, 231, 76, 72, 193, 179, 47, 153,
182, 203, 193, 116, 70, 37, 57, 198, 67, 82, 24, 119, 60, 68, 16, 94, 206, 178, 173,
135, 89, 90, 238, 245, 30, 112, 59, 76, 110, 217, 188, 135, 68, 155, 184, 16, 27, 168,
203, 44, 101, 96, 164, 184, 48, 24, 84, 56, 169, 44, 179, 19, 6, 66, 143, 3, 71, 128,
181, 244, 43, 70, 67, 168, 17, 58, 184, 74, 35, 167, 50, 198, 180, 205, 85, 126, 11,
151, 116, 238, 21, 83, 146, 34, 69, 233, 32, 87, 117, 107, 28, 116, 102, 62, 74, 26,
80, 38, 201, 44, 145, 248, 129, 136, 243, 177, 191, 215, 107, 93, 56, 99, 35, 252, 183,
220, 23, 47, 173, 7, 14, 238, 5, 159, 88, 242, 101, 223, 68, 112, 158, 40, 148, 198,
26, 27, 128, 231, 177, 17, 101, 191, 12, 103, 91, 62, 216, 112, 75, 188, 64, 65, 218,
29, 122, 177, 204, 50, 66, 123, 84, 91, 15, 238, 220, 41, 24, 17, 36, 169, 231, 152,
245, 59, 25, 35, 49, 141, 205, 67, 5, 102, 162, 146, 41, 89, 49, 215, 58, 35, 106, 7,
110, 209, 231, 123, 2, 27, 170, 25, 1, 163, 200, 120, 142, 73, 202, 16, 156, 162, 133,
182, 186, 171, 153, 181, 179, 230, 225, 202, 184, 149, 73, 210, 51, 201, 134, 227, 80,
51, 108, 103, 171, 201, 107, 237, 18, 197, 4, 167, 121, 210, 53, 156, 245, 120, 117,
174, 167, 83, 220, 73, 125, 3, 179, 87, 70, 25, 59, 182, 103, 164, 134, 193, 51, 16,
137, 23, 27, 194, 18, 98, 112, 93, 18, 220, 137, 37, 97, 123, 161, 232, 157, 231, 44,
147, 11, 179, 89, 88, 4, 45, 192, 36, 124, 12, 203, 84, 109, 164, 19, 151, 183, 145,
108, 233, 122, 115, 227, 104, 186, 128, 38, 185, 87, 89, 220, 198, 174, 160, 165, 152,
40, 236, 0, 167, 106, 175, 244, 4, 55, 213, 18, 133, 255, 153, 9, 188, 247, 38, 225,
140, 52, 45, 186, 134, 255, 10, 138, 132, 237, 79, 72, 133, 108, 147, 85, 247, 55, 61,
252, 187, 135, 75, 170, 63, 63, 239, 196, 99, 147, 72, 117, 111, 126,
];
assert!(expected_ssk == keys.value.0.secret_spending_key);
assert!(expected_ccc == keys.ccc);
assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key);
assert!(expected_ask == keys.value.0.private_key_holder.authorization_secret_key);
assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key());
assert!(expected_npk == keys.value.0.nullifier_public_key);
assert!(expected_vsk == keys.value.0.private_key_holder.viewing_secret_key);
assert!(expected_vpk == keys.value.0.viewing_public_key.to_bytes());
@@ -230,105 +242,119 @@ mod tests {
let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32);
let expected_ssk = key_management::secret_holders::SecretSpendingKey([
151, 183, 113, 151, 215, 187, 207, 64, 197, 182, 207, 32, 5, 49, 180, 98, 119, 14, 248,
175, 39, 100, 47, 109, 148, 173, 217, 253, 159, 234, 209, 113,
255, 91, 0, 255, 146, 9, 219, 224, 232, 0, 145, 114, 101, 34, 207, 151, 203, 96, 120,
175, 228, 160, 182, 56, 113, 198, 151, 158, 223, 183, 121, 111,
]);
let expected_ccc = [
138, 243, 142, 163, 62, 107, 63, 131, 230, 158, 185, 60, 204, 50, 243, 222, 13, 123,
98, 116, 131, 194, 7, 25, 129, 209, 163, 72, 178, 143, 192, 240,
149, 233, 214, 67, 128, 214, 160, 223, 4, 243, 147, 157, 59, 9, 140, 26, 30, 60, 246,
175, 98, 220, 192, 241, 47, 25, 197, 150, 25, 192, 2, 229,
];
let expected_ask = lee_core::AuthorizationSecretKey([
181, 178, 105, 190, 28, 96, 174, 62, 65, 157, 130, 133, 57, 64, 9, 248, 208, 87, 211,
185, 124, 194, 181, 148, 243, 25, 63, 35, 38, 65, 48, 74,
]);
let expected_nsk: NullifierSecretKey = [
196, 33, 11, 39, 220, 84, 119, 182, 187, 194, 135, 20, 124, 33, 244, 205, 96, 58, 102,
52, 74, 67, 110, 213, 24, 16, 160, 64, 247, 3, 107, 235,
9, 95, 3, 185, 154, 73, 180, 31, 241, 193, 141, 13, 247, 72, 210, 198, 29, 39, 33, 149,
238, 157, 50, 138, 162, 227, 70, 164, 93, 169, 83, 231,
];
let expected_npk = lee_core::NullifierPublicKey([
247, 253, 217, 86, 157, 208, 39, 172, 59, 190, 88, 165, 7, 173, 183, 106, 172, 211, 4,
180, 51, 107, 177, 107, 51, 117, 231, 176, 200, 103, 1, 121,
220, 138, 42, 117, 37, 161, 212, 202, 29, 17, 97, 186, 118, 172, 21, 159, 163, 147, 39,
65, 114, 231, 255, 39, 207, 253, 76, 171, 57, 141, 34, 11,
]);
let expected_vsk = ViewingSecretKey::new(
[
185, 209, 179, 92, 7, 131, 98, 121, 215, 46, 154, 56, 238, 106, 162, 225, 83, 82,
134, 3, 80, 186, 35, 178, 161, 204, 205, 163, 28, 19, 149, 18,
122, 167, 237, 237, 86, 162, 1, 146, 79, 10, 160, 65, 61, 77, 8, 65, 241, 247, 144,
153, 109, 134, 20, 7, 229, 52, 249, 30, 32, 109, 23, 112,
],
[
174, 24, 72, 205, 129, 123, 131, 9, 146, 152, 224, 151, 10, 184, 224, 109, 94, 149,
117, 60, 26, 10, 212, 125, 113, 147, 87, 67, 73, 26, 101, 193,
112, 168, 129, 117, 127, 160, 73, 110, 21, 164, 82, 213, 253, 135, 11, 26, 213, 53,
120, 199, 254, 143, 28, 169, 170, 187, 92, 12, 43, 217, 129, 58,
],
);
// Length matches MlKem768EncapsulationKey::LEN.
// Length matches MlKem768EncapsulationKey::LEN. Oracle-sourced from the ML-KEM-768
// implementation, unlike every other vector here; its trailing 32 bytes are
// rho = SHA3-512(d || 3)[..32] per FIPS-203, checked against the independent `d`.
let expected_vpk: [u8; 1184] = [
215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84,
232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57,
71, 11, 204, 156, 130, 30, 170, 199, 201, 25, 2, 21, 34, 155, 136, 124, 145, 223, 128,
177, 207, 92, 38, 252, 165, 118, 61, 128, 71, 154, 242, 105, 165, 52, 7, 6, 244, 120,
227, 134, 191, 25, 169, 150, 123, 246, 138, 25, 196, 126, 156, 144, 33, 123, 120, 44,
142, 89, 201, 49, 219, 205, 87, 236, 110, 64, 129, 102, 100, 155, 26, 101, 121, 42,
236, 82, 111, 141, 117, 75, 71, 194, 73, 123, 170, 110, 69, 149, 107, 96, 195, 55, 122,
140, 131, 106, 140, 156, 147, 75, 28, 128, 138, 113, 86, 37, 63, 173, 214, 200, 2, 214,
84, 234, 176, 120, 252, 184, 99, 192, 65, 112, 150, 99, 26, 174, 187, 183, 187, 64, 90,
248, 100, 66, 63, 195, 3, 44, 43, 128, 59, 149, 107, 66, 180, 67, 200, 183, 200, 36,
91, 7, 65, 228, 159, 79, 44, 89, 35, 163, 145, 92, 227, 104, 2, 72, 5, 7, 193, 21, 51,
116, 198, 184, 6, 192, 188, 68, 183, 163, 193, 142, 244, 217, 155, 197, 187, 189, 174,
225, 45, 126, 112, 93, 194, 156, 102, 150, 1, 188, 222, 76, 108, 73, 149, 44, 28, 219,
66, 95, 215, 204, 148, 217, 16, 36, 121, 112, 2, 51, 10, 195, 137, 12, 93, 203, 146,
138, 211, 15, 201, 42, 72, 146, 186, 160, 222, 235, 127, 83, 48, 182, 49, 248, 29, 138,
16, 32, 232, 179, 163, 187, 161, 174, 152, 187, 93, 76, 166, 48, 230, 219, 111, 123,
181, 103, 130, 28, 109, 235, 115, 45, 57, 193, 206, 160, 17, 52, 92, 194, 25, 3, 80,
97, 142, 249, 151, 94, 250, 95, 12, 57, 11, 165, 92, 47, 85, 182, 48, 22, 60, 97, 244,
59, 194, 135, 180, 133, 106, 227, 56, 192, 60, 91, 15, 241, 146, 89, 240, 130, 219,
202, 187, 43, 85, 98, 50, 104, 64, 114, 113, 80, 54, 69, 69, 5, 43, 90, 19, 0, 0, 188,
251, 184, 70, 160, 18, 117, 76, 53, 209, 166, 96, 34, 224, 137, 115, 183, 168, 243, 19,
1, 255, 4, 97, 162, 199, 104, 72, 213, 111, 62, 54, 172, 82, 184, 82, 143, 71, 99, 25,
104, 74, 120, 70, 84, 235, 32, 22, 20, 218, 163, 77, 194, 125, 75, 22, 72, 236, 192,
200, 107, 91, 156, 201, 10, 178, 87, 19, 181, 211, 91, 17, 145, 200, 17, 179, 65, 75,
200, 186, 89, 144, 91, 184, 116, 214, 51, 91, 42, 162, 243, 202, 92, 18, 54, 0, 213,
67, 149, 151, 51, 29, 220, 196, 160, 201, 68, 113, 210, 164, 175, 152, 121, 168, 231,
161, 91, 132, 218, 1, 171, 176, 84, 100, 57, 1, 3, 2, 196, 194, 76, 181, 79, 171, 157,
35, 162, 155, 192, 210, 149, 142, 120, 189, 127, 151, 96, 202, 225, 73, 242, 81, 112,
237, 224, 155, 130, 130, 34, 196, 153, 131, 161, 113, 163, 172, 114, 48, 207, 32, 151,
172, 83, 145, 79, 210, 100, 161, 92, 82, 216, 90, 104, 238, 212, 38, 50, 107, 17, 228,
195, 190, 6, 151, 165, 148, 245, 102, 51, 8, 185, 8, 85, 59, 247, 219, 95, 219, 170,
155, 233, 123, 27, 64, 251, 56, 24, 200, 16, 181, 212, 146, 61, 116, 106, 215, 214, 62,
118, 27, 68, 233, 148, 73, 135, 199, 74, 184, 89, 159, 217, 139, 24, 208, 250, 30, 224,
97, 185, 237, 193, 8, 216, 23, 186, 5, 50, 41, 161, 203, 22, 217, 23, 194, 191, 148,
124, 10, 212, 171, 209, 210, 145, 184, 171, 74, 35, 220, 43, 145, 241, 23, 43, 92, 171,
216, 43, 114, 77, 155, 147, 156, 86, 56, 170, 27, 1, 54, 182, 169, 96, 22, 201, 51,
145, 94, 143, 133, 106, 47, 176, 112, 197, 197, 96, 80, 73, 164, 207, 179, 22, 229,
171, 201, 223, 219, 13, 219, 1, 91, 224, 252, 171, 199, 217, 25, 60, 128, 135, 9, 71,
105, 231, 86, 34, 21, 155, 50, 0, 105, 72, 117, 108, 175, 140, 9, 181, 249, 139, 97, 3,
161, 66, 248, 42, 67, 113, 132, 8, 119, 232, 6, 169, 18, 157, 222, 53, 176, 56, 137,
120, 18, 115, 199, 187, 112, 48, 223, 211, 206, 152, 252, 108, 179, 129, 20, 227, 248,
183, 234, 87, 202, 49, 17, 69, 215, 118, 89, 188, 180, 33, 238, 245, 206, 40, 179, 129,
242, 59, 73, 254, 117, 114, 250, 179, 103, 109, 250, 202, 99, 152, 2, 167, 130, 169,
35, 71, 89, 211, 140, 71, 103, 154, 121, 108, 147, 191, 186, 73, 10, 73, 203, 23, 55,
106, 144, 98, 227, 157, 25, 27, 81, 67, 11, 57, 88, 227, 116, 61, 100, 94, 23, 166,
146, 57, 226, 72, 124, 33, 65, 226, 35, 167, 206, 156, 202, 213, 213, 158, 89, 249,
181, 19, 113, 109, 217, 71, 168, 142, 180, 122, 30, 5, 54, 170, 155, 73, 56, 170, 124,
139, 4, 165, 103, 82, 32, 183, 84, 7, 239, 117, 135, 239, 48, 24, 28, 210, 49, 137, 6,
158, 65, 211, 113, 205, 135, 146, 83, 10, 46, 90, 27, 97, 135, 135, 185, 173, 69, 58,
34, 247, 141, 150, 6, 158, 117, 23, 198, 139, 65, 81, 179, 187, 194, 247, 203, 127,
106, 232, 119, 122, 215, 197, 110, 69, 203, 174, 227, 63, 185, 106, 14, 184, 104, 113,
233, 83, 92, 104, 38, 188, 9, 135, 107, 108, 121, 193, 33, 209, 89, 39, 137, 17, 208,
26, 21, 238, 169, 86, 181, 193, 153, 82, 8, 151, 53, 39, 88, 91, 252, 3, 33, 75, 127,
9, 168, 53, 34, 1, 173, 202, 123, 157, 174, 170, 199, 254, 187, 196, 144, 37, 29, 48,
112, 173, 107, 147, 155, 69, 134, 137, 156, 247, 123, 242, 72, 5, 43, 106, 89, 179,
204, 41, 15, 60, 48, 78, 214, 180, 26, 170, 67, 71, 66, 146, 113, 220, 159, 153, 201,
176, 116, 154, 21, 186, 33, 180, 72, 39, 187, 240, 80, 112, 132, 144, 173, 210, 12, 76,
184, 146, 89, 178, 178, 82, 109, 71, 201, 241, 160, 207, 219, 124, 77, 2, 105, 124,
178, 71, 3, 38, 64, 41, 83, 170, 137, 82, 242, 144, 76, 102, 82, 7, 25, 149, 141, 169,
46, 4, 68, 40, 244, 146, 131, 107, 148, 18, 111, 85, 104, 243, 28, 75, 176, 249, 88,
82, 123, 89, 29, 104, 135, 230, 117, 67, 26, 249, 108, 145, 76, 38, 175, 89, 185, 94,
106, 128, 201, 150, 151, 194, 133, 21, 81, 213, 231, 15, 117, 44, 61, 86, 223, 162, 56,
190, 166, 177, 157, 137, 60, 208, 155, 234, 158, 252, 30,
65, 58, 16, 29, 18, 146, 252, 162, 117, 98, 133, 1, 86, 200, 127, 2, 35, 59, 88, 227,
203, 23, 97, 155, 214, 138, 119, 145, 232, 190, 40, 8, 202, 43, 194, 147, 48, 246, 95,
103, 232, 203, 34, 203, 50, 46, 4, 65, 209, 74, 112, 3, 183, 4, 102, 71, 80, 232, 148,
53, 102, 71, 87, 246, 99, 161, 208, 197, 127, 244, 115, 136, 37, 201, 60, 45, 25, 24,
181, 88, 141, 151, 149, 135, 121, 242, 184, 195, 138, 23, 247, 195, 56, 183, 51, 166,
176, 119, 101, 130, 177, 15, 126, 85, 163, 226, 98, 17, 154, 134, 159, 188, 231, 49,
64, 25, 183, 21, 59, 119, 203, 22, 143, 113, 136, 89, 202, 9, 8, 233, 24, 125, 11, 87,
18, 178, 240, 33, 13, 106, 55, 148, 2, 145, 164, 105, 15, 233, 43, 202, 169, 10, 151,
68, 92, 100, 137, 22, 155, 153, 91, 14, 96, 2, 43, 183, 97, 133, 77, 6, 99, 238, 26,
163, 123, 53, 41, 221, 83, 19, 46, 243, 52, 252, 168, 187, 195, 241, 148, 57, 149, 118,
92, 188, 47, 177, 234, 57, 78, 229, 188, 194, 108, 55, 254, 24, 75, 25, 19, 143, 28,
84, 105, 245, 50, 41, 255, 176, 84, 41, 58, 87, 171, 33, 117, 14, 112, 109, 136, 168,
28, 53, 90, 35, 135, 145, 200, 209, 136, 123, 58, 28, 138, 241, 193, 64, 118, 187, 29,
228, 50, 11, 179, 134, 161, 251, 245, 100, 148, 224, 103, 97, 103, 132, 222, 72, 176,
26, 39, 186, 246, 44, 19, 125, 131, 156, 243, 16, 121, 77, 33, 60, 72, 17, 43, 229, 70,
69, 74, 41, 166, 109, 88, 195, 9, 154, 193, 25, 107, 41, 12, 82, 126, 68, 251, 131, 84,
69, 31, 60, 226, 26, 236, 185, 11, 238, 147, 23, 72, 80, 170, 124, 201, 96, 173, 242,
24, 186, 114, 44, 129, 72, 35, 170, 115, 18, 30, 218, 137, 50, 21, 62, 145, 227, 76,
107, 146, 196, 72, 249, 122, 137, 91, 97, 127, 120, 19, 199, 21, 10, 20, 187, 182, 109,
160, 103, 224, 155, 182, 38, 148, 48, 203, 166, 188, 243, 181, 153, 34, 88, 108, 75,
114, 178, 25, 5, 84, 168, 10, 162, 249, 49, 197, 100, 57, 94, 211, 12, 178, 174, 229,
192, 146, 210, 166, 37, 209, 124, 207, 98, 69, 146, 244, 164, 102, 182, 69, 156, 201,
184, 70, 117, 11, 20, 64, 151, 125, 235, 80, 144, 165, 58, 4, 32, 162, 46, 82, 92, 244,
180, 153, 58, 225, 39, 53, 34, 194, 181, 149, 148, 204, 117, 167, 245, 24, 108, 229,
119, 114, 149, 171, 122, 54, 182, 171, 36, 117, 117, 181, 44, 152, 228, 85, 19, 183,
172, 157, 201, 236, 28, 232, 216, 143, 203, 34, 131, 38, 91, 108, 104, 170, 147, 54,
66, 115, 254, 246, 165, 50, 97, 24, 167, 49, 206, 118, 41, 74, 143, 28, 75, 160, 211,
183, 93, 181, 142, 209, 40, 70, 218, 151, 170, 138, 69, 37, 33, 118, 9, 143, 148, 92,
149, 114, 84, 161, 8, 107, 34, 7, 142, 78, 169, 157, 245, 20, 32, 189, 245, 2, 254, 92,
43, 161, 155, 69, 23, 60, 33, 208, 70, 149, 44, 70, 131, 241, 119, 134, 95, 7, 10, 188,
89, 44, 62, 44, 40, 60, 178, 34, 227, 187, 152, 63, 169, 145, 197, 217, 6, 227, 55, 59,
62, 145, 5, 235, 107, 35, 147, 7, 106, 240, 144, 24, 45, 161, 118, 112, 16, 169, 45,
150, 36, 207, 184, 161, 51, 64, 93, 198, 226, 97, 167, 163, 120, 211, 165, 198, 151,
130, 44, 133, 249, 201, 198, 52, 190, 156, 83, 158, 104, 131, 0, 57, 132, 69, 53, 33,
86, 83, 70, 173, 121, 108, 80, 164, 234, 169, 74, 155, 158, 242, 187, 139, 31, 213, 65,
188, 135, 49, 86, 105, 18, 48, 91, 142, 123, 181, 50, 26, 26, 199, 176, 128, 102, 28,
210, 100, 237, 186, 112, 22, 147, 10, 175, 196, 154, 73, 151, 194, 230, 90, 56, 213,
34, 44, 250, 231, 120, 232, 89, 193, 116, 244, 39, 82, 149, 135, 248, 0, 181, 98, 68,
185, 83, 194, 8, 200, 166, 74, 187, 200, 75, 177, 194, 67, 248, 91, 63, 83, 33, 36, 14,
39, 99, 17, 194, 47, 209, 51, 138, 113, 227, 23, 171, 236, 26, 217, 229, 199, 27, 3,
14, 22, 6, 93, 150, 136, 182, 162, 146, 103, 24, 71, 31, 26, 131, 201, 177, 71, 25, 53,
7, 70, 126, 99, 22, 240, 240, 43, 124, 234, 83, 8, 67, 166, 220, 7, 128, 49, 165, 165,
19, 145, 78, 121, 215, 177, 73, 58, 150, 211, 50, 74, 243, 150, 107, 41, 196, 146, 238,
180, 67, 28, 58, 78, 237, 229, 164, 220, 60, 90, 93, 169, 51, 76, 166, 195, 59, 250,
22, 199, 68, 57, 149, 74, 102, 68, 164, 207, 161, 213, 132, 156, 228, 144, 163, 165,
68, 37, 227, 62, 115, 41, 112, 141, 162, 71, 169, 92, 22, 84, 22, 132, 90, 132, 141,
196, 182, 25, 252, 240, 117, 142, 172, 144, 243, 209, 5, 167, 247, 142, 58, 49, 139,
55, 73, 103, 112, 149, 185, 134, 103, 73, 243, 194, 137, 70, 144, 178, 47, 182, 10, 78,
144, 60, 83, 26, 108, 205, 165, 60, 73, 91, 203, 73, 36, 111, 157, 241, 180, 203, 145,
3, 233, 131, 87, 54, 34, 201, 178, 232, 195, 214, 80, 98, 234, 19, 22, 170, 195, 123,
169, 56, 170, 40, 112, 112, 91, 139, 25, 222, 99, 98, 228, 50, 97, 115, 2, 44, 175, 55,
147, 130, 12, 158, 167, 161, 63, 45, 48, 81, 63, 48, 68, 61, 88, 92, 225, 26, 104, 129,
74, 21, 89, 212, 147, 229, 216, 11, 90, 51, 5, 45, 22, 158, 243, 8, 69, 67, 133, 4, 31,
138, 58, 77, 101, 200, 125, 52, 181, 56, 36, 73, 40, 178, 187, 254, 71, 138, 174, 187,
76, 172, 6, 164, 8, 84, 184, 72, 11, 112, 63, 138, 123, 44, 241, 90, 108, 71, 113, 108,
83, 87, 67, 168, 9, 236, 210, 23, 130, 162, 108, 108, 103, 112, 136, 248, 38, 94, 156,
64, 15, 219, 108, 193, 199, 131, 81, 188, 53, 236, 57, 181, 253, 6, 164, 70, 202, 121,
214, 149, 161, 252, 132, 74, 106, 82, 160, 17, 83, 196, 103, 70, 159, 176, 55, 19, 224,
91, 153, 155, 53, 48, 240, 133, 28, 180, 44, 121, 102, 82, 70, 28, 17, 130, 155, 91,
31, 51, 33, 34, 149, 33, 121, 74, 39, 16, 157, 86, 90, 81, 74, 152, 98, 59, 204, 153,
220, 136, 219, 231, 161, 198, 232, 60, 243, 227, 220, 48, 189, 105, 166, 191, 118, 202,
76, 193, 226, 21, 183, 54, 157, 96, 84, 1, 235, 248, 25, 18, 152, 102, 177, 171, 246,
];
assert!(expected_ssk == child_node.value.0.secret_spending_key);
assert!(expected_ccc == child_node.ccc);
assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key);
assert!(
expected_ask
== child_node
.value
.0
.private_key_holder
.authorization_secret_key
);
assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key());
assert!(expected_npk == child_node.value.0.nullifier_public_key);
assert!(expected_vsk == child_node.value.0.private_key_holder.viewing_secret_key);
assert!(expected_vpk == child_node.value.0.viewing_public_key.to_bytes());
@@ -20,7 +20,7 @@ pub struct ChildKeysPublic {
impl ChildKeysPublic {
#[must_use]
pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
let hash_value = hmac_sha512::HMAC::mac(seed, "/LEE-Keys/v1/Master/Public");
let (first, cc) = split_hash(&hash_value);
let sk = lee::PrivateKey::try_new(first).expect("Expect a valid Private Key");
@@ -120,25 +120,25 @@ mod tests {
let keys = ChildKeysPublic::root(SEED);
let expected_cc = [
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155,
184, 94, 197, 114, 84, 79, 170, 62, 107, 107, 141, 196, 11, 255, 15, 165, 7, 40, 93,
211, 244, 153, 12, 70, 10, 174, 141, 69, 117, 167, 165, 81,
];
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,
142, 140, 44, 81, 255, 159, 131, 163, 210, 67, 198, 176, 43, 243, 163, 35, 242, 200,
232, 99, 69, 240, 63, 16, 33, 104, 8, 152, 243, 153, 180, 169,
])
.unwrap();
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,
241, 47, 167, 208, 182, 77, 106, 158, 182, 41, 17, 3, 91, 229, 165, 35, 90, 33, 145,
202, 246, 65, 127, 65, 124, 240, 165, 152, 127, 50, 60, 198,
])
.unwrap();
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,
43, 138, 92, 79, 223, 49, 90, 162, 205, 76, 143, 151, 96, 77, 10, 85, 179, 208, 244,
71, 251, 191, 237, 226, 120, 247, 194, 57, 117, 180, 96, 65,
])
.unwrap();
@@ -155,25 +155,25 @@ mod tests {
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
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,
184, 162, 65, 125, 129, 202, 96, 126, 157, 15, 189, 122, 22, 152, 31, 107, 244, 188,
215, 30, 70, 205, 164, 142, 6, 152, 106, 147, 160, 1, 168, 168,
];
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,
222, 78, 224, 138, 167, 32, 235, 208, 192, 129, 121, 150, 204, 149, 151, 33, 82, 109,
238, 245, 20, 106, 70, 126, 120, 66, 165, 169, 241, 242, 224, 10,
])
.unwrap();
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,
103, 101, 18, 63, 86, 198, 110, 120, 163, 160, 181, 249, 184, 163, 7, 38, 132, 223, 72,
208, 74, 223, 16, 110, 60, 227, 167, 192, 89, 28, 14, 222,
])
.unwrap();
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,
107, 153, 105, 58, 6, 157, 131, 253, 141, 130, 168, 182, 82, 2, 99, 26, 211, 22, 55,
203, 23, 34, 236, 147, 86, 156, 194, 114, 89, 77, 219, 173,
])
.unwrap();
@@ -361,8 +361,8 @@ mod tests {
assert!(tree.key_map.contains_key(&ChainIndex::root()));
assert!(tree.account_id_map.contains_key(&AccountId::new([
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
215, 164, 47, 51, 250, 90, 227, 248, 132, 109, 120, 59, 116, 142, 34, 79, 242, 112, 89,
142, 210, 12, 183, 217, 160, 19, 169, 147, 203, 173, 172, 105
])));
}
@@ -1,6 +1,8 @@
use bip39::Mnemonic;
use common::HashType;
use lee_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey};
use lee_core::{
AuthorizationSecretKey, NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey,
};
use ml_kem;
use rand::{RngCore as _, rngs::OsRng};
use serde::{Deserialize, Serialize};
@@ -36,7 +38,7 @@ impl ViewingSecretKey {
/// for recepient.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PrivateKeyHolder {
pub nullifier_secret_key: NullifierSecretKey,
pub authorization_secret_key: AuthorizationSecretKey,
pub viewing_secret_key: ViewingSecretKey,
}
@@ -87,43 +89,37 @@ impl SeedHolder {
impl SecretSpendingKey {
#[must_use]
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
pub fn generate_nullifier_secret_key(&self, index: Option<u32>) -> NullifierSecretKey {
const PREFIX: &[u8; 8] = b"LEE/keys";
const SUFFIX_1: &[u8; 1] = &[1];
const SUFFIX_2: &[u8; 19] = &[0; 19];
pub fn generate_authorization_secret_key(&self, index: Option<u32>) -> AuthorizationSecretKey {
const DOMAIN: &[u8; 33] = b"/LEE-Keys/v1/Authorization/Secret";
let index = index.unwrap_or(0);
let mut hasher = sha2::Sha256::new();
hasher.update(PREFIX);
hasher.update(DOMAIN);
hasher.update(self.0);
hasher.update(SUFFIX_1);
hasher.update(index.to_be_bytes());
hasher.update(SUFFIX_2);
<NullifierSecretKey>::from(hasher.finalize_fixed())
AuthorizationSecretKey(hasher.finalize_fixed().into())
}
#[must_use]
pub fn generate_nullifier_secret_key(&self, index: Option<u32>) -> NullifierSecretKey {
<NullifierSecretKey>::from(&self.generate_authorization_secret_key(index))
}
#[must_use]
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
pub fn generate_viewing_secret_seed_key(&self, index: Option<u32>) -> ViewingSecretKey {
const PREFIX: &[u8; 8] = b"LEE/keys";
const SUFFIX_1: &[u8; 1] = &[2];
const SUFFIX_2: &[u8; 19] = &[0; 19];
const DOMAIN: &[u8; 27] = b"/LEE-Keys/v1/Viewing/Secret";
let index = index.unwrap_or(0);
let mut bytes: Vec<u8> = Vec::with_capacity(64);
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(&self.0);
bytes.extend_from_slice(SUFFIX_1);
bytes.extend_from_slice(&index.to_be_bytes());
bytes.extend_from_slice(SUFFIX_2);
let bytes: [u8; 64] = bytes
.try_into()
.expect("`generate_viewing_secret_seed_key`: bytes must be exactly 64");
let mut bytes = [0_u8; 27 + 32 + 4];
bytes[..27].copy_from_slice(DOMAIN);
bytes[27..59].copy_from_slice(&self.0);
bytes[59..].copy_from_slice(&index.to_be_bytes());
let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed");
let full_seed = hmac_sha512::HMAC::mac(bytes, b"/LEE-Keys/v1/Viewing/Seed");
Self::generate_viewing_secret_key(full_seed)
}
@@ -139,7 +135,7 @@ impl SecretSpendingKey {
#[must_use]
pub fn produce_private_key_holder(&self, index: Option<u32>) -> PrivateKeyHolder {
PrivateKeyHolder {
nullifier_secret_key: self.generate_nullifier_secret_key(index),
authorization_secret_key: self.generate_authorization_secret_key(index),
viewing_secret_key: self.generate_viewing_secret_seed_key(index),
}
}
@@ -158,9 +154,14 @@ impl From<&ViewingSecretKey> for ViewingPublicKey {
}
impl PrivateKeyHolder {
#[must_use]
pub fn nullifier_secret_key(&self) -> NullifierSecretKey {
(&self.authorization_secret_key).into()
}
#[must_use]
pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey {
(&self.nullifier_secret_key).into()
NullifierPublicKey::from(&self.nullifier_secret_key())
}
#[must_use]
@@ -8,9 +8,9 @@ use lee_core::{
account::{Account, AccountId, AccountWithMetadata},
encryption::ViewingPublicKey,
program::{
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
validate_execution,
AccountPostState, BlockValidityWindow, CallerData, ChainedCall, Claim,
DEFAULT_PROGRAM_OWNER, MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput,
TimestampValidityWindow, validate_execution,
},
};
use risc0_zkvm::{guest::env, serde::to_vec};
@@ -51,7 +51,9 @@ pub struct ExecutionState {
/// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) ==
/// pre_state.account_id`.
private_pda_by_position: HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
authorized_accounts: HashSet<AccountId>,
/// The set containing non-PDA accounts authorized at their first sight, anywhere in the
/// call tree, remaining authorized throughout all calls.
globally_authorized: HashSet<AccountId>,
}
impl ExecutionState {
@@ -112,7 +114,7 @@ impl ExecutionState {
private_pda_bound_positions: HashMap::new(),
pda_family_binding: HashMap::new(),
private_pda_by_position,
authorized_accounts: HashSet::new(),
globally_authorized: HashSet::new(),
};
let Some(first_output) = program_outputs.first() else {
@@ -125,12 +127,17 @@ impl ExecutionState {
pre_states: first_output.pre_states.clone(),
pda_seeds: Vec::new(),
};
let mut chained_calls = VecDeque::from_iter([(initial_call, None)]);
let initial_caller_data = CallerData {
program_id: None,
authorized_accounts: HashSet::new(),
};
let mut chained_calls =
VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]);
let mut program_outputs_iter = program_outputs.into_iter();
let mut chain_calls_counter = 0;
while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() {
while let Some((chained_call, caller_data)) = chained_calls.pop_front() {
assert!(
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
"Max chained calls depth is exceeded"
@@ -166,7 +173,7 @@ impl ExecutionState {
// by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id
// to bypass access control checks).
assert_eq!(
program_output.caller_program_id, caller_program_id,
program_output.caller_program_id, caller_data.program_id,
"Program output caller_program_id does not match actual caller"
);
@@ -184,18 +191,25 @@ impl ExecutionState {
);
}
for next_call in program_output.chained_calls.iter().rev() {
chained_calls.push_front((next_call.clone(), Some(chained_call.program_id)));
}
execution_state.validate_and_sync_states(
let authorized_accounts = execution_state.validate_and_sync_states(
account_identities,
chained_call.program_id,
caller_program_id,
caller_data,
&chained_call.pda_seeds,
program_output.pre_states,
program_output.post_states,
);
for next_call in program_output.chained_calls.into_iter().rev() {
// Push the call with newly-authorized account set.
chained_calls.push_front((
next_call,
CallerData {
program_id: Some(chained_call.program_id),
authorized_accounts: authorized_accounts.clone(),
},
));
}
chain_calls_counter = chain_calls_counter.checked_add(1).expect(
"Chain calls counter should not overflow as it checked before incrementing",
);
@@ -225,7 +239,7 @@ impl ExecutionState {
for (account_id, post) in execution_state
.pre_states
.iter()
.filter(|a| a.account.program_owner == DEFAULT_PROGRAM_ID)
.filter(|a| a.account.program_owner == DEFAULT_PROGRAM_OWNER)
.map(|a| {
let post = execution_state
.post_states
@@ -237,7 +251,7 @@ impl ExecutionState {
.map(|(pre, post)| (pre.account_id, post))
{
assert_ne!(
post.program_owner, DEFAULT_PROGRAM_ID,
post.program_owner, DEFAULT_PROGRAM_OWNER,
"Account {account_id} was modified but not claimed"
);
}
@@ -246,16 +260,20 @@ impl ExecutionState {
}
/// Validate program pre and post states and populate the execution state.
///
/// Return the set of authorized accounts as the result of the processed
/// call.
fn validate_and_sync_states(
&mut self,
account_identities: &[InputAccountIdentity],
program_id: ProgramId,
caller_program_id: Option<ProgramId>,
caller: CallerData,
caller_pda_seeds: &[PdaSeed],
output_pre_states: Vec<AccountWithMetadata>,
output_post_states: Vec<AccountPostState>,
) {
for (pre, mut post) in output_pre_states.into_iter().zip(output_post_states) {
) -> HashSet<AccountId> {
let mut authorized_output_accounts = Vec::new();
for (mut pre, mut post) in output_pre_states.into_iter().zip(output_post_states) {
let pre_account_id = pre.account_id;
let pre_is_authorized = pre.is_authorized;
let post_states_entry = self.post_states.entry(pre.account_id);
@@ -278,33 +296,26 @@ impl ExecutionState {
"Inconsistent pre state for account {pre_account_id}",
);
let (previous_is_authorized, pre_state_position) = self
let pre_state_position = self
.pre_states
.iter()
.enumerate()
.find(|(_, acc)| acc.account_id == pre_account_id)
.map_or_else(
|| panic!(
.position(|acc| acc.account_id == pre_account_id)
.unwrap_or_else(|| {
panic!(
"Pre state must exist in execution state for account {pre_account_id}",
),
|(pos, acc)| (acc.is_authorized, pos)
);
)
});
let is_authorized = resolve_authorization_and_record_bindings(
assert_authorization_and_record_bindings(
&mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_by_position,
&mut self.authorized_accounts,
&self.globally_authorized,
&caller,
caller_pda_seeds,
pre_account_id,
pre_state_position,
caller_program_id,
caller_pda_seeds,
previous_is_authorized,
);
assert_eq!(
pre_is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id}",
pre_is_authorized,
);
}
Entry::Vacant(_) => {
@@ -340,10 +351,6 @@ impl ExecutionState {
// Subsequent calls need no re-check because the entry is already recorded on
// private_pda_bound_positions.
if let Some((authority_program_id, seed)) = external_seed {
assert!(
!pre.is_authorized,
"Private PDA with externally-provided seed must not be authorized at position {pre_state_position}"
);
bind_private_pda_position(
&mut self.private_pda_bound_positions,
pre_state_position,
@@ -357,15 +364,55 @@ impl ExecutionState {
pre_account_id,
);
}
let has_private_pda_witness = self
.private_pda_by_position
.contains_key(&pre_state_position);
if has_private_pda_witness {
assert_authorization_and_record_bindings(
&mut self.pda_family_binding,
&mut self.private_pda_bound_positions,
&self.private_pda_by_position,
&self.globally_authorized,
&caller,
caller_pda_seeds,
pre_account_id,
pre_state_position,
pre_is_authorized,
);
}
if !has_private_pda_witness
&& authorize_first_sight_without_pda_witness(
&mut self.pda_family_binding,
&mut self.globally_authorized,
&caller,
caller_pda_seeds,
pre_account_id,
pre_is_authorized,
)
{
// authorize_first_sight_without_pda_witness is only true for PDAs
// which will be recorded in output journal.
//
// Since we are in a privacy circuit, the verifier cannot
// replay the transaction to see which public PDAs were
// actually authorized. We mark them false as the
// verifier checks regular account signatures as well.
pre.is_authorized = false;
}
self.pre_states.push(pre);
}
}
// If an account it authorized, push it to the autorized set.
if pre_is_authorized {
authorized_output_accounts.push(pre_account_id);
}
if let Some(claim) = post.required_claim() {
// The invoked program can only claim accounts with default program id.
assert_eq!(
post.account().program_owner,
DEFAULT_PROGRAM_ID,
DEFAULT_PROGRAM_OWNER,
"Cannot claim an initialized account {pre_account_id}"
);
@@ -439,11 +486,15 @@ impl ExecutionState {
}
}
post.account_mut().program_owner = program_id;
post.account_mut().program_owner = AccountId::from(program_id);
}
post_states_entry.insert_entry(post.into_account());
}
let mut authorized_accounts = caller.authorized_accounts;
authorized_accounts.extend(authorized_output_accounts);
authorized_accounts
}
/// Consume self and yield the validity windows, the per-position PDA seed/program map
@@ -527,66 +578,127 @@ fn bind_private_pda_position(
}
}
/// Resolve the authorization state of a `pre_state` seen again in a chained call and record
/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a
/// previously-seen authorization or a matching caller seed (under the public or private
/// derivation). When a caller seed matches, also records the `(caller, seed) → account_id`
/// family binding and, for the private form, marks the position in
/// `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`,
/// top-level flows have no caller-emitted seeds, so binding at top level must come from the
/// claim path. Free function so callers can pass individual `&mut self.*` field borrows
/// without holding a borrow on the surrounding struct's other fields.
/// Match `account_id` against the caller's seeds under the public-PDA derivation. `None`
/// if no appropriate authorization given.
fn match_caller_seed_as_public_pda(
caller: &CallerData,
caller_pda_seeds: &[PdaSeed],
account_id: AccountId,
) -> Option<(PdaSeed, ProgramId)> {
let caller_program_id = caller.program_id?;
// Costy for calls with multiple seeds in one call.
caller_pda_seeds.iter().find_map(|seed| {
if AccountId::for_public_pda(&caller_program_id, seed) == account_id {
return Some((*seed, caller_program_id));
}
None
})
}
/// Match `account_id` against the caller's seeds interpreted as private-PDA derivations, using the
/// (npk, vpk, identifier) supplied for this position. `None` when the position carries no
/// private-PDA witness.
fn match_caller_seed_as_private_pda(
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
caller: &CallerData,
caller_pda_seeds: &[PdaSeed],
account_id: AccountId,
pre_state_position: usize,
) -> Option<(PdaSeed, ProgramId)> {
let (npk, vpk, identifier) = private_pda_by_position.get(&pre_state_position)?;
let caller_program_id = caller.program_id?;
// Costy for calls with multiple seeds in one call.
caller_pda_seeds.iter().find_map(|seed| {
if AccountId::for_private_pda(&caller_program_id, seed, npk, vpk, *identifier) == account_id
{
return Some((*seed, caller_program_id));
}
None
})
}
/// Judge a non-private-PDA `pre_state` at its first sighting and resolve its journal mask.
///
/// Either the account is a public PDA in which case the public mask should be changed, or
/// it is a regular account. For PDAs, we assert the family bindings. For regular accounts,
/// add to global authorization set.
fn authorize_first_sight_without_pda_witness(
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
globally_authorized: &mut HashSet<AccountId>,
caller: &CallerData,
caller_pda_seeds: &[PdaSeed],
pre_account_id: AccountId,
pre_is_authorized: bool,
) -> bool {
if let Some((seed, caller_program_id)) =
match_caller_seed_as_public_pda(caller, caller_pda_seeds, pre_account_id)
{
assert!(
pre_is_authorized,
"Caller-seeded public PDA must be declared authorized at first sight: {pre_account_id}"
);
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
true
} else {
// If an authorized account is a non-PDA one, it is globally authorized.
if pre_is_authorized {
globally_authorized.insert(pre_account_id);
}
false
}
}
/// When a caller seed matches, also records the `(caller, seed) → account_id` family binding
/// and, for the private form, marks the position in `private_pda_bound_positions`. Free
/// function so callers can pass individual `&mut self.*` field borrows without holding a borrow
/// on the surrounding struct's other fields.
#[expect(
clippy::too_many_arguments,
reason = "breaking out a context struct does not buy us anything here"
)]
fn resolve_authorization_and_record_bindings(
fn assert_authorization_and_record_bindings(
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
authorized_accounts: &mut HashSet<AccountId>,
globally_authorized: &HashSet<AccountId>,
caller: &CallerData,
caller_pda_seeds: &[PdaSeed],
pre_account_id: AccountId,
pre_state_position: usize,
caller_program_id: Option<ProgramId>,
caller_pda_seeds: &[PdaSeed],
previous_is_authorized: bool,
) -> bool {
pre_is_authorized: bool,
) {
let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> =
caller_program_id.and_then(|caller| {
caller_pda_seeds.iter().find_map(|seed| {
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
return Some((*seed, false, caller));
}
if let Some((npk, vpk, identifier)) =
private_pda_by_position.get(&pre_state_position)
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
== pre_account_id
{
return Some((*seed, true, caller));
}
None
})
});
match_caller_seed_as_public_pda(caller, caller_pda_seeds, pre_account_id)
.map(|(seed, caller_program_id)| (seed, false, caller_program_id))
.or_else(|| {
match_caller_seed_as_private_pda(
private_pda_by_position,
caller,
caller_pda_seeds,
pre_account_id,
pre_state_position,
)
.map(|(seed, caller_program_id)| (seed, true, caller_program_id))
});
if let Some((seed, is_private_form, caller)) = matched_caller_seed {
assert_family_binding(pda_family_binding, caller, seed, pre_account_id);
if let Some((seed, is_private_form, caller_program_id)) = matched_caller_seed {
assert_family_binding(pda_family_binding, caller_program_id, seed, pre_account_id);
if is_private_form {
bind_private_pda_position(
private_pda_bound_positions,
pre_state_position,
caller,
caller_program_id,
seed,
);
}
}
if authorized_accounts.contains(&pre_account_id) {
return true;
}
let is_authorized = matched_caller_seed.is_some()
|| globally_authorized.contains(&pre_account_id)
|| caller.authorized_accounts.contains(&pre_account_id);
let authorized = previous_is_authorized || matched_caller_seed.is_some();
if authorized {
authorized_accounts.insert(pre_account_id);
}
authorized
assert_eq!(
pre_is_authorized, is_authorized,
"Inconsistent authorization for account {pre_account_id}",
);
}
+26 -25
View File
@@ -1,8 +1,8 @@
use lee_core::{
Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme,
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierSecretKey,
NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind, PrivateAction,
PrivateWitness, PublicAction, SharedSecretKey, WitnessKind,
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, NullifierWitness, PrivacyPreservingCircuitOutput, PrivateAccountKind,
PrivateAction, PrivateWitness, PublicAction, SharedSecretKey, WitnessKind,
account::{Account, AccountId, Nonce},
compute_digest_for_path,
encryption::{ViewTag, ViewingPublicKey},
@@ -48,7 +48,7 @@ pub fn compute_circuit_output(
nullifier,
}) => {
let account_id = match kind {
WitnessKind::Regular => {
WitnessKind::Regular { .. } => {
let derived = AccountId::for_regular_private_account(
&nullifier.npk(),
vpk,
@@ -66,27 +66,28 @@ pub fn compute_circuit_output(
WitnessKind::Pda { .. } => pre_state.account_id,
};
match (kind, nullifier) {
(
WitnessKind::Regular,
NullifierWitness::Init { .. } | NullifierWitness::Update { .. },
) => assert!(
if let WitnessKind::Regular { ask } = kind {
if let Some(ask) = ask {
let derived = NullifierSecretKey::from(ask);
match nullifier {
// Check that the authorization key is actually bound to the
// account Id.
NullifierWitness::Update { nsk, .. } => assert_eq!(
derived, *nsk,
"Authorization secret key does not derive this account's nullifier secret key"
),
NullifierWitness::Init { npk, .. } => assert_eq!(
NullifierPublicKey::from(&derived),
*npk,
"Authorization secret key does not derive this account's nullifier public key"
),
}
}
assert_eq!(
pre_state.is_authorized,
"Regular private account pre-state must be authorized"
),
(WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!(
!pre_state.is_authorized,
"Private PDA init requires unauthorized pre_state"
),
// With an external seed the binding comes from the circuit input and the
// pre_state is intentionally unauthorized; without one the binding comes from
// a Claim or caller pda_seeds, so the pre_state must already be authorized.
// When `binding` is `Some`, execution_state already asserted
// `!pre_state.is_authorized`.
(WitnessKind::Pda { binding }, NullifierWitness::Update { .. }) => assert!(
pre_state.is_authorized ^ binding.is_some(),
"Private PDA update requires authorized pre_state or external seed"
),
ask.is_some(),
"Regular private account authorization must match the supplied credential"
);
}
let (new_nullifier, new_nonce, view_tag) = match nullifier {
@@ -126,7 +127,7 @@ pub fn compute_circuit_output(
};
let account_kind = match kind {
WitnessKind::Regular => PrivateAccountKind::Regular(*identifier),
WitnessKind::Regular { .. } => PrivateAccountKind::Regular(*identifier),
WitnessKind::Pda { .. } => {
let (authority_program_id, seed) = pda_seed_by_position
.get(&pos)
+6 -28
View File
@@ -1,7 +1,4 @@
use std::{
fmt::{Display, Write as _},
str::FromStr,
};
use std::{fmt::Display, str::FromStr};
use base58::{FromBase58 as _, ToBase58 as _};
use borsh::{BorshDeserialize, BorshSerialize};
@@ -10,7 +7,7 @@ use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use crate::{NullifierSecretKey, program::ProgramId};
use crate::NullifierSecretKey;
pub mod data;
@@ -93,34 +90,15 @@ pub type Balance = u128;
/// Account to be used both in public and private contexts.
#[derive(
Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub struct Account {
pub program_owner: ProgramId,
pub program_owner: AccountId,
pub balance: Balance,
pub data: Data,
pub nonce: Nonce,
}
impl std::fmt::Debug for Account {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let program_owner_hex = self
.program_owner
.iter()
.flat_map(|n| n.to_le_bytes())
.fold(String::new(), |mut acc, bytes| {
write!(acc, "{bytes:02x}").expect("writing to string should not fail");
acc
});
f.debug_struct("Account")
.field("program_owner", &program_owner_hex)
.field("balance", &self.balance)
.field("data", &self.data)
.field("nonce", &self.nonce)
.finish()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct AccountWithMetadata {
pub account: Account,
@@ -243,14 +221,14 @@ mod tests {
fn default_program_owner_account_data_creation() {
let new_acc = Account::default();
assert_eq!(new_acc.program_owner, DEFAULT_PROGRAM_ID);
assert_eq!(new_acc.program_owner, DEFAULT_PROGRAM_ID.into());
}
#[cfg(feature = "host")]
#[test]
fn account_with_metadata_constructor() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 1337,
data: b"testing_account_with_metadata_constructor"
.to_vec()
+9 -1
View File
@@ -4,7 +4,15 @@ use borsh::{BorshDeserialize, BorshSerialize};
use bytesize::ByteSize;
use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH: ByteSize = ByteSize::kib(100);
/// Raised from the original 100 KiB to accommodate program elfs stored directly in
/// `Account.data` under the Program-as-Account migration.
///
/// Observed elfs currently run 375 KB-520 KB, plus 631 KB for the fixed
/// privacy-preserving circuit itself. This value is a rough placeholder, not a considered
/// protocol constant yet — it still needs to be refined against real transaction/block-size
/// budgets (e.g. `SequencerConfig::max_block_size`, currently 1 MiB) before this is something
/// production traffic should rely on.
pub const DATA_MAX_LENGTH: ByteSize = ByteSize::kib(700);
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
pub struct Data(Vec<u8>);
+10 -12
View File
@@ -2,8 +2,8 @@ use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use crate::{
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey,
AuthorizationSecretKey, Commitment, CommitmentSetDigest, Identifier, MembershipProof,
Nullifier, NullifierPublicKey, NullifierSecretKey,
account::{Account, AccountWithMetadata},
encryption::{EncryptedAccountData, ViewTag, ViewingPublicKey},
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
@@ -48,20 +48,18 @@ pub struct PrivateWitness {
pub enum WitnessKind {
/// Standalone private account. The `account_id` is derived as
/// `AccountId::for_regular_private_account(&npk, vpk, identifier)` and matched against
/// `pre_state.account_id`.
Regular,
/// `pre_state.account_id`. An honest authorized account's `npk` for Id computation gets
/// derived from the supplied `ask`.
Regular { ask: Option<AuthorizationSecretKey> },
/// Private PDA. The npk-to-account_id binding is proven upstream via `Claim::Pda(seed)` or a
/// caller's `pda_seeds` match. The identifier diversifies the PDA within the
/// `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it as the 4th input.
/// An init is unauthorized; on an update, authorization may be established upstream by a
/// caller `pda_seeds` match or a previously-seen authorization in a chained call.
Pda {
/// When `Some((authority_program_id, seed))`, the circuit binds this position via the
/// external derivation check
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
/// == false`.
/// `pda_seeds` to establish the binding.
binding: Option<(ProgramId, PdaSeed)>,
},
}
@@ -220,7 +218,7 @@ mod tests {
PublicAction {
pre: AccountWithMetadata::new(
Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 12_345_678_901_234_567_890,
data: b"test data".to_vec().try_into().unwrap(),
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFE),
@@ -229,7 +227,7 @@ mod tests {
AccountId::new([0; 32]),
),
post: Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 100,
data: b"post state data".to_vec().try_into().unwrap(),
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
@@ -238,7 +236,7 @@ mod tests {
PublicAction {
pre: AccountWithMetadata::new(
Account {
program_owner: [9, 9, 9, 8, 8, 8, 7, 7],
program_owner: [9, 9, 9, 8, 8, 8, 7, 7].into(),
balance: 123_123_123_456_456_567_112,
data: b"test data".to_vec().try_into().unwrap(),
nonce: Nonce(9_999_999_999_999_999_999_999),
@@ -247,7 +245,7 @@ mod tests {
AccountId::new([1; 32]),
),
post: Account {
program_owner: [2, 3, 4, 5, 6, 7, 8, 9],
program_owner: [2, 3, 4, 5, 6, 7, 8, 9].into(),
balance: 200,
data: b"post state data 2".to_vec().try_into().unwrap(),
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFD),
+1 -3
View File
@@ -66,9 +66,7 @@ impl Commitment {
bytes.extend_from_slice(account_id.value());
let account_bytes_with_hashed_data = {
let mut this = Vec::new();
for word in &account.program_owner {
this.extend_from_slice(&word.to_le_bytes());
}
this.extend_from_slice(account.program_owner.as_ref());
this.extend_from_slice(&account.balance.to_le_bytes());
this.extend_from_slice(&account.nonce.0.to_le_bytes());
let hashed_data: [u8; 32] = Impl::hash_bytes(&account.data)
+6 -11
View File
@@ -21,9 +21,7 @@ impl Account {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
for word in &self.program_owner {
bytes.extend_from_slice(&word.to_le_bytes());
}
bytes.extend_from_slice(self.program_owner.as_ref());
bytes.extend_from_slice(&self.balance.to_le_bytes());
bytes.extend_from_slice(&self.nonce.0.to_le_bytes());
let data_length: u32 = u32::try_from(self.data.len()).expect("Invalid u32");
@@ -37,15 +35,12 @@ impl Account {
pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result<Self, LeeCoreError> {
use crate::account::{Nonce, data::Data};
let mut u32_bytes = [0_u8; 4];
let mut u128_bytes = [0_u8; 16];
// program owner
let mut program_owner = [0_u32; 8];
for word in &mut program_owner {
cursor.read_exact(&mut u32_bytes)?;
*word = u32::from_le_bytes(u32_bytes);
}
let mut program_owner_bytes = [0_u8; 32];
cursor.read_exact(&mut program_owner_bytes)?;
let program_owner = AccountId::new(program_owner_bytes);
// balance
cursor.read_exact(&mut u128_bytes)?;
@@ -183,7 +178,7 @@ mod tests {
#[test]
fn enconding() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 123_456_789_012_345_678_901_234_567_890_123_456,
nonce: 42_u128.into(),
data: b"hola mundo".to_vec().try_into().unwrap(),
@@ -244,7 +239,7 @@ mod tests {
#[test]
fn account_to_bytes_roundtrip() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 123_456_789_012_345_678_901_234_567_890_123_456,
nonce: 42_u128.into(),
data: b"hola mundo".to_vec().try_into().unwrap(),
+1 -1
View File
@@ -234,7 +234,7 @@ mod tests {
let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap();
let account = Account {
program_owner: [12_u32; 8],
program_owner: [12_u32; 8].into(),
balance: 999,
..Account::default()
};
+3 -1
View File
@@ -15,7 +15,9 @@ pub use encryption::{
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey,
ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag,
};
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
pub use nullifier::{
AuthorizationSecretKey, Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey,
};
pub use program::PrivateAccountKind;
pub mod account;
+40 -16
View File
@@ -48,16 +48,29 @@ impl AsRef<[u8]> for NullifierPublicKey {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(any(feature = "host", test), derive(Hash))]
pub struct AuthorizationSecretKey(pub [u8; 32]);
impl From<&AuthorizationSecretKey> for NullifierSecretKey {
fn from(value: &AuthorizationSecretKey) -> Self {
const DOMAIN: &[u8; 29] = b"/LEE-Keys/v1/Nullifier/Secret";
let mut bytes = [0_u8; 29 + 32];
bytes[..29].copy_from_slice(DOMAIN);
bytes[29..].copy_from_slice(&value.0);
Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.expect("hash should be exactly 32 bytes long")
}
}
impl From<&NullifierSecretKey> for NullifierPublicKey {
fn from(value: &NullifierSecretKey) -> Self {
const PREFIX: &[u8; 8] = b"LEE/keys";
const SUFFIX_1: &[u8; 1] = &[7];
const SUFFIX_2: &[u8; 23] = &[0; 23];
let mut bytes = Vec::new();
bytes.extend_from_slice(PREFIX);
bytes.extend_from_slice(value);
bytes.extend_from_slice(SUFFIX_1);
bytes.extend_from_slice(SUFFIX_2);
const DOMAIN: &[u8; 29] = b"/LEE-Keys/v1/Nullifier/Public";
let mut bytes = [0_u8; 29 + 32];
bytes[..29].copy_from_slice(DOMAIN);
bytes[29..].copy_from_slice(value);
Self(
Impl::hash_bytes(&bytes)
.as_bytes()
@@ -154,6 +167,17 @@ mod tests {
assert_eq!(nullifier, expected_nullifier);
}
#[test]
fn from_authorization_key() {
let ask = AuthorizationSecretKey([0; 32]);
let expected_nsk: NullifierSecretKey = [
135, 144, 25, 255, 27, 190, 82, 191, 49, 83, 55, 248, 251, 98, 149, 55, 143, 129, 2,
201, 237, 77, 248, 237, 15, 11, 188, 41, 219, 213, 10, 74,
];
let nsk = NullifierSecretKey::from(&ask);
assert_eq!(nsk, expected_nsk);
}
#[test]
fn from_secret_key() {
let nsk = [
@@ -161,8 +185,8 @@ mod tests {
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
];
let expected_npk = NullifierPublicKey([
78, 20, 20, 5, 177, 198, 233, 100, 175, 134, 174, 200, 24, 205, 68, 215, 130, 74, 35,
54, 154, 184, 219, 42, 168, 106, 126, 147, 133, 244, 18, 218,
44, 121, 113, 131, 34, 101, 53, 97, 87, 111, 83, 78, 157, 34, 59, 248, 105, 103, 194,
137, 127, 221, 25, 17, 105, 84, 114, 129, 183, 83, 168, 193,
]);
let npk = NullifierPublicKey::from(&nsk);
assert_eq!(npk, expected_npk);
@@ -177,8 +201,8 @@ mod tests {
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
242, 239, 57, 244, 89, 109, 65, 201, 223, 100, 43, 87, 205, 83, 148, 161, 176, 22, 208,
220, 68, 135, 10, 171, 182, 80, 54, 74, 228, 244, 236, 7,
6, 35, 121, 102, 237, 184, 156, 247, 28, 185, 212, 214, 51, 229, 66, 170, 10, 75, 126,
12, 93, 139, 88, 61, 65, 246, 230, 184, 223, 232, 252, 124,
]);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
@@ -195,8 +219,8 @@ mod tests {
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
149, 125, 157, 109, 119, 81, 9, 163, 231, 181, 214, 43, 57, 113, 221, 72, 180, 149,
189, 170, 32, 181, 255, 231, 19, 92, 235, 59, 153, 185, 172, 206,
56, 217, 214, 244, 51, 212, 184, 73, 217, 85, 4, 126, 54, 35, 135, 225, 75, 253, 183,
19, 96, 182, 189, 138, 62, 101, 131, 30, 2, 236, 157, 235,
]);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1);
@@ -214,8 +238,8 @@ mod tests {
let npk = NullifierPublicKey::from(&nsk);
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
let expected_account_id = AccountId::new([
30, 232, 222, 201, 233, 125, 124, 194, 58, 39, 121, 96, 185, 84, 168, 109, 80, 111,
159, 112, 84, 100, 133, 244, 16, 34, 221, 35, 128, 131, 98, 159,
14, 231, 97, 140, 18, 163, 250, 222, 102, 223, 118, 160, 65, 228, 201, 232, 182, 198,
230, 213, 216, 143, 78, 95, 163, 95, 32, 1, 20, 240, 97, 95,
]);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier);
+56 -7
View File
@@ -11,9 +11,47 @@ use crate::{
};
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
/// TODO: Placeholder `program_owner` for uninitialized `Account`.
pub const DEFAULT_PROGRAM_OWNER: AccountId = AccountId::new([0; 32]);
/// TODO: Temporary placeholder for program deployment program id; this serves as
/// `program_owner` for program `Account`s.
pub const PROGRAM_STORAGE_OWNER: AccountId = AccountId::new([0xFF; 32]);
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub type ProgramId = [u32; 8];
/// Derives the `AccountId` under which a program's data is stored, directly from its
/// `ProgramId`, by reinterpreting the 8 little-endian `u32` words as 32 raw bytes.
///
/// A 1:1, information-preserving mapping (both types are exactly 32 bytes) rather than a
/// hash — `ProgramId` is already content-derived (RISC0's `image_id`), so no extra domain
/// separation is needed just to use it as a `HashMap<AccountId, Account>` key.
impl From<ProgramId> for AccountId {
fn from(program_id: ProgramId) -> Self {
let bytes: Vec<u8> = program_id
.iter()
.flat_map(|word| word.to_le_bytes())
.collect();
Self::new(bytes.try_into().expect("8 u32 words are exactly 32 bytes"))
}
}
impl From<AccountId> for ProgramId {
fn from(account_id: AccountId) -> Self {
let mut program_id = [0_u32; 8];
for (word, chunk) in program_id
.iter_mut()
.zip(account_id.value().chunks_exact(4))
{
*word = u32::from_le_bytes(chunk.try_into().expect("chunk is exactly 4 bytes"));
}
program_id
}
}
pub type InstructionData = Vec<u32>;
pub struct ProgramInput<T> {
pub self_program_id: ProgramId,
@@ -198,6 +236,12 @@ impl AccountId {
}
}
#[derive(Debug)]
pub struct CallerData {
pub program_id: Option<ProgramId>,
pub authorized_accounts: HashSet<AccountId>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct ChainedCall {
/// The program ID of the program to execute.
@@ -289,7 +333,7 @@ impl AccountPostState {
/// if the account's program owner is the default program ID.
#[must_use]
pub fn new_claimed_if_default(account: Account, claim: Claim) -> Self {
let is_default_owner = account.program_owner == DEFAULT_PROGRAM_ID;
let is_default_owner = account.program_owner == DEFAULT_PROGRAM_OWNER;
Self {
account,
claim: is_default_owner.then_some(claim),
@@ -589,11 +633,11 @@ pub enum ExecutionValidationError {
ModifiedProgramOwner { account_id: AccountId },
#[error(
"Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner"
"Trying to decrease balance of account {account_id} owned by {owner_account_id:?} in a program {executing_program_id:?} which is not the owner"
)]
UnauthorizedBalanceDecrease {
account_id: AccountId,
owner_program_id: ProgramId,
owner_account_id: AccountId,
executing_program_id: ProgramId,
},
@@ -672,6 +716,10 @@ pub fn validate_execution(
post_states: &[AccountPostState],
executing_program_id: ProgramId,
) -> Result<(), ExecutionValidationError> {
// `program_owner` is `AccountId`-typed; convert once up front rather than at each
// comparison below (see `From<ProgramId> for AccountId`'s doc comment).
let executing_account_id = AccountId::from(executing_program_id);
// 1. Check account ids are all different
if !validate_uniqueness_of_account_ids(pre_states) {
return Err(ExecutionValidationError::PreStateAccountIdsNotUnique);
@@ -706,11 +754,11 @@ pub fn validate_execution(
// 5. Decreasing balance only allowed if owned by executing program
if post.account.balance < pre.account.balance
&& account_program_owner != executing_program_id
&& account_program_owner != executing_account_id
{
return Err(ExecutionValidationError::UnauthorizedBalanceDecrease {
account_id: pre.account_id,
owner_program_id: account_program_owner,
owner_account_id: account_program_owner,
executing_program_id,
});
}
@@ -719,7 +767,7 @@ pub fn validate_execution(
// default values
if pre.account.data != post.account.data
&& pre.account != Account::default()
&& account_program_owner != executing_program_id
&& account_program_owner != executing_account_id
{
return Err(ExecutionValidationError::UnauthorizedDataModification {
account_id: pre.account_id,
@@ -729,7 +777,8 @@ pub fn validate_execution(
// 7. If a post state has default program owner, the pre state must have been a default
// account
if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() {
if post.account.program_owner == DEFAULT_PROGRAM_OWNER && pre.account != Account::default()
{
return Err(
ExecutionValidationError::NonDefaultAccountWithDefaultOwner {
account_id: pre.account_id,
+61 -3
View File
@@ -132,7 +132,7 @@ fn program_output_try_with_block_validity_window_empty_range_fails() {
#[test]
fn post_state_new_with_claim_constructor() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10_u128.into(),
@@ -147,7 +147,7 @@ fn post_state_new_with_claim_constructor() {
#[test]
fn post_state_new_without_claim_constructor() {
let account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10_u128.into(),
@@ -162,7 +162,7 @@ fn post_state_new_without_claim_constructor() {
#[test]
fn post_state_account_getter() {
let mut account = Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
balance: 1337,
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
nonce: 10_u128.into(),
@@ -339,3 +339,61 @@ fn compute_public_authorized_pdas_no_caller_returns_empty() {
let result = compute_public_authorized_pdas(None, &[seed]);
assert!(result.is_empty());
}
#[test]
fn account_id_from_program_id_reinterprets_words_as_le_bytes() {
let program_id: ProgramId = [
0x0403_0201,
0x0807_0605,
0x0c0b_0a09,
0x100f_0e0d,
0x1413_1211,
0x1817_1615,
0x1c1b_1a19,
0x201f_1e1d,
];
let expected: [u8; 32] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32,
];
assert_eq!(AccountId::from(program_id).value(), &expected);
}
#[test]
fn account_id_from_default_program_id_is_default_program_owner() {
assert_eq!(AccountId::from(DEFAULT_PROGRAM_ID), DEFAULT_PROGRAM_OWNER);
}
#[test]
fn program_id_from_account_id_reinterprets_le_bytes_as_words() {
let account_id = AccountId::new([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32,
]);
let expected: ProgramId = [
0x0403_0201,
0x0807_0605,
0x0c0b_0a09,
0x100f_0e0d,
0x1413_1211,
0x1817_1615,
0x1c1b_1a19,
0x201f_1e1d,
];
assert_eq!(ProgramId::from(account_id), expected);
}
#[test]
fn program_id_account_id_conversion_round_trips() {
let program_id: ProgramId = [
0x1122_3344,
0x5566_7788,
0x99aa_bbcc,
0xddee_ff00,
0xcafe_babe,
0xdead_beef,
0x0bad_f00d,
0xfeed_face,
];
assert_eq!(ProgramId::from(AccountId::from(program_id)), program_id);
}
+24
View File
@@ -135,6 +135,30 @@ mod test_methods {
)
}
#[must_use]
pub const fn selective_pda_delegator() -> Program {
Program::new_unchecked(
test_methods::SELECTIVE_PDA_DELEGATOR_ID,
Cow::Borrowed(test_methods::SELECTIVE_PDA_DELEGATOR_ELF),
)
}
#[must_use]
pub const fn undeclaring_pda_delegator() -> Program {
Program::new_unchecked(
test_methods::UNDECLARING_PDA_DELEGATOR_ID,
Cow::Borrowed(test_methods::UNDECLARING_PDA_DELEGATOR_ELF),
)
}
#[must_use]
pub const fn non_delegating_forwarder() -> Program {
Program::new_unchecked(
test_methods::NON_DELEGATING_FORWARDER_ID,
Cow::Borrowed(test_methods::NON_DELEGATING_FORWARDER_ELF),
)
}
#[must_use]
pub const fn pda_claimer() -> Program {
Program::new_unchecked(
@@ -15,7 +15,7 @@ use crate::{
program::Program,
state::{
CommitmentSet,
tests::{test_private_account_keys_1, test_private_account_keys_2},
tests::{init_pda_witness, test_private_account_keys_1, test_private_account_keys_2},
},
};
@@ -50,7 +50,7 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
let program = crate::test_methods::simple_balance_transfer();
let sender = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 100,
..Account::default()
},
@@ -65,14 +65,14 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
let balance_to_move: u128 = 37;
let expected_sender_post = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 100 - balance_to_move,
nonce: Nonce::default(),
data: Data::default(),
};
let expected_recipient_post = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
data: Data::default(),
@@ -93,7 +93,9 @@ fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts()
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: recipient_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -132,7 +134,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
Account {
balance: 100,
nonce: sender_nonce,
program_owner: program.id(),
program_owner: program.id().into(),
data: Data::default(),
},
true,
@@ -151,7 +153,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
commitment_set.extend(std::slice::from_ref(&commitment_sender));
let expected_new_nullifiers = vec![
(
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk()),
commitment_set.digest(),
),
(
@@ -163,13 +165,13 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
let program = crate::test_methods::simple_balance_transfer();
let expected_private_account_1 = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 100 - balance_to_move,
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
..Default::default()
};
let expected_private_account_2 = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: balance_to_move,
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
..Default::default()
@@ -182,7 +184,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
let esk_1 = EphemeralSecretKey::new(
&sender_account_id,
&[0; 32],
&sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
&sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
);
let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0;
@@ -199,10 +201,12 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: commitment_set
.get_proof_for(&commitment_sender)
.expect("sender's commitment must be in the set"),
@@ -212,7 +216,9 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: recipient_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -284,7 +290,9 @@ fn init_note_view_tag_is_derived_from_account_keys() {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(keys.ask),
},
nullifier: NullifierWitness::Init {
npk: keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -309,7 +317,7 @@ fn update_note_view_tag_is_the_supplied_value() {
let identifier: u128 = 99;
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
let account = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 1,
..Account::default()
};
@@ -329,10 +337,12 @@ fn update_note_view_tag_is_the_supplied_value() {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: fed_tag,
nsk: keys.nsk,
nsk: keys.nsk(),
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
},
})],
@@ -381,7 +391,9 @@ fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
vpk: account_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(account_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: account_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -461,16 +473,7 @@ fn private_pda_init() {
let result = execute_and_prove(
vec![pda_pre],
instruction,
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Init {
npk,
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
vec![init_pda_witness(&keys, 0, None)],
&program_with_deps,
);
@@ -496,7 +499,7 @@ fn private_pda_withdraw() {
let recipient_id = AccountId::new([88; 32]);
let recipient_pre = AccountWithMetadata::new(
Account {
program_owner: simple_transfer.id(),
program_owner: simple_transfer.id().into(),
balance: 10000,
..Account::default()
},
@@ -515,16 +518,7 @@ fn private_pda_withdraw() {
vec![pda_pre, recipient_pre],
instruction,
vec![
InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Init {
npk,
commitment_root: DUMMY_COMMITMENT_HASH,
},
}),
init_pda_witness(&keys, 0, None),
InputAccountIdentity::Public,
],
&program_with_deps,
@@ -550,7 +544,7 @@ fn shared_account_receives_via_simple_transfer() {
let sender_id = AccountId::new([99; 32]);
let sender = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 1000,
..Account::default()
},
@@ -574,7 +568,9 @@ fn shared_account_receives_via_simple_transfer() {
vpk: shared_keys.vpk(),
random_seed: [0; 32],
identifier: shared_identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(shared_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: shared_npk,
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -613,9 +609,11 @@ fn private_authorized_init_encrypts_regular_kind_with_identifier() {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(keys.ask),
},
nullifier: NullifierWitness::Init {
npk: NullifierPublicKey::from(&keys.nsk),
npk: NullifierPublicKey::from(&keys.nsk()),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
@@ -653,7 +651,9 @@ fn private_foreign_init_encrypts_regular_kind_with_identifier() {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(keys.ask),
},
nullifier: NullifierWitness::Init {
npk: keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -680,11 +680,11 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() {
let esk = EphemeralSecretKey::new(
&account_id,
&[0; 32],
&Nonce::default().private_account_nonce_increment(&keys.nsk),
&Nonce::default().private_account_nonce_increment(&keys.nsk()),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let account = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 1,
..Account::default()
};
@@ -701,10 +701,12 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk,
nsk: keys.nsk(),
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
},
})],
@@ -718,26 +720,236 @@ fn private_authorized_update_encrypts_regular_kind_with_identifier() {
);
}
/// A private-PDA update with a non-default identifier produces a ciphertext that decrypts
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
/// Builds an on-chain regular private account owned by `program`, returning its id, pre-state
/// and a membership proof for its commitment.
fn seeded_regular_account(
keys: &crate::state::tests::TestPrivateKeys,
program: &Program,
identifier: u128,
) -> (AccountId, AccountWithMetadata, lee_core::MembershipProof) {
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier);
let account = Account {
program_owner: program.id().into(),
balance: 1,
..Account::default()
};
let commitment = Commitment::new(&account_id, &account);
let mut commitment_set = CommitmentSet::with_capacity(1);
commitment_set.extend(std::slice::from_ref(&commitment));
let proof = commitment_set.get_proof_for(&commitment).unwrap();
(
account_id,
AccountWithMetadata::new(account, false, account_id),
proof,
)
}
/// Spending without consenting. The witness carries no `ask`, so the pre-state is unauthorized,
/// and the nullifier is still produced from the `nsk`.
#[test]
fn private_pda_update_encrypts_pda_kind_with_identifier() {
fn private_regular_update_without_ask_is_spendable() {
let program = crate::test_methods::noop();
let keys = test_private_account_keys_1();
let (_, pre, membership_proof) = seeded_regular_account(&keys, &program, 0);
assert!(!pre.is_authorized);
execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular { ask: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk(),
membership_proof,
},
})],
&program.into(),
)
.unwrap();
}
/// Claiming authorization without supplying an `ask` is rejected.
#[test]
fn private_regular_witness_without_ask_cannot_assert_authorization() {
let program = crate::test_methods::noop();
let keys = test_private_account_keys_1();
let (account_id, pre, membership_proof) = seeded_regular_account(&keys, &program, 0);
let pre = AccountWithMetadata::new(pre.account, true, account_id);
let result = execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular { ask: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk(),
membership_proof,
},
})],
&program.into(),
);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
/// An `ask` that does not derive this account's `nsk` is not a credential for it.
#[test]
fn regular_update_with_wrong_ask_nsk_is_rejected() {
let program = crate::test_methods::noop();
let keys = test_private_account_keys_1();
let foreign = test_private_account_keys_2();
let (account_id, pre, membership_proof) = seeded_regular_account(&keys, &program, 0);
let pre = AccountWithMetadata::new(pre.account, true, account_id);
let result = execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular {
ask: Some(foreign.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk(),
membership_proof,
},
})],
&program.into(),
);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
/// An `ask` that does not derive this account's `npk` is not a credential for it.
#[test]
fn regular_init_with_non_chaining_ask_npk_is_rejected() {
let program = crate::test_methods::claimer();
let keys = test_private_account_keys_1();
let foreign = test_private_account_keys_2();
let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0);
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
let result = execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular {
ask: Some(foreign.ask),
},
nullifier: NullifierWitness::Init {
npk: keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
&program.into(),
);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
#[test]
fn unauthorized_private_init_can_be_claimed() {
let program = crate::test_methods::claimer();
let program_id = program.id();
let keys = test_private_account_keys_1();
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0);
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
let esk = EphemeralSecretKey::new(
&recipient_id,
&[0; 32],
&Nonce::private_account_nonce_init(&recipient_id),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let (output, _) = execute_and_prove(
vec![recipient],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular { ask: None },
nullifier: NullifierWitness::Init {
npk: keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
&program.into(),
)
.unwrap();
let (_, claimed) = EncryptionScheme::decrypt(
&output.private_actions[0].encrypted_post_state.ciphertext,
&ssk,
&output.private_actions[0].nullifier,
)
.unwrap();
assert_eq!(claimed.program_owner, program_id.into());
}
/// A program that asserts authorization over its pre-states rejects a regular private account
/// whose witness supplied no `ask`.
#[test]
fn auth_asserting_program_rejects_unauthorized_regular_private_account() {
let program = crate::test_methods::auth_asserting_noop();
let keys = test_private_account_keys_1();
let (_, pre, membership_proof) = seeded_regular_account(&keys, &program, 0);
let result = execute_and_prove(
vec![pre],
Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular { ask: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk(),
membership_proof,
},
})],
&program.into(),
);
assert!(matches!(result, Err(LeeError::ProgramProveFailed(_))));
}
/// Root-call private-PDA update attempt: `pda_spend_proxy` spends a PDA it owns via
/// `simple_balance_transfer`.
fn pda_update_attempt(
declare_authorized: bool,
derivation_identifier: u128,
witness_identifier: u128,
) -> Result<lee_core::PrivacyPreservingCircuitOutput, LeeError> {
let program = crate::test_methods::pda_spend_proxy();
let simple_transfer = crate::test_methods::simple_balance_transfer();
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let simple_transfer_id = simple_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&pda_id,
&[0; 32],
&Nonce::default().private_account_nonce_increment(&keys.nsk),
let pda_id = AccountId::for_private_pda(
&program.id(),
&seed,
&keys.npk(),
&keys.vpk(),
derivation_identifier,
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
let pda_account = Account {
program_owner: simple_transfer_id,
program_owner: simple_transfer_id.into(),
balance: 1,
..Account::default()
};
@@ -745,26 +957,24 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
let mut commitment_set = CommitmentSet::with_capacity(1);
commitment_set.extend(std::slice::from_ref(&pda_commitment));
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
let pda_pre = AccountWithMetadata::new(pda_account, declare_authorized, pda_id);
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
let program_with_deps = ProgramWithDependencies::new(
program.clone(),
[(simple_transfer_id, simple_transfer)].into(),
);
let program_with_deps =
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
let (output, _) = execute_and_prove(
execute_and_prove(
vec![pda_pre, recipient_pre],
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
Program::serialize_instruction((seed, 1_u128, simple_transfer_id)).unwrap(),
vec![
InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
identifier: witness_identifier,
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk,
nsk: keys.nsk(),
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
},
}),
@@ -772,18 +982,46 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
],
&program_with_deps,
)
.unwrap();
.map(|(output, _proof)| output)
}
/// A private-PDA update with a non-default identifier produces a ciphertext that decrypts
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
#[test]
fn private_pda_update_encrypts_pda_kind_with_identifier() {
let program_id = crate::test_methods::pda_spend_proxy().id();
let keys = test_private_account_keys_1();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 99;
let output = pda_update_attempt(false, identifier, identifier)
.expect("a well-formed private PDA update must prove");
let pda_id =
AccountId::for_private_pda(&program_id, &seed, &keys.npk(), &keys.vpk(), identifier);
let esk = EphemeralSecretKey::new(
&pda_id,
&[0; 32],
&Nonce::default().private_account_nonce_increment(&keys.nsk()),
);
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0;
assert_eq!(
decrypt_kind(&output, &ssk, 0),
PrivateAccountKind::Pda {
program_id: program.id(),
program_id,
seed,
identifier
},
);
}
#[test]
fn private_pda_update_at_root_call_may_not_declare_authorization() {
let result = pda_update_attempt(true, 99, 99);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
#[test]
fn private_pda_init_identifier_mismatch_fails() {
let program = crate::test_methods::pda_claimer();
@@ -791,6 +1029,27 @@ fn private_pda_init_identifier_mismatch_fails() {
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
let result = execute_and_prove(
vec![pre_state],
Program::serialize_instruction(seed).unwrap(),
vec![init_pda_witness(&keys, 99, None)],
&program.into(),
);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
#[test]
fn private_pda_init_at_root_call_may_not_declare_authorization() {
let program = crate::test_methods::pda_claimer();
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let identifier: u128 = 5;
let account_id =
AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier);
let pre_state = AccountWithMetadata::new(Account::default(), true, account_id);
let result = execute_and_prove(
@@ -799,7 +1058,7 @@ fn private_pda_init_identifier_mismatch_fails() {
vec![InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 99,
identifier,
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Init {
npk,
@@ -814,47 +1073,7 @@ fn private_pda_init_identifier_mismatch_fails() {
#[test]
fn private_pda_update_identifier_mismatch_fails() {
let program = crate::test_methods::pda_spend_proxy();
let simple_transfer = crate::test_methods::simple_balance_transfer();
let keys = test_private_account_keys_1();
let npk = keys.npk();
let seed = PdaSeed::new([42; 32]);
let simple_transfer_id = simple_transfer.id();
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5);
let pda_account = Account {
program_owner: simple_transfer_id,
balance: 1,
..Account::default()
};
let pda_commitment = Commitment::new(&pda_id, &pda_account);
let mut commitment_set = CommitmentSet::with_capacity(1);
commitment_set.extend(std::slice::from_ref(&pda_commitment));
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
let program_with_deps =
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
let result = execute_and_prove(
vec![pda_pre, recipient_pre],
Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(),
vec![
InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier: 99,
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
},
}),
InputAccountIdentity::Public,
],
&program_with_deps,
);
let result = pda_update_attempt(false, 5, 99);
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
}
+4 -1
View File
@@ -5,11 +5,14 @@ use k256::ecdsa::signature::hazmat::PrehashVerifier as _;
pub use private_key::PrivateKey;
pub use public_key::PublicKey;
use rand::{RngCore as _, rngs::OsRng};
use serde_with::{DeserializeFromStr, SerializeDisplay};
mod private_key;
mod public_key;
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
#[derive(
Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, SerializeDisplay, DeserializeFromStr,
)]
pub struct Signature {
pub value: [u8; 64],
}
+27 -26
View File
@@ -4,8 +4,8 @@ use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
Timestamp,
account::{Account, AccountId},
program::ProgramId,
account::{Account, AccountId, Data},
program::{PROGRAM_STORAGE_OWNER, ProgramId},
};
use crate::{
@@ -114,7 +114,6 @@ impl BorshDeserialize for NullifierSet {
pub struct V03State {
public_state: HashMap<AccountId, Account>,
private_state: (CommitmentSet, NullifierSet),
programs: HashMap<ProgramId, Program>,
}
impl Default for V03State {
@@ -127,7 +126,6 @@ impl Default for V03State {
Self {
public_state: HashMap::default(),
private_state,
programs: HashMap::default(),
}
}
}
@@ -190,13 +188,20 @@ impl V03State {
#[must_use]
pub fn with_programs(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
for program in programs {
self.insert_program(program);
self.insert_program(&program);
}
self
}
pub(crate) fn insert_program(&mut self, program: Program) {
self.programs.insert(program.id(), program);
pub(crate) fn insert_program(&mut self, program: &Program) {
let account_id = AccountId::from(program.id());
let account = Account {
program_owner: PROGRAM_STORAGE_OWNER,
data: Data::try_from(program.elf().to_vec())
.expect("elf must fit under DATA_MAX_LENGTH"),
..Account::default()
};
self.public_state.insert(account_id, account);
}
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
@@ -222,7 +227,7 @@ impl V03State {
self.private_state.0.extend(&new_commitments);
self.private_state.1.extend(&new_nullifiers);
if let Some(program) = program {
self.insert_program(program);
self.insert_program(&program);
}
}
@@ -276,22 +281,30 @@ impl V03State {
self.public_state.get(&account_id)
}
/// Looks up a deployed program's storage account by its `ProgramId`, verifying it is
/// actually owned by [`PROGRAM_STORAGE_OWNER`].
///
/// An account at `AccountId::from(program_id)` that lacks this ownership isn't a deployed
/// program, whatever its contents — this is the single place that distinction is enforced,
/// so callers never have to remember to re-check it themselves.
#[must_use]
pub fn get_program(&self, program_id: ProgramId) -> Option<&Account> {
let account = self.get_account_by_id_ref(AccountId::from(program_id))?;
(account.program_owner == PROGRAM_STORAGE_OWNER).then_some(account)
}
#[must_use]
pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option<MembershipProof> {
self.private_state.0.get_proof_for(commitment)
}
pub(crate) const fn programs(&self) -> &HashMap<ProgramId, Program> {
&self.programs
}
#[must_use]
pub fn commitment_set_digest(&self) -> CommitmentSetDigest {
self.private_state.0.digest()
}
/// Order-independent fingerprint of the genesis-relevant state: the public
/// account set, the deployed program set, and the commitment-set digest.
/// Order-independent fingerprint of the genesis-relevant state: the public account set
/// (which includes deployed programs' storage accounts) and the commitment-set digest.
///
/// The sequencer and the indexer build the directly-seeded part of genesis
/// (base builtins plus any directly-seeded accounts) separately from their own
@@ -308,17 +321,11 @@ impl V03State {
let Self {
public_state,
private_state,
programs,
} = self;
let mut accounts: Vec<(&AccountId, &Account)> = public_state.iter().collect();
accounts.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
let mut program_ids: Vec<ProgramId> = programs.keys().copied().collect();
program_ids.sort_unstable();
let account_count = u64::try_from(accounts.len()).expect("account count fits in u64");
let program_count = u64::try_from(program_ids.len()).expect("program count fits in u64");
let mut hasher = Sha256::new();
hasher.update(account_count.to_le_bytes());
@@ -329,12 +336,6 @@ impl V03State {
hasher.update(len.to_le_bytes());
hasher.update(&bytes);
}
hasher.update(program_count.to_le_bytes());
for id in program_ids {
for word in id {
hasher.update(word.to_le_bytes());
}
}
hasher.update(private_state.0.digest());
let mut out = [0_u8; 32];
@@ -7,7 +7,7 @@ fn transition_from_authenticated_transfer_program_invocation_default_account_des
let initial_data = [(
account_id,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
},
@@ -64,7 +64,7 @@ fn transition_from_authenticated_transfer_program_invocation_non_default_account
(
account_id1,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
},
@@ -72,7 +72,7 @@ fn transition_from_authenticated_transfer_program_invocation_non_default_account
(
account_id2,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 200,
..Account::default()
},
@@ -106,7 +106,7 @@ fn transition_from_sequence_of_authenticated_transfer_program_invocations() {
let initial_data = [(
account_id1,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
},
@@ -75,10 +75,12 @@ fn private_changer_claimer_no_data_change_no_claim_succeeds() {
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: (0, vec![]),
},
})],
@@ -109,10 +111,12 @@ fn private_changer_claimer_data_change_no_claim_fails() {
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: (0, vec![]),
},
})],
File diff suppressed because it is too large Load Diff
+27 -21
View File
@@ -18,7 +18,7 @@ fn claiming_mechanism() {
assert_eq!(state.get_account_by_id(to), Account::default());
let expected_recipient_post = Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: amount,
nonce: Nonce(1),
..Account::default()
@@ -86,7 +86,7 @@ fn authorized_public_account_claiming_succeeds() {
assert_eq!(
state.get_account_by_id(account_id),
Account {
program_owner: program.id(),
program_owner: program.id().into(),
nonce: Nonce(1),
..Account::default()
}
@@ -114,7 +114,7 @@ fn public_chained_call() {
);
let expected_to_post = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: amount * 2, // The `chain_caller` chains the program twice
..Account::default()
};
@@ -197,7 +197,7 @@ fn execution_that_requires_authentication_of_a_program_derived_account_id_succee
);
let expected_to_post = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: amount, // The `chain_caller` chains the program twice
..Account::default()
};
@@ -244,7 +244,7 @@ fn claiming_mechanism_within_chain_call() {
let expected_to_post = Account {
// The expected program owner is the authenticated transfer program
program_owner: simple_transfer.id(),
program_owner: simple_transfer.id().into(),
balance: amount,
nonce: Nonce(1),
..Account::default()
@@ -299,7 +299,7 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
let program_id = program.id();
let sender_keys = test_private_account_keys_1();
let sender_private_account = Account {
program_owner: program_id,
program_owner: program_id.into(),
balance: 100,
..Account::default()
};
@@ -329,10 +329,12 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
.expect("sender's commitment must be in state"),
@@ -353,13 +355,13 @@ fn authorized_public_account_claiming_succeeds_when_executed_privately() {
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
.unwrap();
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk);
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk());
assert!(state.private_state.1.contains(&nullifier));
assert_eq!(
state.get_account_by_id(recipient_account_id),
Account {
program_owner: program_id,
program_owner: program_id.into(),
balance,
nonce: Nonce(1),
..Account::default()
@@ -378,7 +380,7 @@ fn private_chained_call(number_of_calls: u32) {
let initial_balance = 100;
let from_account = AccountWithMetadata::new(
Account {
program_owner: simple_transfers.id(),
program_owner: simple_transfers.id().into(),
balance: initial_balance,
..Account::default()
},
@@ -387,7 +389,7 @@ fn private_chained_call(number_of_calls: u32) {
);
let to_account = AccountWithMetadata::new(
Account {
program_owner: simple_transfers.id(),
program_owner: simple_transfers.id().into(),
..Account::default()
},
true,
@@ -420,8 +422,8 @@ fn private_chained_call(number_of_calls: u32) {
dependencies.insert(simple_transfers.id(), simple_transfers);
let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies);
let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk);
let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk);
let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk());
let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk());
let from_expected_post = Account {
balance: initial_balance - u128::from(number_of_calls) * amount,
@@ -446,10 +448,12 @@ fn private_chained_call(number_of_calls: u32) {
vpk: from_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(from_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: from_keys.nsk,
nsk: from_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&from_commitment)
.expect("from's commitment must be in state"),
@@ -459,10 +463,12 @@ fn private_chained_call(number_of_calls: u32) {
vpk: to_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(to_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: to_keys.nsk,
nsk: to_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&to_commitment)
.expect("to's commitment must be in state"),
@@ -504,7 +510,7 @@ fn claiming_mechanism_cannot_claim_initialied_accounts() {
state.force_insert_account(
account_id,
Account {
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
program_owner: [1, 2, 3, 4, 5, 6, 7, 8].into(),
..Account::default()
},
);
@@ -543,7 +549,7 @@ fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() {
(
sender_id,
Account {
program_owner: modified_transfer_id,
program_owner: modified_transfer_id.into(),
balance: sender_init_balance,
..Account::default()
},
@@ -551,7 +557,7 @@ fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() {
(
recipient_id,
Account {
program_owner: modified_transfer_id,
program_owner: modified_transfer_id.into(),
balance: recipient_init_balance,
..Account::default()
},
@@ -13,12 +13,12 @@ fn flash_swap_successful() {
let amount_out: u128 = 100;
let vault_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: initial_balance,
..Account::default()
};
let receiver_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: 0,
..Account::default()
};
@@ -64,12 +64,12 @@ fn flash_swap_callback_keeps_funds_rollback() {
let amount_out: u128 = 100;
let vault_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: initial_balance,
..Account::default()
};
let receiver_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: 0,
..Account::default()
};
@@ -121,12 +121,12 @@ fn flash_swap_self_call_targets_correct_program() {
let initial_balance: u128 = 1000;
let vault_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: initial_balance,
..Account::default()
};
let receiver_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: 0,
..Account::default()
};
@@ -167,7 +167,7 @@ fn flash_swap_standalone_invariant_check_rejected() {
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
let vault_account = Account {
program_owner: token.id(),
program_owner: token.id().into(),
balance: 1000,
..Account::default()
};
+5 -16
View File
@@ -24,13 +24,10 @@ fn new_works() {
);
this
};
let expected_builtin_programs = HashMap::new();
let state =
V03State::new().with_public_account_balances([(addr1, 100_u128), (addr2, 151_u128)]);
assert_eq!(state.public_state, expected_public_state);
assert_eq!(state.programs, expected_builtin_programs);
}
#[test]
@@ -67,11 +64,12 @@ fn insert_program() {
let mut state = V03State::new();
let program_to_insert = crate::test_methods::simple_balance_transfer();
let program_id = program_to_insert.id();
assert!(!state.programs.contains_key(&program_id));
let account_id = lee_core::account::AccountId::from(program_id);
assert!(!state.public_state.contains_key(&account_id));
state.insert_program(program_to_insert);
state.insert_program(&program_to_insert);
assert!(state.programs.contains_key(&program_id));
assert!(state.public_state.contains_key(&account_id));
}
#[test]
@@ -81,7 +79,7 @@ fn get_account_by_account_id_non_default_account() {
let initial_data = [(
account_id,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
},
@@ -105,15 +103,6 @@ fn get_account_by_account_id_default_account() {
assert_eq!(account, expected_account);
}
#[test]
fn builtin_programs_getter() {
let state = V03State::new();
let builtin_programs = state.programs();
assert_eq!(builtin_programs, &state.programs);
}
#[test]
fn state_serialization_roundtrip() {
let account_id_1 = AccountId::new([1; 32]);
+76 -46
View File
@@ -7,9 +7,9 @@
use std::collections::HashMap;
use lee_core::{
BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier,
NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp,
WitnessKind,
AuthorizationSecretKey, BlockId, Commitment, DUMMY_COMMITMENT_HASH, Identifier,
InputAccountIdentity, Nullifier, NullifierPublicKey, NullifierSecretKey, NullifierWitness,
PrivateWitness, Timestamp, WitnessKind,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
program::{
@@ -45,36 +45,36 @@ impl V03State {
/// Include test programs in the builtin programs map.
#[must_use]
pub fn with_test_programs(mut self) -> Self {
self.insert_program(crate::test_methods::simple_balance_transfer());
self.insert_program(crate::test_methods::nonce_changer());
self.insert_program(crate::test_methods::extra_output());
self.insert_program(crate::test_methods::missing_output());
self.insert_program(crate::test_methods::dropped_account());
self.insert_program(crate::test_methods::program_owner_changer());
self.insert_program(crate::test_methods::data_changer());
self.insert_program(crate::test_methods::minter());
self.insert_program(crate::test_methods::burner());
self.insert_program(crate::test_methods::auth_asserting_noop());
self.insert_program(crate::test_methods::private_pda_delegator());
self.insert_program(crate::test_methods::pda_claimer());
self.insert_program(crate::test_methods::two_pda_claimer());
self.insert_program(crate::test_methods::noop());
self.insert_program(crate::test_methods::chain_caller());
self.insert_program(crate::test_methods::modified_transfer_program());
self.insert_program(crate::test_methods::malicious_authorization_changer());
self.insert_program(crate::test_methods::validity_window());
self.insert_program(crate::test_methods::flash_swap_initiator());
self.insert_program(crate::test_methods::flash_swap_callback());
self.insert_program(crate::test_methods::malicious_self_program_id());
self.insert_program(crate::test_methods::malicious_caller_program_id());
self.insert_program(crate::test_methods::pda_spend_proxy());
self.insert_program(crate::test_methods::claimer());
self.insert_program(crate::test_methods::changer_claimer());
self.insert_program(crate::test_methods::validity_window_chain_caller());
self.insert_program(crate::test_methods::simple_transfer_proxy());
self.insert_program(crate::test_methods::malicious_injector());
self.insert_program(crate::test_methods::malicious_launderer());
self.insert_program(crate::test_methods::modified_transfer_program());
self.insert_program(&crate::test_methods::simple_balance_transfer());
self.insert_program(&crate::test_methods::nonce_changer());
self.insert_program(&crate::test_methods::extra_output());
self.insert_program(&crate::test_methods::missing_output());
self.insert_program(&crate::test_methods::dropped_account());
self.insert_program(&crate::test_methods::program_owner_changer());
self.insert_program(&crate::test_methods::data_changer());
self.insert_program(&crate::test_methods::minter());
self.insert_program(&crate::test_methods::burner());
self.insert_program(&crate::test_methods::auth_asserting_noop());
self.insert_program(&crate::test_methods::private_pda_delegator());
self.insert_program(&crate::test_methods::pda_claimer());
self.insert_program(&crate::test_methods::two_pda_claimer());
self.insert_program(&crate::test_methods::noop());
self.insert_program(&crate::test_methods::chain_caller());
self.insert_program(&crate::test_methods::modified_transfer_program());
self.insert_program(&crate::test_methods::malicious_authorization_changer());
self.insert_program(&crate::test_methods::validity_window());
self.insert_program(&crate::test_methods::flash_swap_initiator());
self.insert_program(&crate::test_methods::flash_swap_callback());
self.insert_program(&crate::test_methods::malicious_self_program_id());
self.insert_program(&crate::test_methods::malicious_caller_program_id());
self.insert_program(&crate::test_methods::pda_spend_proxy());
self.insert_program(&crate::test_methods::claimer());
self.insert_program(&crate::test_methods::changer_claimer());
self.insert_program(&crate::test_methods::validity_window_chain_caller());
self.insert_program(&crate::test_methods::simple_transfer_proxy());
self.insert_program(&crate::test_methods::malicious_injector());
self.insert_program(&crate::test_methods::malicious_launderer());
self.insert_program(&crate::test_methods::modified_transfer_program());
self
}
@@ -110,7 +110,7 @@ impl V03State {
#[must_use]
pub fn with_account_owned_by_burner_program(mut self) -> Self {
let account = Account {
program_owner: crate::test_methods::burner().id(),
program_owner: crate::test_methods::burner().id().into(),
balance: 100,
..Default::default()
};
@@ -138,14 +138,18 @@ impl TestPublicKeys {
}
pub struct TestPrivateKeys {
pub nsk: NullifierSecretKey,
pub ask: AuthorizationSecretKey,
pub d: [u8; 32],
pub z: [u8; 32],
}
impl TestPrivateKeys {
pub fn nsk(&self) -> NullifierSecretKey {
(&self.ask).into()
}
pub fn npk(&self) -> NullifierPublicKey {
NullifierPublicKey::from(&self.nsk)
NullifierPublicKey::from(&self.nsk())
}
pub fn vpk(&self) -> ViewingPublicKey {
@@ -183,7 +187,7 @@ fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<Acc
(
account_id,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance,
..Account::default()
},
@@ -241,7 +245,7 @@ fn test_public_account_keys_2() -> TestPublicKeys {
pub fn test_private_account_keys_1() -> TestPrivateKeys {
TestPrivateKeys {
nsk: [13; 32],
ask: AuthorizationSecretKey([13; 32]),
d: [31; 32],
z: [32; 32],
}
@@ -249,12 +253,30 @@ pub fn test_private_account_keys_1() -> TestPrivateKeys {
pub fn test_private_account_keys_2() -> TestPrivateKeys {
TestPrivateKeys {
nsk: [38; 32],
ask: AuthorizationSecretKey([38; 32]),
d: [83; 32],
z: [84; 32],
}
}
/// Init-lifecycle private-PDA witness for `keys`, the shape every PDA circuit test starts from.
pub fn init_pda_witness(
keys: &TestPrivateKeys,
identifier: Identifier,
binding: Option<(ProgramId, PdaSeed)>,
) -> InputAccountIdentity {
InputAccountIdentity::Private(PrivateWitness {
vpk: keys.vpk(),
random_seed: [0; 32],
identifier,
kind: WitnessKind::Pda { binding },
nullifier: NullifierWitness::Init {
npk: keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})
}
fn shielded_balance_transfer_for_tests(
sender_keys: &TestPublicKeys,
recipient_keys: &TestPrivateKeys,
@@ -284,7 +306,9 @@ fn shielded_balance_transfer_for_tests(
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: recipient_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -331,10 +355,12 @@ fn private_balance_transfer_for_tests(
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
.expect("sender's commitment must be in state"),
@@ -344,7 +370,9 @@ fn private_balance_transfer_for_tests(
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: recipient_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -392,10 +420,12 @@ fn deshielded_balance_transfer_for_tests(
vpk: sender_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(sender_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: sender_keys.nsk,
nsk: sender_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&sender_commitment)
.expect("sender's commitment must be in state"),
@@ -417,7 +447,7 @@ fn deshielded_balance_transfer_for_tests(
fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) {
let sender_keys = test_private_account_keys_1();
let sender_private_account = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
nonce: Nonce(0xdead_beef),
..Account::default()
@@ -8,7 +8,7 @@ fn transition_from_privacy_preserving_transaction_shielded() {
let mut state = V03State::new().with_public_accounts([(
sender_keys.account_id(),
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 200,
..Account::default()
},
@@ -49,7 +49,7 @@ fn transition_from_privacy_preserving_transaction_private() {
let sender_nonce = Nonce(0xdead_beef);
let sender_private_account = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
nonce: sender_nonce,
data: Data::default(),
@@ -75,8 +75,8 @@ fn transition_from_privacy_preserving_transaction_private() {
let expected_new_commitment_1 = Commitment::new(
&sender_account_id,
&Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
balance: sender_private_account.balance - balance_to_move,
data: Data::default(),
},
@@ -84,12 +84,12 @@ fn transition_from_privacy_preserving_transaction_private() {
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk());
let expected_new_commitment_2 = Commitment::new(
&recipient_account_id,
&Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
balance: balance_to_move,
..Account::default()
@@ -171,7 +171,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
let sender_nonce = Nonce(0xdead_beef);
let sender_private_account = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
nonce: sender_nonce,
data: Data::default(),
@@ -182,7 +182,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
.with_public_accounts([(
recipient_keys.account_id(),
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: recipient_initial_balance,
..Account::default()
},
@@ -210,8 +210,8 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
let expected_new_commitment = Commitment::new(
&sender_account_id,
&Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
balance: sender_private_account.balance - balance_to_move,
data: Data::default(),
},
@@ -219,7 +219,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
let expected_new_nullifier =
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk());
assert!(state.private_state.0.contains(&sender_pre_commitment));
assert!(!state.private_state.0.contains(&expected_new_commitment));
@@ -245,7 +245,7 @@ fn burner_program_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::burner();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 100,
..Account::default()
},
@@ -268,7 +268,7 @@ fn minter_program_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::minter();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -291,7 +291,7 @@ fn nonce_changer_program_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::nonce_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -314,7 +314,7 @@ fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_
let program = crate::test_methods::data_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
program_owner: [0, 1, 2, 3, 4, 5, 6, 7].into(),
balance: 0,
..Account::default()
},
@@ -337,7 +337,7 @@ fn data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_cir
let program = crate::test_methods::data_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -368,7 +368,7 @@ fn extra_output_program_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::extra_output();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -391,7 +391,7 @@ fn missing_output_program_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::missing_output();
let public_account_1 = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -400,7 +400,7 @@ fn missing_output_program_should_fail_in_privacy_preserving_circuit() {
);
let public_account_2 = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -423,7 +423,7 @@ fn program_owner_changer_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::program_owner_changer();
let public_account = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -446,7 +446,7 @@ fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() {
let program = crate::test_methods::simple_balance_transfer();
let public_account_1 = AccountWithMetadata::new(
Account {
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
program_owner: [0, 1, 2, 3, 4, 5, 6, 7].into(),
balance: 100,
..Account::default()
},
@@ -455,7 +455,7 @@ fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() {
);
let public_account_2 = AccountWithMetadata::new(
Account {
program_owner: program.id(),
program_owner: program.id().into(),
balance: 0,
..Account::default()
},
@@ -483,7 +483,7 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
let sender_account = AccountWithMetadata::new(
Account {
program_owner: simple_transfers.id(),
program_owner: simple_transfers.id().into(),
balance: 100,
..Default::default()
},
@@ -525,10 +525,12 @@ fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
vpk: recipient_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(recipient_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: recipient_keys.nsk,
nsk: recipient_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&recipient_commitment)
.expect("recipient's commitment must be in state"),
@@ -96,7 +96,7 @@ fn program_should_fail_if_it_drops_a_declared_account() {
(
AccountId::new([1; 32]),
Account {
program_owner: crate::test_methods::dropped_account().id(),
program_owner: crate::test_methods::dropped_account().id().into(),
balance: 100,
..Account::default()
},
@@ -104,7 +104,7 @@ fn program_should_fail_if_it_drops_a_declared_account() {
(
AccountId::new([2; 32]),
Account {
program_owner: crate::test_methods::dropped_account().id(),
program_owner: crate::test_methods::dropped_account().id().into(),
balance: 0,
..Account::default()
},
@@ -136,7 +136,7 @@ fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_o
let initial_data = [(
AccountId::new([1; 32]),
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
..Account::default()
},
)];
@@ -268,7 +268,7 @@ fn program_should_fail_if_transfers_balance_from_non_owned_account() {
let program_id = crate::test_methods::simple_balance_transfer().id();
assert_ne!(
state.get_account_by_id(sender_account_id).program_owner,
program_id
program_id.into()
);
let message = public_transaction::Message::try_new(
program_id,
@@ -285,8 +285,8 @@ fn program_should_fail_if_transfers_balance_from_non_owned_account() {
assert!(matches!(
result,
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id }
))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_account_id, executing_program_id }
))) if err_account_id == sender_account_id && owner_account_id != program_id.into() && executing_program_id == program_id
));
}
@@ -303,7 +303,7 @@ fn program_should_fail_if_modifies_data_of_non_owned_account() {
assert_ne!(state.get_account_by_id(account_id), Account::default());
assert_ne!(
state.get_account_by_id(account_id).program_owner,
program_id
program_id.into()
);
let message =
public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0])
@@ -356,7 +356,7 @@ fn program_should_fail_if_does_not_preserve_total_balance_by_burning() {
let account_id = AccountId::new([252; 32]);
assert_eq!(
state.get_account_by_id(account_id).program_owner,
program_id
program_id.into()
);
let balance_to_burn: u128 = 1;
assert!(state.get_account_by_id(account_id).balance > balance_to_burn);
@@ -142,7 +142,9 @@ fn validity_window_works_in_privacy_preserving_transactions(
vpk: account_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(account_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: account_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -210,7 +212,9 @@ fn timestamp_validity_window_works_in_privacy_preserving_transactions(
vpk: account_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(account_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: account_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -1,4 +1,5 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet, VecDeque},
hash::Hash,
};
@@ -7,7 +8,7 @@ use lee_core::{
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp,
account::{Account, AccountId, AccountWithMetadata},
program::{
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas,
CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_OWNER, compute_public_authorized_pdas,
validate_execution,
},
};
@@ -111,10 +112,13 @@ impl ValidatedStateDiff {
LeeError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
let Some(program_account) = state.get_program(chained_call.program_id) else {
return Err(LeeError::InvalidInput("Unknown program".into()));
};
let program = Program::new_unchecked(
chained_call.program_id,
Cow::Owned(program_account.data.to_vec()),
);
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
@@ -217,7 +221,7 @@ impl ValidatedStateDiff {
// The invoked program can only claim accounts with default program id.
ensure!(
post.account().program_owner == DEFAULT_PROGRAM_ID,
post.account().program_owner == DEFAULT_PROGRAM_OWNER,
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
);
@@ -244,7 +248,7 @@ impl ValidatedStateDiff {
}
}
post.account_mut().program_owner = chained_call.program_id;
post.account_mut().program_owner = AccountId::from(chained_call.program_id);
}
// Update the state diff
@@ -264,17 +268,14 @@ impl ValidatedStateDiff {
// Union with the caller's authorized set so that authorization is monotonically
// growing: once an account is authorized at any point in the chain it remains
// authorized for all subsequent calls.
let authorized_accounts: HashSet<_> = caller_data
.authorized_accounts
.into_iter()
.chain(
program_output
.pre_states
.iter()
.filter(|pre| pre.is_authorized)
.map(|pre| pre.account_id),
)
.collect();
let mut authorized_accounts = caller_data.authorized_accounts;
authorized_accounts.extend(
program_output
.pre_states
.iter()
.filter(|pre| pre.is_authorized)
.map(|pre| pre.account_id),
);
for new_call in program_output.chained_calls.into_iter().rev() {
chained_calls.push_front((
new_call,
@@ -293,7 +294,7 @@ impl ValidatedStateDiff {
// Check that all modified uninitialized accounts where claimed
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
let pre = state.get_account_by_id(*account_id);
if pre.program_owner != DEFAULT_PROGRAM_ID {
if pre.program_owner != DEFAULT_PROGRAM_OWNER {
return None;
}
if pre == *post {
@@ -302,7 +303,7 @@ impl ValidatedStateDiff {
Some((*account_id, post))
}) {
ensure!(
post.program_owner != DEFAULT_PROGRAM_ID,
post.program_owner != DEFAULT_PROGRAM_OWNER,
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
);
}
@@ -444,7 +445,7 @@ impl ValidatedStateDiff {
) -> Result<Self, LeeError> {
// TODO: remove clone
let program = Program::new(tx.message.bytecode.clone().into())?;
if state.programs().contains_key(&program.id()) {
if state.get_program(program.id()).is_some() {
return Err(LeeError::ProgramAlreadyExists);
}
Ok(Self(StateDiff {
@@ -470,12 +471,6 @@ impl ValidatedStateDiff {
}
}
#[derive(Debug)]
struct CallerData {
program_id: Option<ProgramId>,
authorized_accounts: HashSet<AccountId>,
}
fn authenticate_public_transaction_signers(
tx: &PublicTransaction,
state: &V03State,
@@ -18,7 +18,7 @@ fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<Acc
(
account_id,
Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance,
..Account::default()
},
@@ -126,7 +126,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
// Build attacker's private account and its local commitment tree.
let attacker_account = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
};
@@ -146,7 +146,7 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
*victim_id.value(),
victim_account.balance,
victim_account.nonce.0,
victim_account.program_owner,
victim_account.program_owner.into(),
*recipient_id.value(),
victim_balance,
);
@@ -168,10 +168,12 @@ fn privacy_malicious_programs_cannot_drain_public_victim() {
vpk: attacker_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(attacker_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: attacker_keys.nsk,
nsk: attacker_keys.nsk(),
membership_proof,
},
}),
@@ -282,7 +284,7 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
// Build attacker's private account and its local commitment tree.
let attacker_account = Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
program_owner: crate::test_methods::simple_balance_transfer().id().into(),
balance: 100,
..Account::default()
};
@@ -330,10 +332,12 @@ fn privacy_malicious_programs_cannot_drain_private_victim() {
vpk: attacker_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(attacker_keys.ask),
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: attacker_keys.nsk,
nsk: attacker_keys.nsk(),
membership_proof,
},
}),
@@ -435,7 +439,7 @@ fn malicious_programs_cannot_drain_victim_without_signature() {
*victim_id.value(),
victim_account.balance,
victim_account.nonce.0,
victim_account.program_owner,
victim_account.program_owner.into(),
*recipient_id.value(),
victim_balance,
);
@@ -60,7 +60,7 @@ fn main() {
// Victim has not signed anything — this flag is forged entirely by P1's logic.
let victim = AccountWithMetadata {
account: Account {
program_owner: victim_program_owner,
program_owner: victim_program_owner.into(),
balance: victim_balance,
data: Data::default(),
nonce: Nonce(victim_nonce),
@@ -75,7 +75,7 @@ fn main() {
// on the recipient — a check that would block the transfer.
let recipient = AccountWithMetadata {
account: Account {
program_owner: auth_transfer_id,
program_owner: auth_transfer_id.into(),
balance: 0,
data: Data::default(),
nonce: Nonce(0),
@@ -0,0 +1,45 @@
use lee_core::program::{
AccountPostState, ChainedCall, InstructionData, ProgramId, ProgramInput, ProgramOutput,
read_lee_inputs,
};
type Instruction = (ProgramId, InstructionData, bool);
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction: (callee_program_id, callee_instruction, declare_pre_states),
},
instruction_words,
) = read_lee_inputs::<Instruction>();
let (output_pre_states, output_post_states) = if declare_pre_states {
let post_states = pre_states
.iter()
.map(|account| AccountPostState::new(account.account.clone()))
.collect();
(pre_states.clone(), post_states)
} else {
(Vec::new(), Vec::new())
};
// Make exactly one chained call based on the input instruction with no
// pda seeds, ensuring the target PDAs are never authorized.
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
output_pre_states,
output_post_states,
)
.with_chained_calls(vec![ChainedCall {
program_id: callee_program_id,
instruction_data: callee_instruction,
pre_states,
pda_seeds: vec![],
}])
.write();
}
@@ -5,7 +5,7 @@ use risc0_zkvm::serde::to_vec;
/// Proxy for spending from a private PDA via `simple_transfer`.
///
/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient.
/// `pre_states = [pda, recipient]`. Debits the PDA and credits the recipient.
/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `simple_transfer`.
type Instruction = (PdaSeed, u128, ProgramId);
@@ -24,15 +24,16 @@ fn main() {
return;
};
assert!(first.is_authorized, "first pre_state must be authorized");
let first_post = AccountPostState::new(first.account.clone());
let second_post = AccountPostState::new(second.account.clone());
let mut first_for_callee = first.clone();
first_for_callee.is_authorized = true;
let chained_call = ChainedCall {
program_id: simple_transfer_id,
instruction_data: to_vec(&amount).unwrap(),
pre_states: vec![first.clone(), second.clone()],
pre_states: vec![first_for_callee, second.clone()],
pda_seeds: vec![seed],
};
@@ -30,7 +30,7 @@ fn main() {
let mut pre_for_callee = pre.clone();
pre_for_callee.is_authorized = true;
pre_for_callee.account.program_owner = self_program_id;
pre_for_callee.account.program_owner = self_program_id.into();
let chained_call = ChainedCall {
program_id: callee_program_id,
@@ -19,7 +19,7 @@ fn main() {
let account_pre = &pre.account;
let mut account_post = account_pre.clone();
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7];
account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7].into();
ProgramOutput::new(
self_program_id,
@@ -0,0 +1,82 @@
use lee_core::program::{
AccountPostState, ChainedCall, Claim, InstructionData, PdaSeed, ProgramId, ProgramInput,
ProgramOutput, read_lee_inputs,
};
use risc0_zkvm::serde::to_vec;
type Instruction = (
PdaSeed,
PdaSeed,
ProgramId,
InstructionData,
Option<(ProgramId, Option<bool>)>,
);
fn main() {
let (
ProgramInput {
self_program_id,
caller_program_id,
pre_states,
instruction:
(claim_seed, delegated_seed, callee_program_id, callee_instruction, sibling),
},
instruction_words,
) = read_lee_inputs::<Instruction>();
let Some((pda, rest)) = pre_states.split_first() else {
return;
};
let pda_for_callee = |is_authorized| {
let mut for_callee = pda.clone();
for_callee.is_authorized = is_authorized;
for_callee.account.program_owner = self_program_id.into();
for_callee
};
// Send a call to the specified program with the same pre-states
// but authorized first PDA supplied.
// Push all the delegated seeds.
let mut chained_calls = vec![ChainedCall {
program_id: callee_program_id,
instruction_data: callee_instruction,
pre_states: std::iter::once(pda_for_callee(true))
.chain(rest.iter().cloned())
.collect(),
pda_seeds: vec![delegated_seed],
}];
// If sibling is present in instruction, send out a call
// with no seeds so that PDAs stay unauthorized in parallel
// branches.
if let Some((sibling_program_id, sibling_pda)) = sibling {
chained_calls.push(ChainedCall {
program_id: sibling_program_id,
instruction_data: to_vec(&()).unwrap(),
pre_states: sibling_pda.map_or_else(
|| rest.to_vec(),
|is_authorized| {
std::iter::once(pda_for_callee(is_authorized))
.chain(rest.iter().cloned())
.collect()
},
),
pda_seeds: vec![],
});
}
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![pda.clone()],
// Claim first PDA supplied
vec![AccountPostState::new_claimed(
pda.account.clone(),
Claim::Pda(claim_seed),
)],
)
.with_chained_calls(chained_calls)
.write();
}

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