Merge remote-tracking branch 'origin/dev' into erhant/sequencer-gossip

Resolves the actors-refactor (#691) collision: gossip now starts in
sequencer_service::run() after ExecutorActor construction (which exposes
its mempool handle), and the RPC-side publish hook threads through
RpcServerActor::new into the actor's Service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
erhant
2026-08-13 19:04:09 +03:00
co-authored by Claude Fable 5
156 changed files with 7091 additions and 2307 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
+109 -16
View File
@@ -1225,6 +1225,7 @@ name = "bridge_lock_core"
version = "0.1.0"
dependencies = [
"lee_core",
"risc0-zkvm",
"serde",
]
@@ -2002,6 +2003,7 @@ dependencies = [
"log",
"ping_core",
"programs",
"rand 0.8.6",
"risc0-zkvm",
"sequencer_service_rpc",
"serde",
@@ -2298,7 +2300,7 @@ dependencies = [
"clap",
"json-pretty-compact",
"sequencer_core_metrics",
"sequencer_service_metrics",
"sequencer_rpc_server_actor_metrics",
"serde",
"serde_json",
]
@@ -2326,7 +2328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
dependencies = [
"data-encoding",
"syn 1.0.109",
"syn 2.0.117",
]
[[package]]
@@ -2572,6 +2574,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "downcast-rs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
[[package]]
name = "downloader"
version = "0.2.8"
@@ -4388,8 +4396,6 @@ dependencies = [
"lee",
"lee_core",
"log",
"logos-blockchain-core",
"logos-blockchain-key-management-system-service",
"ping_core",
"programs",
"risc0-zkvm",
@@ -4817,6 +4823,46 @@ dependencies = [
"wnaf",
]
[[package]]
name = "kameo"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cbab7323ed30490812f43ef6416a9e21bb150e9633e451ed124ca276b1ca82a"
dependencies = [
"downcast-rs 2.0.2",
"dyn-clone",
"futures",
"kameo_macros",
"serde",
"tokio",
"tracing",
]
[[package]]
name = "kameo_actors"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069ef0ae25f4da6f817ce7d81f7990b3d12c3ae398b59e6e16e37b5fcc92a443"
dependencies = [
"futures",
"glob",
"kameo",
"thiserror 2.0.18",
"tokio",
]
[[package]]
name = "kameo_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7566055976eb86ee8e8fbafa0fbdad985c5d7c3f4eed04fc11bb495f71e3856"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "keccak"
version = "0.1.6"
@@ -7702,7 +7748,9 @@ dependencies = [
name = "ping_core"
version = "0.1.0"
dependencies = [
"borsh",
"lee_core",
"risc0-zkvm",
"serde",
]
@@ -7710,6 +7758,7 @@ dependencies = [
name = "ping_receiver_program"
version = "0.1.0"
dependencies = [
"cross_zone_inbox_core",
"lee_core",
"ping_core",
]
@@ -9038,7 +9087,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4382d3af3a4ebdae7f64ba6edd9114fff92c89808004c4943b393377a25d001"
dependencies = [
"downcast-rs",
"downcast-rs 1.2.1",
"paste",
]
@@ -9534,38 +9583,77 @@ dependencies = [
]
[[package]]
name = "sequencer_service"
name = "sequencer_executor_actor"
version = "0.1.0"
dependencies = [
"anyhow",
"borsh",
"bytesize",
"clap",
"common",
"env_logger",
"futures",
"hex",
"jsonrpsee",
"kameo",
"lee",
"lee_core",
"log",
"mempool",
"metrics-exporter-prometheus",
"programs",
"num-bigint 0.4.6",
"sequencer_core",
"sequencer_service_metrics",
"sequencer_service_protocol",
"sequencer_service_rpc",
"storage",
"tempfile",
"test_programs",
"thiserror 2.0.18",
"tokio",
"tokio-util",
]
[[package]]
name = "sequencer_service_metrics"
name = "sequencer_rpc_server_actor"
version = "0.1.0"
dependencies = [
"borsh",
"bytesize",
"common",
"jsonrpsee",
"kameo",
"lee",
"log",
"programs",
"sequencer_core",
"sequencer_executor_actor",
"sequencer_rpc_server_actor_metrics",
"sequencer_service_protocol",
"sequencer_service_rpc",
"thiserror 2.0.18",
"tokio",
]
[[package]]
name = "sequencer_rpc_server_actor_metrics"
version = "0.1.0"
dependencies = [
"metrics",
]
[[package]]
name = "sequencer_service"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"env_logger",
"futures",
"hex",
"kameo",
"kameo_actors",
"log",
"metrics-exporter-prometheus",
"sequencer_core",
"sequencer_executor_actor",
"sequencer_rpc_server_actor",
"tokio",
"tokio-util",
]
[[package]]
name = "sequencer_service_protocol"
version = "0.1.0"
@@ -9574,6 +9662,7 @@ dependencies = [
"hex",
"lee",
"lee_core",
"serde",
"serde_with",
]
@@ -10710,6 +10799,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.6.4",
"tokio-macros",
"tracing",
"windows-sys 0.61.2",
]
@@ -11575,6 +11665,7 @@ version = "0.1.0"
dependencies = [
"bip39",
"cbindgen",
"common",
"key_protocol",
"lee",
"lee_core",
@@ -12255,6 +12346,7 @@ dependencies = [
name = "wrapped_token_core"
version = "0.1.0"
dependencies = [
"borsh",
"lee_core",
"risc0-zkvm",
"serde",
@@ -12264,6 +12356,7 @@ dependencies = [
name = "wrapped_token_program"
version = "0.1.0"
dependencies = [
"cross_zone_inbox_core",
"lee_core",
"wrapped_token_core",
]
+10 -2
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",
@@ -84,7 +86,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" }
@@ -130,6 +134,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"
@@ -353,6 +359,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.
+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
@@ -36,8 +36,6 @@ programs.workspace = true
test_programs.workspace = true
testnet_initial_state.workspace = true
logos-blockchain-core.workspace = true
logos-blockchain-key-management-system-service.workspace = true
anyhow.workspace = true
log.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+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);
+4 -5
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},
@@ -33,7 +32,7 @@ async fn get_existing_account() -> Result<()> {
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(())
}
@@ -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?;
@@ -388,7 +387,7 @@ async fn initialize_private_account() -> Result<()> {
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(())
}
@@ -465,7 +464,7 @@ async fn initialize_private_account_using_label() -> Result<()> {
programs::authenticated_transfer().id()
);
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);
+33 -34
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,7 +208,7 @@ 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!(
@@ -220,7 +219,7 @@ async fn initialize_public_account() -> Result<()> {
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());
+1 -2
View File
@@ -6,7 +6,6 @@
use anyhow::Result;
use integration_tests::TestContext;
use log::info;
use tokio::test;
use wallet::cli::{Command, config::ConfigSubcommand};
@@ -33,7 +32,7 @@ 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(())
}
+43 -38
View File
@@ -9,12 +9,10 @@
//! wrapped token is minted to the recipient. Reuses the M3/M4 spine unchanged;
//! only the source caller (`bridge_lock`) and target (`wrapped_token`) are new.
//!
//! Not production-safe. The inbox allowlist gates the target program, not the
//! source emitter, and `extract_emission` recognizes any known emitter, so in a
//! zone that allows `wrapped_token` as a target a permissionless `ping_sender`
//! send can carry a `wrapped_token::Mint` and mint with no lock. Making this safe
//! needs source verification, where a value-bearing target checks the message
//! originated from `bridge_lock`; that is out of scope for the demo.
//! A `ping_sender` send carrying a `wrapped_token::Mint` is refused as long as no
//! operator writes a `(ping_sender, wrapped_token)` route: the allowlist is a
//! source-and-target pair. Nothing forbids writing that route, and the token
//! still trusts the table rather than checking its own sources, which is #673.
use std::time::Duration;
@@ -24,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,
@@ -32,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);
@@ -41,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 +67,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 +117,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,
@@ -156,14 +161,14 @@ fn build_lock_tx(
target_program_id: wrapped_token_id,
target_accounts,
payload,
outbox_program_id: outbox_id,
ordinal,
};
let accounts = vec![
bridge_lock_core::config_account_id(bridge_lock_id),
holder_id,
bridge_lock_core::escrow_account_id(bridge_lock_id),
outbox_pda(outbox_id, &target_zone, ordinal),
outbox_pda(outbox_id, bridge_lock_id, &target_zone, ordinal),
];
// One nonce per signature: the holder signs, at its genesis nonce 0.
let message = Message::try_new(bridge_lock_id, accounts, vec![0_u128.into()], lock)
@@ -9,41 +9,46 @@
//! 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();
let msg = CrossZoneMessage {
src_zone: [2; 32],
src_block_id: 1,
src_block_hash: [7; 32],
src_tx_index: 0,
src_program_id: [9; 8],
target_program_id: programs::ping_receiver().id(),
@@ -63,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"),
+51 -30
View File
@@ -16,15 +16,18 @@ 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::{ReceiverInstruction, SenderInstruction, ping_record_pda};
use ping_core::{
ReceiverInstruction, SenderInstruction, ping_record_pda, receiver_config_account_id,
sender_config_account_id,
};
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);
@@ -32,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();
@@ -57,30 +55,50 @@ async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
}],
};
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,
@@ -104,18 +122,21 @@ fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransactio
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
let send = SenderInstruction::Send {
outbox_program_id: outbox_id,
target_zone,
target_program_id: receiver_id,
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
target_accounts: vec![
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload,
ordinal,
};
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
let sender_id = programs::ping_sender().id();
let outbox_account = outbox_pda(outbox_id, sender_id, &target_zone, ordinal);
let message = Message::try_new(
programs::ping_sender().id(),
vec![outbox_account],
sender_id,
vec![sender_config_account_id(sender_id), outbox_account],
vec![],
send,
)
File diff suppressed because it is too large Load Diff
+45 -36
View File
@@ -17,13 +17,18 @@ 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;
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
use ping_core::{
ReceiverInstruction, SenderInstruction, ping_record_pda, receiver_config_account_id,
sender_config_account_id,
};
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);
@@ -31,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();
@@ -54,31 +54,40 @@ async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
}],
};
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")?;
@@ -86,11 +95,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"
@@ -109,18 +115,21 @@ fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransactio
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
let send = SenderInstruction::Send {
outbox_program_id: outbox_id,
target_zone,
target_program_id: receiver_id,
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
target_accounts: vec![
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload,
ordinal,
};
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
let sender_id = programs::ping_sender().id();
let outbox_account = outbox_pda(outbox_id, sender_id, &target_zone, ordinal);
let message = Message::try_new(
programs::ping_sender().id(),
vec![outbox_account],
sender_id,
vec![sender_config_account_id(sender_id), outbox_account],
vec![],
send,
)
@@ -25,7 +25,10 @@ use integration_tests::{
};
use lee::{AccountId, PublicTransaction, public_transaction::Message};
use lee_core::program::ProgramId;
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
use ping_core::{
ReceiverInstruction, SenderInstruction, ping_record_pda, receiver_config_account_id,
sender_config_account_id,
};
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, CrossZoneRoute};
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
use tokio::test;
@@ -190,18 +193,21 @@ fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransactio
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
let send = SenderInstruction::Send {
outbox_program_id: outbox_id,
target_zone,
target_program_id: receiver_id,
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
target_accounts: vec![
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload,
ordinal,
};
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
let sender_id = programs::ping_sender().id();
let outbox_account = outbox_pda(outbox_id, sender_id, &target_zone, ordinal);
let message = Message::try_new(
programs::ping_sender().id(),
vec![outbox_account],
sender_id,
vec![sender_config_account_id(sender_id), outbox_account],
vec![],
send,
)
@@ -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);
+4 -5
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?;
@@ -150,7 +149,7 @@ async fn restore_keys_from_seed() -> Result<()> {
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(())
}
+69 -70
View File
@@ -14,18 +14,14 @@ 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},
};
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;
@@ -34,86 +30,74 @@ 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();
let bedrock_channel_id = 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()
.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?;
let ctx = MultiZoneTestContextBuilder::default()
.with_zone(
ZoneTestContextBuilder::new(MultiNodeTestContextConfig {
num_nodes: 2,
bedrock_channel: bedrock_channel_id,
})
.disable_wallet()
.with_sequencer_partial_config(partial)
.with_genesis(vec![]),
)
.build()
.await?;
wait_for_height(&a, 2, "sequencer A to produce past genesis").await?;
let mut seq_iterator = ctx.sequencer_components_iter(bedrock_channel_id).unwrap();
// 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 seq_client_a = &(seq_iterator.next().unwrap().sequencer_client);
let seq_client_b = &(seq_iterator.next().unwrap().sequencer_client);
let height_at_config = a.get_last_block_id().await?;
let indexer = ctx.indexer_client();
wait_for_height(seq_client_a, 2, "sequencer A to produce past genesis").await?;
log::info!("Passed wait for height A to be at least 2");
let height_at_config = seq_client_a.get_last_block_id().await?;
wait_for_height(
&a,
seq_client_a,
height_at_config + 1,
"A to produce after the roster change",
)
.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())?;
log::info!(
"Passed wait for height A to be at least {}",
height_at_config + 1
);
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?;
let join_height = seq_client_a.get_last_block_id().await?;
wait_for_height(seq_client_b, join_height, "B to sync to A's height at join").await?;
log::info!("Passed wait for height B to be at least {join_height}");
// Phase 4: rotation + convergence over ≈4 turn windows.
let rotation_target = join_height + ROTATION_BLOCKS;
wait_for_height(
&a,
seq_client_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?;
log::info!("Passed wait for height A to be at least {rotation_target}");
wait_for_height(
seq_client_b,
rotation_target,
"B to follow across turn windows",
)
.await?;
assert_same_chain(seq_client_a, seq_client_b).await?;
log::info!("Passed wait for height B to be at least {rotation_target}");
// Phase 5: a tx submitted only to B is included by B and visible on A.
let accounts = initial_public_user_accounts();
@@ -121,8 +105,8 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
let to = accounts[1].account_id;
let sign_key = initial_pub_accounts_private_keys()[0].pub_sign_key.clone();
let to_balance_before = a.get_account_balance(to).await?;
let nonce = b.get_accounts_nonces(vec![from]).await?[0];
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,
@@ -130,21 +114,30 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
TRANSFER_AMOUNT,
&sign_key,
);
b.send_transaction(tx)
seq_client_b
.send_transaction(tx)
.await
.context("Failed to submit the transfer to B")?;
wait_for_balance(&a, to, to_balance_before + TRANSFER_AMOUNT).await?;
wait_for_balance(seq_client_a, to, to_balance_before + TRANSFER_AMOUNT).await?;
log::info!(
"Passed wait for height balance {to} to be {}",
to_balance_before + TRANSFER_AMOUNT
);
// Phase 6: the indexer finalizes the same chain, with no stall.
wait_for_finalized(&indexer, join_height).await?;
wait_for_finalized(indexer, join_height).await?;
log::info!("Passed indexer to see finalized {join_height}");
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
for id in 1..=finalized {
let block_i = indexer
.get_block_by_id(id)
.await?
.with_context(|| format!("Indexer is missing finalized block {id}"))?;
let block_a = a
let block_a = seq_client_a
.get_block(id)
.await?
.with_context(|| format!("A is missing block {id}"))?;
@@ -165,6 +158,8 @@ async fn multi_sequencer_committee_converges() -> Result<()> {
/// 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 {
@@ -184,6 +179,8 @@ async fn wait_for_balance(
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 {
@@ -199,6 +196,8 @@ async fn wait_for_balance(
/// Polls the indexer until its finalized height reaches `target`.
async fn wait_for_finalized(indexer: &IndexerClient, target: u64) -> Result<()> {
log::info!("Waiting for indexer to see target finalized, target is {target}");
let wait = async {
loop {
if indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= target {
+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(())
}
+17 -11
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;
@@ -58,7 +60,7 @@ async fn deploy_and_execute_program() -> Result<()> {
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.
{
+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
);
+28 -16
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,7 +261,8 @@ 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(
@@ -268,7 +275,8 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
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}"
+69 -54
View File
@@ -27,7 +27,6 @@ use lee::{
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
};
use lee_core::program::DEFAULT_PROGRAM_ID;
use log::info;
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 {
@@ -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 {
@@ -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,
@@ -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
@@ -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
@@ -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,
73, 227, 101, 209, 210, 127, 85, 171, 217, 20, 52, 68, 63, 127, 88, 157, 162, 165, 221,
15, 86, 162, 128, 15, 56, 89, 95, 33, 216, 229, 181, 123,
]);
// 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 _;
@@ -32,11 +34,13 @@ impl ChildKeysPrivate {
#[must_use]
pub fn nth_child(&self, cci: u32) -> Self {
const DOMAIN: &[u8; 21] = b"/LEE/v0.3/Keys/Parent";
// `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();
@@ -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,
},
},
@@ -130,95 +134,104 @@ mod tests {
111, 13, 5, 195, 75, 20, 255, 162, 85, 40, 251, 8, 168,
];
let expected_ask = lee_core::AuthorizationSecretKey([
3, 154, 34, 187, 166, 138, 64, 10, 172, 210, 224, 75, 165, 157, 94, 27, 81, 209, 194,
189, 60, 171, 252, 226, 25, 136, 158, 59, 56, 39, 60, 175,
]);
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,
227, 13, 41, 248, 160, 185, 37, 158, 48, 134, 157, 185, 50, 249, 13, 114, 128, 43, 92,
148, 161, 91, 158, 206, 209, 246, 46, 49, 114, 165, 72, 64,
];
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,
42, 60, 83, 112, 244, 198, 238, 159, 150, 105, 13, 134, 103, 228, 213, 247, 121, 42,
65, 51, 122, 196, 228, 163, 244, 251, 219, 119, 8, 14, 68, 16,
]);
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,
92, 182, 50, 80, 228, 152, 149, 83, 44, 33, 179, 59, 237, 153, 45, 46, 216, 142,
62, 31, 28, 18, 44, 27, 130, 54, 10, 13, 148, 111, 214, 107,
],
[
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,
135, 73, 174, 183, 171, 136, 40, 174, 28, 18, 73, 1, 183, 13, 208, 39, 113, 79,
136, 163, 234, 119, 117, 192, 103, 49, 193, 16, 188, 111, 4, 78,
],
);
// 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,
95, 42, 170, 49, 164, 173, 200, 156, 66, 32, 71, 126, 122, 140, 148, 144, 114, 143,
233, 199, 104, 82, 179, 49, 43, 114, 130, 182, 71, 4, 45, 101, 65, 136, 196, 72, 129,
128, 204, 239, 137, 84, 230, 210, 18, 214, 252, 40, 198, 210, 24, 158, 53, 151, 166,
24, 47, 143, 8, 158, 119, 240, 204, 210, 242, 96, 191, 147, 106, 98, 198, 93, 193, 163,
31, 132, 36, 16, 50, 83, 24, 225, 250, 106, 55, 231, 188, 90, 194, 128, 10, 225, 186,
41, 225, 165, 126, 57, 32, 163, 129, 42, 68, 113, 177, 239, 106, 144, 217, 188, 192,
174, 38, 161, 189, 24, 107, 14, 54, 167, 221, 120, 194, 6, 22, 163, 86, 96, 47, 220,
227, 176, 173, 52, 150, 183, 25, 40, 200, 19, 134, 51, 172, 126, 35, 147, 79, 207, 235,
9, 243, 197, 84, 4, 194, 142, 207, 118, 121, 133, 58, 12, 58, 226, 22, 106, 172, 56,
223, 161, 145, 60, 28, 47, 95, 84, 127, 1, 235, 72, 0, 131, 202, 15, 151, 93, 52, 18,
13, 247, 91, 80, 240, 229, 85, 72, 135, 84, 230, 113, 196, 162, 3, 24, 87, 176, 80,
202, 99, 44, 87, 229, 96, 254, 27, 181, 181, 58, 191, 116, 19, 68, 235, 35, 86, 227,
89, 49, 70, 102, 54, 153, 224, 117, 34, 113, 57, 121, 202, 42, 248, 24, 125, 134, 134,
57, 126, 204, 131, 191, 181, 71, 197, 184, 137, 48, 76, 29, 174, 137, 154, 253, 50, 68,
184, 122, 173, 106, 144, 207, 48, 213, 156, 182, 26, 103, 203, 133, 131, 47, 184, 189,
109, 4, 182, 126, 71, 180, 153, 18, 82, 77, 201, 23, 176, 92, 12, 146, 48, 26, 236,
139, 157, 174, 214, 77, 253, 163, 94, 52, 133, 88, 200, 251, 156, 197, 201, 7, 239,
117, 83, 57, 188, 85, 31, 196, 106, 164, 147, 36, 32, 241, 143, 54, 121, 195, 183, 98,
182, 135, 90, 84, 118, 212, 91, 115, 41, 75, 193, 156, 44, 9, 196, 199, 241, 123, 148,
31, 105, 126, 160, 234, 16, 196, 149, 192, 66, 34, 199, 132, 160, 98, 229, 90, 158, 46,
108, 112, 126, 165, 115, 234, 128, 164, 241, 132, 171, 186, 212, 121, 74, 217, 165,
111, 216, 21, 169, 89, 86, 173, 163, 183, 61, 28, 117, 104, 211, 206, 30, 194, 180, 34,
180, 151, 150, 212, 90, 75, 139, 138, 253, 52, 60, 252, 5, 126, 152, 12, 153, 77, 232,
167, 14, 163, 130, 76, 18, 117, 96, 113, 144, 234, 22, 56, 106, 210, 78, 83, 50, 43,
99, 120, 20, 172, 89, 61, 10, 75, 121, 118, 226, 153, 53, 161, 144, 53, 246, 37, 213,
216, 48, 183, 124, 58, 161, 145, 126, 238, 120, 112, 103, 65, 176, 40, 104, 60, 47, 10,
138, 154, 89, 174, 164, 69, 182, 168, 196, 131, 68, 18, 189, 204, 74, 180, 16, 233,
178, 175, 57, 180, 212, 58, 148, 92, 2, 16, 255, 103, 27, 212, 117, 12, 10, 54, 105,
253, 9, 124, 250, 210, 14, 127, 151, 74, 49, 209, 59, 125, 184, 183, 175, 251, 200,
172, 120, 59, 41, 89, 199, 3, 161, 189, 138, 50, 69, 108, 102, 155, 210, 17, 73, 235,
75, 145, 132, 67, 89, 88, 225, 182, 156, 248, 199, 112, 52, 22, 134, 80, 40, 250, 42,
185, 57, 200, 90, 137, 16, 158, 98, 114, 48, 151, 35, 128, 49, 49, 118, 195, 57, 40,
94, 103, 156, 186, 1, 112, 130, 178, 59, 22, 71, 153, 173, 195, 178, 216, 149, 24, 202,
245, 123, 117, 106, 44, 55, 128, 37, 165, 26, 103, 158, 52, 10, 188, 10, 195, 146, 204,
85, 66, 66, 162, 73, 25, 59, 107, 57, 149, 100, 216, 24, 69, 49, 134, 233, 96, 29, 176,
8, 188, 121, 145, 44, 35, 199, 4, 48, 24, 76, 69, 250, 92, 126, 40, 52, 162, 72, 113,
81, 96, 116, 105, 150, 59, 211, 236, 141, 87, 178, 9, 17, 117, 43, 139, 17, 150, 153,
114, 195, 212, 2, 192, 56, 91, 70, 200, 75, 2, 57, 171, 147, 184, 236, 15, 64, 26, 191,
131, 179, 13, 195, 195, 166, 208, 180, 93, 186, 155, 102, 189, 57, 82, 73, 39, 44, 249,
249, 183, 33, 112, 59, 130, 20, 193, 41, 40, 128, 131, 106, 136, 51, 75, 56, 188, 167,
119, 5, 118, 73, 84, 168, 38, 121, 182, 190, 252, 182, 87, 142, 33, 66, 131, 75, 36,
216, 181, 186, 213, 148, 191, 182, 115, 159, 83, 1, 14, 170, 55, 21, 251, 65, 135, 117,
171, 147, 38, 210, 129, 251, 151, 177, 213, 1, 18, 22, 241, 62, 173, 80, 76, 85, 129,
139, 192, 137, 205, 203, 114, 181, 121, 40, 141, 9, 194, 58, 20, 200, 126, 151, 51,
129, 146, 92, 156, 93, 192, 72, 26, 33, 138, 107, 138, 124, 193, 138, 8, 244, 84, 116,
28, 156, 123, 1, 19, 186, 119, 231, 157, 70, 160, 5, 34, 80, 201, 4, 39, 38, 217, 85,
53, 10, 40, 136, 145, 225, 26, 65, 32, 76, 33, 245, 72, 166, 5, 165, 44, 67, 86, 99,
87, 9, 148, 131, 72, 223, 71, 179, 243, 39, 36, 34, 145, 86, 134, 12, 127, 103, 3, 191,
254, 216, 195, 12, 197, 184, 238, 67, 34, 226, 4, 100, 135, 165, 40, 164, 113, 110,
132, 68, 100, 72, 217, 67, 169, 199, 96, 120, 152, 27, 26, 241, 103, 61, 162, 154, 113,
55, 75, 156, 17, 114, 105, 145, 158, 13, 251, 50, 221, 219, 150, 88, 5, 184, 92, 137,
164, 25, 117, 51, 87, 233, 93, 5, 84, 125, 251, 162, 110, 231, 36, 2, 235, 251, 185,
45, 180, 132, 53, 104, 206, 144, 133, 67, 164, 76, 84, 152, 236, 157, 253, 115, 97,
195, 177, 172, 233, 51, 161, 196, 66, 59, 233, 88, 133, 12, 146, 172, 148, 236, 58, 5,
226, 48, 53, 219, 185, 72, 86, 7, 249, 151, 205, 32, 57, 163, 17, 71, 37, 162, 97, 137,
142, 252, 190, 58, 196, 70, 181, 4, 48, 123, 9, 75, 198, 100, 134, 36, 18, 45, 99, 18,
191, 75, 55, 30, 144, 197, 0, 44, 71, 199, 78, 121, 92, 76, 84, 43, 133, 139, 77, 105,
83, 178, 221, 215, 108, 55, 58, 7, 106, 96, 146, 9, 70, 140, 250, 187, 206, 95, 54, 74,
30, 146, 15, 182, 5, 79, 41, 135, 59, 75, 103, 82, 63, 39, 69, 178, 215, 49, 234, 146,
127, 186, 192, 189, 107, 140, 11, 39, 162, 120, 90, 133, 106, 184, 87, 144, 5, 80, 80,
22, 241, 181, 128, 201, 61, 186, 124, 9, 165, 192, 78, 67, 141, 57, 10, 94, 36, 75,
118, 21, 105, 252, 45, 196, 60, 23, 182, 189, 252, 152, 182, 72, 229, 213, 89, 165,
222, 151, 52, 182, 110, 127, 158,
];
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 +243,120 @@ 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,
109, 21, 107, 97, 112, 105, 143, 134, 185, 35, 168, 205, 138, 110, 125, 155, 193, 57,
36, 19, 214, 180, 194, 46, 107, 235, 43, 80, 132, 19, 254, 231,
]);
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,
64, 218, 41, 59, 115, 126, 128, 3, 77, 77, 54, 84, 87, 253, 181, 112, 244, 254, 176,
243, 86, 127, 219, 255, 64, 164, 218, 129, 83, 65, 176, 179,
];
let expected_ask = lee_core::AuthorizationSecretKey([
33, 29, 199, 63, 191, 14, 196, 15, 51, 70, 236, 125, 93, 120, 78, 99, 90, 239, 220,
168, 226, 46, 208, 238, 70, 117, 17, 28, 163, 89, 177, 129,
]);
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,
97, 83, 94, 170, 125, 55, 27, 105, 106, 42, 73, 99, 169, 221, 210, 124, 117, 52, 98,
131, 98, 202, 79, 95, 151, 196, 239, 242, 6, 127, 160, 160,
];
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,
81, 199, 141, 154, 209, 135, 105, 75, 106, 240, 124, 190, 245, 233, 152, 34, 225, 234,
212, 221, 121, 68, 255, 97, 231, 142, 26, 54, 169, 53, 69, 242,
]);
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,
14, 173, 255, 235, 8, 1, 246, 119, 243, 18, 235, 31, 209, 92, 142, 7, 175, 223,
228, 201, 173, 165, 148, 137, 141, 160, 175, 161, 110, 139, 54, 183,
],
[
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,
10, 253, 101, 172, 213, 221, 88, 85, 178, 89, 218, 73, 28, 212, 1, 2, 105, 161,
180, 24, 49, 6, 182, 155, 22, 220, 121, 42, 175, 59, 233, 109,
],
);
// 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,
153, 195, 136, 113, 42, 34, 28, 116, 169, 190, 53, 81, 59, 6, 163, 170, 199, 98, 201,
54, 24, 171, 26, 191, 144, 248, 95, 63, 199, 113, 27, 67, 97, 49, 210, 66, 190, 74, 54,
252, 12, 143, 197, 243, 27, 111, 198, 43, 23, 146, 3, 183, 171, 41, 72, 92, 46, 112,
184, 160, 0, 25, 155, 109, 203, 70, 196, 88, 135, 121, 147, 12, 96, 230, 205, 217, 66,
55, 78, 215, 196, 6, 8, 160, 186, 73, 56, 218, 76, 10, 31, 148, 182, 170, 131, 26, 101,
210, 125, 125, 242, 160, 146, 35, 195, 59, 182, 55, 130, 198, 67, 109, 245, 66, 229,
226, 154, 189, 112, 5, 160, 39, 7, 139, 152, 58, 197, 167, 81, 160, 37, 96, 175, 250,
78, 232, 181, 152, 131, 41, 92, 45, 33, 184, 142, 25, 157, 62, 166, 111, 148, 137, 143,
241, 176, 74, 176, 243, 178, 26, 232, 204, 119, 181, 17, 128, 91, 134, 8, 90, 35, 11,
124, 98, 76, 76, 43, 82, 135, 85, 215, 72, 177, 37, 218, 131, 158, 139, 59, 155, 104,
13, 13, 150, 34, 192, 54, 115, 90, 44, 84, 132, 51, 174, 155, 64, 26, 42, 246, 28, 30,
252, 136, 227, 69, 174, 27, 233, 56, 214, 102, 117, 141, 177, 133, 181, 177, 8, 150,
85, 104, 243, 123, 22, 25, 7, 42, 152, 226, 88, 93, 213, 201, 197, 235, 17, 185, 84,
57, 161, 179, 37, 251, 129, 62, 214, 48, 161, 247, 124, 147, 120, 180, 197, 9, 83, 63,
38, 182, 206, 20, 116, 28, 143, 134, 173, 55, 54, 1, 254, 86, 9, 67, 213, 144, 95, 48,
29, 47, 119, 156, 188, 70, 18, 37, 202, 65, 173, 22, 177, 14, 244, 120, 166, 20, 82,
203, 162, 24, 56, 240, 154, 249, 11, 31, 182, 55, 71, 171, 196, 3, 20, 151, 112, 159,
131, 91, 201, 80, 116, 228, 6, 184, 140, 55, 205, 4, 91, 116, 46, 224, 32, 216, 146,
46, 232, 75, 156, 109, 54, 193, 146, 248, 2, 49, 17, 102, 214, 150, 202, 1, 6, 110, 2,
202, 34, 162, 107, 205, 241, 123, 83, 108, 152, 145, 231, 219, 158, 24, 171, 48, 121,
2, 193, 15, 21, 9, 173, 172, 30, 111, 58, 94, 0, 18, 1, 140, 7, 52, 209, 214, 59, 127,
228, 30, 154, 102, 119, 233, 170, 160, 120, 108, 154, 117, 28, 23, 70, 56, 35, 11, 9,
39, 198, 86, 79, 119, 213, 88, 193, 5, 15, 214, 139, 199, 236, 243, 159, 171, 185, 22,
35, 216, 20, 149, 137, 17, 74, 117, 165, 55, 116, 139, 171, 181, 204, 196, 104, 178,
246, 103, 73, 249, 165, 108, 237, 234, 155, 133, 35, 139, 98, 136, 51, 220, 98, 181,
40, 151, 80, 58, 242, 84, 206, 242, 199, 145, 156, 160, 61, 116, 100, 77, 89, 184, 213,
119, 25, 4, 107, 26, 23, 87, 101, 85, 68, 115, 221, 171, 170, 123, 246, 26, 51, 108,
13, 101, 217, 17, 0, 103, 110, 15, 136, 70, 255, 12, 197, 89, 225, 158, 25, 116, 32,
53, 169, 122, 4, 32, 108, 30, 181, 180, 142, 250, 141, 217, 8, 33, 111, 89, 48, 196,
90, 158, 207, 213, 165, 97, 196, 21, 193, 114, 199, 167, 123, 95, 118, 101, 107, 130,
161, 186, 67, 27, 27, 161, 192, 157, 252, 98, 131, 186, 124, 64, 224, 199, 126, 47,
230, 66, 92, 97, 38, 0, 35, 197, 178, 219, 121, 240, 149, 197, 18, 165, 39, 3, 137, 58,
8, 227, 152, 124, 247, 103, 85, 150, 194, 140, 108, 96, 151, 57, 117, 182, 177, 47,
168, 112, 184, 214, 42, 48, 65, 3, 30, 179, 187, 137, 179, 122, 98, 210, 3, 7, 102,
235, 3, 145, 107, 200, 72, 4, 153, 135, 43, 202, 155, 170, 174, 148, 161, 37, 43, 219,
16, 229, 65, 45, 225, 49, 126, 168, 215, 93, 153, 179, 49, 108, 51, 131, 227, 82, 134,
77, 28, 175, 237, 136, 49, 137, 164, 197, 224, 42, 85, 250, 48, 109, 196, 70, 12, 236,
65, 121, 254, 3, 37, 183, 201, 124, 246, 130, 174, 89, 7, 81, 235, 147, 148, 40, 147,
174, 204, 99, 22, 150, 133, 195, 22, 108, 119, 59, 21, 104, 37, 251, 61, 92, 82, 9,
122, 73, 48, 97, 82, 52, 170, 10, 79, 134, 17, 8, 247, 96, 108, 186, 230, 6, 138, 90,
16, 36, 56, 45, 179, 121, 179, 17, 1, 174, 106, 4, 170, 81, 185, 60, 47, 201, 26, 67,
199, 160, 52, 60, 63, 12, 82, 114, 122, 87, 166, 141, 168, 101, 177, 129, 197, 127,
226, 166, 159, 168, 108, 218, 112, 31, 246, 4, 73, 134, 155, 197, 192, 8, 71, 135, 163,
9, 122, 183, 127, 156, 60, 178, 124, 135, 129, 7, 185, 105, 232, 250, 199, 173, 80, 18,
15, 118, 183, 3, 105, 56, 189, 107, 157, 44, 101, 103, 60, 99, 69, 187, 220, 146, 116,
202, 85, 115, 219, 87, 241, 209, 39, 14, 184, 154, 236, 211, 31, 117, 106, 24, 68, 233,
161, 49, 229, 105, 179, 58, 171, 215, 194, 178, 6, 243, 155, 2, 148, 202, 45, 145, 118,
210, 233, 112, 208, 186, 157, 150, 68, 148, 94, 184, 49, 69, 36, 16, 33, 89, 36, 188,
156, 122, 117, 124, 48, 83, 99, 153, 72, 148, 163, 14, 116, 15, 162, 169, 50, 10, 244,
155, 136, 105, 76, 118, 209, 144, 130, 88, 102, 85, 234, 137, 222, 195, 37, 93, 169,
179, 227, 195, 162, 203, 242, 19, 175, 252, 19, 197, 230, 97, 174, 167, 72, 10, 169,
184, 26, 249, 41, 49, 199, 54, 205, 183, 178, 241, 5, 35, 142, 227, 196, 245, 2, 91,
201, 6, 41, 62, 214, 158, 48, 39, 106, 189, 134, 27, 86, 2, 83, 27, 170, 30, 140, 103,
7, 87, 65, 87, 163, 117, 188, 132, 38, 15, 116, 251, 23, 157, 113, 156, 165, 96, 41,
112, 103, 3, 81, 200, 3, 166, 137, 98, 14, 86, 183, 200, 161, 100, 212, 199, 206, 167,
114, 15, 235, 131, 103, 77, 103, 188, 254, 5, 30, 172, 183, 174, 176, 252, 9, 29, 90,
78, 178, 188, 101, 158, 74, 200, 231, 75, 89, 233, 113, 206, 254, 122, 136, 41, 148,
20, 10, 217, 144, 177, 108, 42, 124, 210, 7, 40, 138, 167, 223, 156, 151, 106, 236, 94,
84, 183, 48, 39, 81, 77, 54, 129, 140, 133, 183, 107, 9, 8, 193, 99, 90, 185, 41, 229,
43, 138, 11, 86, 161, 39, 124, 40, 114, 119, 122, 101, 124, 226, 227, 192, 241, 54,
182, 89, 37, 186, 17, 90, 171, 245, 185, 119, 76, 21, 11, 170, 146, 46, 110, 228, 51,
213, 162, 5, 193, 211, 153, 112, 148, 208, 43, 3, 254, 16, 173, 51, 88, 252, 219, 69,
48, 242, 206, 198, 185, 105, 26,
];
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());
@@ -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,41 +89,35 @@ 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; 35] = b"/LEE/v0.3/Keys/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; 29] = b"/LEE/v0.3/Keys/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; 29 + 32 + 4];
bytes[..29].copy_from_slice(DOMAIN);
bytes[29..61].copy_from_slice(&self.0);
bytes[61..].copy_from_slice(&index.to_be_bytes());
let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_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]
+29 -10
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,
@@ -68,12 +68,31 @@ pub fn compute_circuit_output(
match (kind, nullifier) {
(
WitnessKind::Regular,
WitnessKind::Regular { ask },
NullifierWitness::Init { .. } | NullifierWitness::Update { .. },
) => assert!(
pre_state.is_authorized,
"Regular private account pre-state must be authorized"
),
) => {
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,
ask.is_some(),
"Regular private account authorization must match the supplied credential"
);
}
(WitnessKind::Pda { .. }, NullifierWitness::Init { .. }) => assert!(
!pre_state.is_authorized,
"Private PDA init requires unauthorized pre_state"
@@ -126,7 +145,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)
+5 -4
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,8 +48,9 @@ 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.
+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; 31] = b"/LEE/v0.3/Keys/Nullifier/Secret";
let mut bytes = [0_u8; 31 + 32];
bytes[..31].copy_from_slice(DOMAIN);
bytes[31..].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; 31] = b"/LEE/v0.3/Keys/Nullifier/Public";
let mut bytes = [0_u8; 31 + 32];
bytes[..31].copy_from_slice(DOMAIN);
bytes[31..].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 = [
31, 33, 90, 89, 193, 14, 149, 46, 107, 38, 51, 65, 178, 242, 118, 11, 235, 198, 242,
144, 192, 64, 39, 205, 244, 122, 210, 55, 11, 245, 117, 29,
];
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,
58, 181, 207, 24, 227, 133, 192, 231, 242, 216, 230, 219, 31, 227, 236, 94, 99, 245,
206, 251, 237, 189, 88, 218, 215, 106, 66, 227, 136, 152, 140, 218,
]);
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,
226, 149, 99, 147, 82, 211, 97, 152, 31, 46, 87, 113, 237, 244, 197, 108, 71, 191, 161,
199, 140, 177, 247, 73, 95, 64, 202, 90, 8, 157, 188, 147,
]);
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,
44, 36, 222, 50, 57, 159, 215, 6, 246, 54, 45, 150, 94, 148, 148, 71, 212, 113, 165,
10, 187, 162, 184, 70, 96, 35, 42, 230, 251, 72, 237, 80,
]);
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,
50, 122, 67, 122, 59, 36, 150, 204, 128, 54, 55, 152, 14, 220, 163, 211, 246, 221, 197,
75, 12, 199, 163, 151, 234, 194, 188, 13, 27, 120, 249, 220,
]);
let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier);
@@ -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,
@@ -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(),
),
(
@@ -165,7 +167,7 @@ fn prove_privacy_preserving_execution_circuit_fully_private() {
let expected_private_account_1 = Account {
program_owner: program.id(),
balance: 100 - balance_to_move,
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
..Default::default()
};
let expected_private_account_2 = Account {
@@ -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,
@@ -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,
@@ -574,7 +586,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 +627,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 +669,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,7 +698,7 @@ 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 {
@@ -701,10 +719,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,6 +738,215 @@ fn private_authorized_update_encrypts_regular_kind_with_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(),
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_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);
}
/// 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(_))));
}
/// 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]
@@ -733,7 +962,7 @@ fn private_pda_update_encrypts_pda_kind_with_identifier() {
let esk = EphemeralSecretKey::new(
&pda_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 pda_account = Account {
@@ -764,7 +993,7 @@ fn private_pda_update_encrypts_pda_kind_with_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(),
},
}),
@@ -847,7 +1076,7 @@ fn private_pda_update_identifier_mismatch_fails() {
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(),
},
}),
@@ -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![]),
},
})],
+69 -33
View File
@@ -65,10 +65,12 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() {
vpk: sender_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: (0, vec![]),
},
}),
@@ -76,7 +78,9 @@ fn circuit_fails_if_invalid_auth_keys_are_provided() {
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,
@@ -121,10 +125,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid
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![]),
},
}),
@@ -132,7 +138,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provid
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,
@@ -177,10 +185,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_
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![]),
},
}),
@@ -188,7 +198,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_
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,
@@ -233,10 +245,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided(
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![]),
},
}),
@@ -244,7 +258,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided(
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,
@@ -289,10 +305,12 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided
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![]),
},
}),
@@ -300,7 +318,9 @@ fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided
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,
@@ -343,10 +363,12 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu
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![]),
},
}),
@@ -354,7 +376,9 @@ fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_bu
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,
@@ -749,10 +773,12 @@ fn circuit_should_fail_if_there_are_repeated_ids() {
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: (1, vec![]),
},
}),
@@ -760,10 +786,12 @@ fn circuit_should_fail_if_there_are_repeated_ids() {
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: (1, vec![]),
},
}),
@@ -802,9 +830,11 @@ fn private_authorized_uninitialized_account() {
vpk: private_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(private_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: NullifierPublicKey::from(&private_keys.nsk),
npk: NullifierPublicKey::from(&private_keys.nsk()),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
@@ -851,7 +881,9 @@ fn private_unauthorized_uninitialized_account_can_still_be_claimed() {
vpk: private_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(private_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: private_keys.npk(),
commitment_root: DUMMY_COMMITMENT_HASH,
@@ -904,9 +936,11 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
vpk: private_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(private_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: NullifierPublicKey::from(&private_keys.nsk),
npk: NullifierPublicKey::from(&private_keys.nsk()),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
@@ -949,9 +983,11 @@ fn private_account_claimed_then_used_without_init_flag_should_fail() {
vpk: private_keys.vpk(),
random_seed: [0; 32],
identifier: 0,
kind: WitnessKind::Regular,
kind: WitnessKind::Regular {
ask: Some(private_keys.ask),
},
nullifier: NullifierWitness::Init {
npk: NullifierPublicKey::from(&private_keys.nsk),
npk: NullifierPublicKey::from(&private_keys.nsk()),
commitment_root: DUMMY_COMMITMENT_HASH,
},
})],
@@ -1104,7 +1140,7 @@ fn two_private_pda_family_members_receive_and_spend() {
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: alice_keys.nsk,
nsk: alice_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&commitment_pda_0)
.expect("pda_0 must be in state"),
@@ -1143,7 +1179,7 @@ fn two_private_pda_family_members_receive_and_spend() {
kind: WitnessKind::Pda { binding: None },
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: alice_keys.nsk,
nsk: alice_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&commitment_pda_1)
.expect("pda_1 must be in state"),
@@ -1174,7 +1210,7 @@ fn two_private_pda_family_members_receive_and_spend() {
balance: 0,
nonce: alice_pda_1_account
.nonce
.private_account_nonce_increment(&alice_keys.nsk),
.private_account_nonce_increment(&alice_keys.nsk()),
..Account::default()
};
let commitment_pda_1_after_spend =
@@ -1199,7 +1235,7 @@ fn two_private_pda_family_members_receive_and_spend() {
},
nullifier: NullifierWitness::Update {
view_tag: 0,
nsk: alice_keys.nsk,
nsk: alice_keys.nsk(),
membership_proof: state
.get_proof_for_commitment(&commitment_pda_1_after_spend)
.expect("pda_1 after spend must be in state"),
+15 -9
View File
@@ -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,7 +355,7 @@ 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!(
@@ -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"),
+24 -12
View File
@@ -7,8 +7,8 @@
use std::collections::HashMap;
use lee_core::{
BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier,
NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp,
AuthorizationSecretKey, BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity,
Nullifier, NullifierPublicKey, NullifierSecretKey, NullifierWitness, PrivateWitness, Timestamp,
WitnessKind,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey,
@@ -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 {
@@ -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,7 +253,7 @@ 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],
}
@@ -284,7 +288,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 +337,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 +352,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 +402,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"),
@@ -76,7 +76,7 @@ fn transition_from_privacy_preserving_transaction_private() {
&sender_account_id,
&Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk()),
balance: sender_private_account.balance - balance_to_move,
data: Data::default(),
},
@@ -84,7 +84,7 @@ 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,
@@ -211,7 +211,7 @@ fn transition_from_privacy_preserving_transaction_deshielded() {
&sender_account_id,
&Account {
program_owner: crate::test_methods::simple_balance_transfer().id(),
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
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));
@@ -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"),
@@ -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,
@@ -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,
},
}),
@@ -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,
},
}),
+136 -39
View File
@@ -9,12 +9,10 @@
//! own block-reading, emission-extraction, delivery-building, and trust model; a
//! shared trait is best lifted from that first real adapter, not from this one.
use std::collections::BTreeMap;
pub use cross_zone_inbox_core::{CrossZoneConfig, CrossZonePeer};
use cross_zone_inbox_core::{
CrossZoneMessage, InboxConfig, Instruction, ZoneId, inbox_config_account_id,
inbox_seen_shard_account_id,
inbox_seen_shard_account_id, inbox_source_marker_account_id,
};
use lee_core::{
account::{Account, AccountId, Balance},
@@ -31,6 +29,21 @@ pub struct Emission {
pub payload: Vec<u8>,
}
/// Where a delivery came from on the peer chain.
///
/// One struct so the watcher and the verifier fill the same field list: their
/// dispatch transactions for one emission must be byte-identical.
///
/// `src_block_hash` is the recomputed hash on both sides, never the declared
/// `header.hash`, which the signature does not cover.
pub struct EmissionSource {
pub src_zone: ZoneId,
pub src_block_id: u64,
pub src_block_hash: [u8; 32],
pub src_tx_index: u32,
pub src_program_id: ProgramId,
}
/// Whether a program may only be invoked by sequencer-origin transactions.
///
/// The cross-zone inbox is injected solely by the watcher; a user-submitted call
@@ -49,13 +62,18 @@ pub fn is_sequencer_only_program(program_id: ProgramId) -> bool {
#[must_use]
pub fn extract_emission(program_id: ProgramId, instruction_data: &[u32]) -> Option<Emission> {
if program_id == programs::ping_sender().id() {
let ping_core::SenderInstruction::Send {
// Not every transaction to an emitter emits: `InitConfig` is one of its
// instructions, so a non-`Send` decode is an ordinary non-emitting tx.
let Ok(ping_core::SenderInstruction::Send {
target_zone,
target_program_id,
target_accounts,
payload,
..
} = risc0_zkvm::serde::from_slice(instruction_data).ok()?;
}) = risc0_zkvm::serde::from_slice(instruction_data)
else {
return None;
};
Some(Emission {
target_zone,
target_program_id,
@@ -63,13 +81,16 @@ pub fn extract_emission(program_id: ProgramId, instruction_data: &[u32]) -> Opti
payload,
})
} else if program_id == programs::bridge_lock().id() {
let bridge_lock_core::Instruction::Lock {
let Ok(bridge_lock_core::Instruction::Lock {
target_zone,
target_program_id,
target_accounts,
payload,
..
} = risc0_zkvm::serde::from_slice(instruction_data).ok()?;
}) = risc0_zkvm::serde::from_slice(instruction_data)
else {
return None;
};
Some(Emission {
target_zone,
target_program_id,
@@ -88,13 +109,21 @@ fn build_inbox_dispatch_tx(
msg: &CrossZoneMessage,
target_account_ids: Vec<AccountId>,
) -> lee::PublicTransaction {
let mut account_ids = Vec::with_capacity(target_account_ids.len().saturating_add(2));
let mut account_ids = Vec::with_capacity(target_account_ids.len().saturating_add(3));
account_ids.push(inbox_config_account_id(inbox_id));
account_ids.push(inbox_seen_shard_account_id(
inbox_id,
&msg.src_zone,
msg.src_block_id,
));
// Declared here rather than derived by the guest, since a guest cannot
// conjure an account. Both the watcher and the verifier build it through this
// one function, so they cannot disagree about the source a target will see.
account_ids.push(inbox_source_marker_account_id(
inbox_id,
&msg.src_zone,
msg.src_program_id,
));
account_ids.extend(target_account_ids);
let message = lee::public_transaction::Message::try_new(
@@ -118,19 +147,17 @@ fn build_inbox_dispatch_tx(
/// Option B check).
#[must_use]
pub fn build_dispatch_from_emission(
src_zone: ZoneId,
src_block_id: u64,
src_tx_index: u32,
src_program_id: ProgramId,
source: &EmissionSource,
target_program_id: ProgramId,
target_accounts: &[[u8; 32]],
payload: Vec<u8>,
) -> lee::PublicTransaction {
let msg = CrossZoneMessage {
src_zone,
src_block_id,
src_tx_index,
src_program_id,
src_zone: source.src_zone,
src_block_id: source.src_block_id,
src_block_hash: source.src_block_hash,
src_tx_index: source.src_tx_index,
src_program_id: source.src_program_id,
target_program_id,
payload,
l1_inclusion_witness: None,
@@ -143,33 +170,18 @@ pub fn build_dispatch_from_emission(
build_inbox_dispatch_tx(programs::cross_zone_inbox().id(), &msg, target_ids)
}
/// The inbox config a zone derives from its cross-zone config: the per-peer
/// delivery routes plus its own zone id.
fn inbox_config(self_zone: ZoneId, cross_zone: &CrossZoneConfig) -> InboxConfig {
let mut allowed_routes = BTreeMap::new();
for peer in &cross_zone.peers {
allowed_routes.insert(peer.channel_id, peer.allowed_routes.clone());
}
InboxConfig {
self_zone,
allowed_routes,
}
}
/// The genesis transaction that initializes this zone's inbox config PDA.
///
/// Lets the inbox guest authorize inbound peer messages; replaying it seeds the
/// same account on every node, keeping their state consistent.
/// The operator's per-peer routes no longer live here. They are fanned out into
/// each target program's own config, so all the inbox keeps is its zone id.
/// Replaying this seeds the same account on every node.
#[must_use]
pub fn build_inbox_init_config_tx(
self_zone: ZoneId,
cross_zone: &CrossZoneConfig,
) -> lee::PublicTransaction {
pub fn build_inbox_init_config_tx(self_zone: ZoneId) -> lee::PublicTransaction {
let inbox_id = programs::cross_zone_inbox().id();
genesis_public_tx(
inbox_id,
vec![inbox_config_account_id(inbox_id)],
Instruction::InitConfig(inbox_config(self_zone, cross_zone)),
Instruction::InitConfig(InboxConfig { self_zone }),
)
}
@@ -189,19 +201,104 @@ pub fn build_holding_account(holder: AccountId, amount: Balance) -> (AccountId,
}
/// The genesis transaction that pins the cross-zone inbox as the wrapped-token
/// minter, without importing the inbox id into the guest.
/// minter and names the peer sources it may mint for, without importing either id
/// into the guest.
///
/// The sources are the operator's own peer routes aimed at this token, moved from
/// the inbox's allowlist to the token's own config: the same information, enforced
/// by the program that owns the value. A zone with no peers gets an empty list,
/// which authorizes nothing, and the config is still seeded so its PDA cannot be
/// claimed by a first initializer.
#[must_use]
pub fn build_wrapped_token_init_config_tx() -> lee::PublicTransaction {
pub fn build_wrapped_token_init_config_tx(
cross_zone: Option<&CrossZoneConfig>,
) -> lee::PublicTransaction {
let wrapped_token_id = programs::wrapped_token().id();
let sources = cross_zone
.map(|cross_zone| {
cross_zone
.peers
.iter()
.flat_map(|peer| {
peer.allowed_routes
.iter()
.filter(|route| route.target_program_id == wrapped_token_id)
.map(|route| (peer.channel_id, route.src_program_id))
})
.collect()
})
.unwrap_or_default();
genesis_public_tx(
wrapped_token_id,
vec![wrapped_token_core::config_account_id(wrapped_token_id)],
wrapped_token_core::Instruction::InitConfig {
wrapped_token_core::Instruction::InitConfig(wrapped_token_core::WrappedTokenConfig {
minter: programs::cross_zone_inbox().id(),
sources,
}),
)
}
/// The genesis transaction that pins the outbox `ping_sender` chains into,
/// without importing the outbox id into the guest.
#[must_use]
pub fn build_ping_sender_init_config_tx() -> lee::PublicTransaction {
let ping_sender_id = programs::ping_sender().id();
genesis_public_tx(
ping_sender_id,
vec![ping_core::sender_config_account_id(ping_sender_id)],
ping_core::SenderInstruction::InitConfig {
outbox_program_id: programs::cross_zone_outbox().id(),
},
)
}
/// The genesis transaction that pins the outbox `bridge_lock` chains into and the
/// wrapped token it mints, without importing either id into the guest.
#[must_use]
pub fn build_bridge_lock_init_config_tx() -> lee::PublicTransaction {
let bridge_lock_id = programs::bridge_lock().id();
genesis_public_tx(
bridge_lock_id,
vec![bridge_lock_core::config_account_id(bridge_lock_id)],
bridge_lock_core::Instruction::InitConfig {
outbox_program_id: programs::cross_zone_outbox().id(),
target_program_id: programs::wrapped_token().id(),
},
)
}
/// The genesis transaction naming the peer sources `ping_receiver` accepts a
/// delivery from, fanned out of the operator's routes exactly as the wrapped
/// token's is.
#[must_use]
pub fn build_ping_receiver_init_config_tx(
cross_zone: Option<&CrossZoneConfig>,
) -> lee::PublicTransaction {
let receiver_id = programs::ping_receiver().id();
let sources = cross_zone
.map(|cross_zone| {
cross_zone
.peers
.iter()
.flat_map(|peer| {
peer.allowed_routes
.iter()
.filter(|route| route.target_program_id == receiver_id)
.map(|route| (peer.channel_id, route.src_program_id))
})
.collect()
})
.unwrap_or_default();
genesis_public_tx(
receiver_id,
vec![ping_core::receiver_config_account_id(receiver_id)],
ping_core::ReceiverInstruction::InitConfig(ping_core::ReceiverConfig {
deliverer: programs::cross_zone_inbox().id(),
sources,
}),
)
}
/// Builds an unsigned, sequencer-origin genesis transaction invoking `instruction`
/// on `program_id` over `account_ids`.
fn genesis_public_tx<I: Serialize>(
+1 -2
View File
@@ -151,9 +151,8 @@ pub async fn get_transactions_by_account(
#[cfg(feature = "ssr")]
pub fn create_indexer_rpc_client(url: &url::Url) -> Result<IndexerRpcClient, String> {
use jsonrpsee::http_client::HttpClientBuilder;
use log::info;
info!("Connecting to Indexer RPC on URL: {url}");
log::info!("Connecting to Indexer RPC on URL: {url}");
HttpClientBuilder::default()
.build(url.as_str())
+119 -31
View File
@@ -6,13 +6,13 @@ use std::{
use anyhow::anyhow;
use common::{block::Block, transaction::LeeTransaction};
use cross_zone::{build_dispatch_from_emission, extract_emission};
use cross_zone::{EmissionSource, build_dispatch_from_emission, extract_emission};
use cross_zone_inbox_core::{
CrossZoneMessage, Instruction as InboxInstruction, MessageKey, ZoneId, message_key,
};
use futures::{Stream, StreamExt as _};
use lee::{GENESIS_BLOCK_ID, PublicKey};
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use logos_blockchain_core::mantle::ops::channel::ChannelId;
use logos_blockchain_zone_sdk::{
CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
@@ -63,6 +63,11 @@ pub enum CrossZoneVerifyError {
},
}
/// The replay key plus the source block, which is what the inbox treats as one
/// delivery. Skipping re-derivation on the key alone would wave through a
/// dispatch the guest refuses, parking the block and holding ingestion.
type SeenKey = (MessageKey, [u8; 32]);
/// One peer zone's cached blocks, plus how far this reader has read them as an
/// unbroken hash-linked run from the peer's genesis.
#[derive(Default)]
@@ -184,7 +189,7 @@ impl PeerBlocks {
);
return false;
}
info!(
log::info!(
"Peer zone {} block {}: replacing held block {} with {}, which continues the verified run where the held one never could.",
hex::encode(zone),
block.header.block_id,
@@ -278,7 +283,7 @@ pub struct CrossZoneVerifier {
/// optional: a peer with no configured key is not signature-checked.
peer_pubkeys: HashMap<ZoneId, PublicKey>,
peers: PeerBlocks,
seen: Arc<RwLock<HashSet<MessageKey>>>,
seen: Arc<RwLock<HashSet<SeenKey>>>,
}
impl CrossZoneVerifier {
@@ -330,17 +335,14 @@ impl CrossZoneVerifier {
/// forged dispatch reuse it to skip re-derivation while the inbox delivers the
/// forgery. A key already seen is a replay the inbox no-ops, so it is accepted
/// without re-derivation rather than halting on a legitimate re-delivery.
pub async fn verify_block(
&self,
block: &Block,
) -> Result<Vec<MessageKey>, CrossZoneVerifyError> {
pub async fn verify_block(&self, block: &Block) -> Result<Vec<SeenKey>, CrossZoneVerifyError> {
let mut verified = Vec::new();
for tx in &block.body.transactions {
let Some(msg) = Self::decode_dispatch(tx) else {
continue;
};
let key = message_key(&msg.src_zone, msg.src_block_id, msg.src_tx_index);
let key = seen_key(&msg);
if self.seen.read().await.contains(&key) {
debug!(
"Skipping already-seen cross-zone dispatch from zone {} block {} tx {} (replay no-op)",
@@ -361,7 +363,7 @@ impl CrossZoneVerifier {
)));
}
info!(
log::info!(
"Verified cross-zone dispatch from zone {} block {} tx {}",
hex::encode(msg.src_zone),
msg.src_block_id,
@@ -375,7 +377,7 @@ impl CrossZoneVerifier {
/// Marks the given dispatch keys seen, so a later replay of them is accepted
/// without re-derivation. Call only after the block that carried them has been
/// applied on chain (see [`Self::verify_block`]).
pub async fn record_seen(&self, keys: Vec<MessageKey>) {
pub async fn record_seen(&self, keys: Vec<SeenKey>) {
if keys.is_empty() {
return;
}
@@ -455,11 +457,16 @@ impl CrossZoneVerifier {
)));
}
// Recomputed rather than read from `msg`, which would make the field
// attest to itself.
Ok(build_dispatch_from_emission(
msg.src_zone,
msg.src_block_id,
msg.src_tx_index,
message.program_id,
&EmissionSource {
src_zone: msg.src_zone,
src_block_id: msg.src_block_id,
src_block_hash: peer_block.recompute_hash().0,
src_tx_index: msg.src_tx_index,
src_program_id: message.program_id,
},
emission.target_program_id,
&emission.target_accounts,
emission.payload,
@@ -506,7 +513,7 @@ impl CrossZoneVerifier {
});
}
if !waited.is_zero() && waited.as_secs().is_multiple_of(LAG_LOG_INTERVAL.as_secs()) {
info!(
log::info!(
"Waiting for peer zone {} to finalize block {} ({}s); reader is behind",
hex::encode(zone),
block_id,
@@ -528,6 +535,13 @@ struct PeerPass {
stalled_at: Option<Slot>,
}
fn seen_key(msg: &CrossZoneMessage) -> SeenKey {
(
message_key(&msg.src_zone, msg.src_block_id, msg.src_tx_index),
msg.src_block_hash,
)
}
/// Whether a block read off a peer's channel may enter the cache. The channel
/// authorizes who may write, not what they may claim.
///
@@ -577,7 +591,7 @@ async fn read_peer(
peers: PeerBlocks,
poll_interval: Duration,
) {
info!(
log::info!(
"Cross-zone peer reader started for {}",
hex::encode(peer_zone)
);
@@ -704,7 +718,7 @@ mod tests {
};
use logos_blockchain_core::mantle::ops::channel::{MsgId, inscribe::Inscription};
use logos_blockchain_zone_sdk::ZoneBlock;
use ping_core::{SenderInstruction, ping_record_pda};
use ping_core::{SenderInstruction, ping_record_pda, receiver_config_account_id};
use super::*;
@@ -732,10 +746,12 @@ mod tests {
fn emission(payload: &[u8]) -> LeeTransaction {
let receiver_id = programs::ping_receiver().id();
let send = SenderInstruction::Send {
outbox_program_id: programs::cross_zone_outbox().id(),
target_zone: SELF_ZONE,
target_program_id: receiver_id,
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
target_accounts: vec![
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload: payload.to_vec(),
ordinal: 0,
};
@@ -808,14 +824,34 @@ mod tests {
/// The dispatch a watcher would inject for a `PEER_BLOCK_ID` emission of `payload`.
fn dispatch(payload: &[u8]) -> LeeTransaction {
dispatch_naming_block_hash(payload, source_block_hash(payload))
}
/// The recomputed hash of the `PEER_BLOCK_ID` block carrying `payload`,
/// which is what an honest watcher puts in the dispatch.
fn source_block_hash(payload: &[u8]) -> [u8; 32] {
peer_chain(payload)
.last()
.expect("chain reaches PEER_BLOCK_ID")
.recompute_hash()
.0
}
fn dispatch_naming_block_hash(payload: &[u8], src_block_hash: [u8; 32]) -> LeeTransaction {
let receiver_id = programs::ping_receiver().id();
LeeTransaction::Public(build_dispatch_from_emission(
PEER_ZONE,
PEER_BLOCK_ID,
0,
programs::ping_sender().id(),
&EmissionSource {
src_zone: PEER_ZONE,
src_block_id: PEER_BLOCK_ID,
src_block_hash,
src_tx_index: 0,
src_program_id: programs::ping_sender().id(),
},
receiver_id,
&[ping_record_pda(receiver_id).into_value()],
&[
receiver_config_account_id(receiver_id).into_value(),
ping_record_pda(receiver_id).into_value(),
],
payload.to_vec(),
))
}
@@ -832,6 +868,24 @@ mod tests {
.expect("dispatch matching the peer emission verifies");
}
#[tokio::test]
async fn rejects_dispatch_naming_the_wrong_source_block_hash() {
let verifier = verifier();
cache_chain(&verifier, peer_chain(b"hi")).await;
// Only the claimed source hash is wrong. Detectable because the verifier
// recomputes it from the resolved block instead of reading the field.
let block =
produce_dummy_block(9, None, vec![dispatch_naming_block_hash(b"hi", [0xab; 32])]);
assert!(
matches!(
verifier.verify_block(&block).await,
Err(CrossZoneVerifyError::Forged(_))
),
"a delivery claiming a source block hash the peer block does not have is forged"
);
}
#[tokio::test]
async fn rejects_dispatch_with_no_matching_emission() {
let verifier = verifier();
@@ -892,18 +946,52 @@ mod tests {
// Mark the delivery seen, as the ingest loop does once the block applies.
verifier.record_seen(keys).await;
// A payload that cannot re-derive, under the key just recorded. Accepted
// only by the seen-key short circuit, since the inbox no-ops it on
// chain; `unaccepted_dispatch_does_not_poison_seen` asserts the same
// input is rejected when the key was never recorded, which is what makes
// this one about the short circuit rather than re-derivation.
let replay = produce_dummy_block(10, None, vec![dispatch(b"forged")]);
// A payload that cannot re-derive, under the key just recorded, which
// now names the source block as well as the coordinates. Accepted only
// by the seen-key short circuit, since the inbox no-ops it on chain;
// `unaccepted_dispatch_does_not_poison_seen` asserts the same input is
// rejected when the key was never recorded, which is what makes this one
// about the short circuit rather than re-derivation.
let replay = produce_dummy_block(
10,
None,
vec![dispatch_naming_block_hash(
b"forged",
source_block_hash(b"hi"),
)],
);
verifier
.verify_block(&replay)
.await
.expect("a replay is accepted as an on-chain no-op");
}
#[tokio::test]
async fn a_seen_coordinate_does_not_excuse_a_different_source_block() {
let verifier = verifier();
cache_chain(&verifier, peer_chain(b"hi")).await;
let first = produce_dummy_block(9, None, vec![dispatch(b"hi")]);
let keys = verifier.verify_block(&first).await.expect("first verifies");
verifier.record_seen(keys).await;
// Same coordinates as the delivery just seen, different source block.
// The inbox refuses rather than no-ops it, so skipping re-derivation
// would wave through a dispatch that parks the block.
let other = produce_dummy_block(
10,
None,
vec![dispatch_naming_block_hash(b"hi", [0xab; 32])],
);
assert!(
matches!(
verifier.verify_block(&other).await,
Err(CrossZoneVerifyError::Forged(_))
),
"the seen set must agree with the guest on what counts as a replay"
);
}
#[tokio::test]
async fn unaccepted_dispatch_does_not_poison_seen() {
// A dispatch verified in a block that never applies (e.g. one that parks)
+5 -5
View File
@@ -7,7 +7,7 @@ use chain_state::{Anchor, ChainConsistency};
use common::block::Block;
// TODO: Remove after testnet
use futures::StreamExt as _;
use log::{error, info, warn};
use log::{error, warn};
use logos_blockchain_zone_sdk::{
CommonHttpClient, Slot, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
};
@@ -270,9 +270,9 @@ impl IndexerCore {
let mut retry_gate = ApplyRetryGate::new();
if let Some(slot) = &cursor {
info!("Resuming indexer from cursor {slot:?}");
log::info!("Resuming indexer from cursor {slot:?}");
} else {
info!("Starting indexer from beginning of channel");
log::info!("Starting indexer from beginning of channel");
}
loop {
@@ -372,12 +372,12 @@ impl IndexerCore {
verifier.record_seen(verified_keys).await;
}
retry_gate.reset();
info!("Indexed L2 block {}", block.header.block_id);
log::info!("Indexed L2 block {} at channel {}", block.header.block_id, self.config.channel_id);
self.set_status(IndexerSyncStatus::syncing());
yield Ok(block);
}
Ok(AcceptOutcome::AlreadyApplied) => {
info!(
log::info!(
"Skipping already-applied block {}",
block.header.block_id
);
+2 -2
View File
@@ -4,7 +4,7 @@ use anyhow::{Context as _, Result};
pub use indexer_core::config::*;
use indexer_service_rpc::RpcServer as _;
use jsonrpsee::server::{Server, ServerHandle};
use log::{error, info};
use log::error;
use tokio_util::sync::CancellationToken;
pub mod service;
@@ -84,7 +84,7 @@ pub async fn run_server(
.local_addr()
.context("Failed to get local address of RPC server")?;
info!("Starting Indexer Service RPC server on {addr}");
log::info!("Starting Indexer Service RPC server on {addr}");
#[cfg(not(feature = "mock-responses"))]
let handle = {
+4 -4
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use log::{error, info};
use log::error;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Parser)]
@@ -40,14 +40,14 @@ async fn main() -> Result<()> {
tokio::select! {
() = cancellation_token.cancelled() => {
info!("Shutting down server...");
log::info!("Shutting down server...");
}
() = indexer_handle.stopped() => {
error!("Server stopped unexpectedly");
}
}
info!("Server shutdown complete");
log::info!("Server shutdown complete");
Ok(())
}
@@ -61,7 +61,7 @@ fn listen_for_shutdown_signal() -> CancellationToken {
error!("Failed to listen for Ctrl-C signal: {err}");
return;
}
info!("Received Ctrl-C signal");
log::info!("Received Ctrl-C signal");
cancellation_token_clone.cancel();
});
+5 -5
View File
@@ -12,7 +12,7 @@ use jsonrpsee::{
core::{Serialize, SubscriptionResult, async_trait},
types::{ErrorCode, ErrorObject, ErrorObjectOwned},
};
use log::{debug, error, info, warn};
use log::{debug, error, warn};
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;
@@ -44,7 +44,7 @@ impl indexer_service_rpc::RpcServer for IndexerService {
subscription_sink: jsonrpsee::PendingSubscriptionSink,
) -> SubscriptionResult {
let sink = subscription_sink.accept().await?;
info!(
log::info!(
"Accepted new subscription to finalized blocks with ID {:?}",
sink.subscription_id()
);
@@ -250,14 +250,14 @@ impl SubscriptionService {
loop {
tokio::select! {
() = shutdown.cancelled() => {
info!("Shutdown requested; stopping block ingestion");
log::info!("Shutdown requested; stopping block ingestion");
return Ok(());
}
sub = sub_receiver.recv() => {
let Some(subscription) = sub else {
bail!("Subscription receiver closed unexpectedly");
};
info!("Added new subscription with ID {:?}", subscription.sink.subscription_id());
log::info!("Added new subscription with ID {:?}", subscription.sink.subscription_id());
subscribers.push(subscription);
}
block_opt = block_stream.next() => {
@@ -332,7 +332,7 @@ impl<T> Subscription<T> {
impl<T> Drop for Subscription<T> {
fn drop(&mut self) {
info!(
log::info!(
"Subscription with ID {:?} is being dropped",
self.sink.subscription_id()
);
+3
View File
@@ -10,3 +10,6 @@ workspace = true
[dependencies]
lee_core.workspace = true
serde = { workspace = true, features = ["alloc"] }
[dev-dependencies]
risc0-zkvm.workspace = true
+92 -4
View File
@@ -9,23 +9,41 @@ use lee_core::{
use serde::{Deserialize, Serialize};
const ESCROW_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeLockEscrow/0000/";
const CONFIG_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/BridgeLockCfg/0000000/";
/// Variants are append-only. risc0 serde encodes the variant as a bare leading
/// tag word, so inserting one ahead of `Lock` shifts every existing encoding.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Instruction {
/// Lock `amount` of the holder's balance and emit a cross-zone message
/// minting the wrapped token on `target_zone`. The emission fields mirror
/// `cross_zone_outbox::Instruction::Emit` so the watcher reads them directly.
/// minting the wrapped token on `target_zone`.
///
/// Required accounts (3): holder holding (authorized), escrow PDA, outbox PDA.
/// `target_program_id` and `target_accounts` are supplied though the guest
/// accepts one value for each: `cross_zone::extract_emission` reads them off
/// the transaction, decoding every emitter through one shape.
///
/// `target_zone` is the caller's, so a lock to a zone that will not route it
/// escrows and never mints. TODO: bound it source-side.
///
/// Required accounts (4): config PDA, holder holding (authorized), escrow
/// PDA, outbox PDA.
Lock {
amount: u128,
target_zone: [u8; 32],
target_program_id: ProgramId,
target_accounts: Vec<[u8; 32]>,
payload: Vec<u8>,
outbox_program_id: ProgramId,
ordinal: u32,
},
/// Pins the outbox program and the mint target, written once into a default
/// config PDA at genesis. A re-run naming different programs is refused; an
/// identical one is a no-op, which is what genesis replay does.
///
/// Required accounts (1): the config PDA.
InitConfig {
outbox_program_id: ProgramId,
target_program_id: ProgramId,
},
}
/// PDA accumulating all locked balance on this zone.
@@ -39,6 +57,49 @@ pub const fn escrow_seed() -> PdaSeed {
PdaSeed::new(ESCROW_SEED_DOMAIN)
}
/// PDA holding the outbox program id and the mint target, seeded at genesis so
/// the guest can pin both without importing their image ids.
#[must_use]
pub fn config_account_id(bridge_lock_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&bridge_lock_id, &config_seed())
}
#[must_use]
pub const fn config_seed() -> PdaSeed {
PdaSeed::new(CONFIG_SEED_DOMAIN)
}
/// Encodes the pinned outbox and mint target for the config account's data.
#[must_use]
pub fn config_bytes(outbox_program_id: ProgramId, target_program_id: ProgramId) -> [u8; 64] {
let mut bytes = [0_u8; 64];
for (word, chunk) in outbox_program_id
.iter()
.chain(target_program_id.iter())
.zip(bytes.chunks_exact_mut(4))
{
chunk.copy_from_slice(&word.to_le_bytes());
}
bytes
}
/// Decodes the pinned outbox and mint target from the config account's data.
#[must_use]
pub fn read_config(data: &[u8]) -> Option<(ProgramId, ProgramId)> {
if data.len() < 64 {
return None;
}
let mut ids = [0_u32; 16];
for (word, chunk) in ids.iter_mut().zip(data[..64].chunks_exact(4)) {
*word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!()));
}
let (outbox, target) = ids.split_at(8);
Some((
outbox.try_into().unwrap_or_else(|_| unreachable!()),
target.try_into().unwrap_or_else(|_| unreachable!()),
))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -48,4 +109,31 @@ mod tests {
let id: ProgramId = [4; 8];
assert_eq!(escrow_account_id(id), escrow_account_id(id));
}
#[test]
fn config_ids_round_trip() {
let outbox: ProgramId = [3; 8];
let target: ProgramId = [5; 8];
assert_eq!(
read_config(&config_bytes(outbox, target)),
Some((outbox, target))
);
}
/// `extract_emission` decodes `Lock` off peer transactions, so its tag word is
/// wire format: a variant inserted ahead of it would silently shift every
/// existing encoding.
#[test]
fn lock_is_the_first_variant() {
let lock = Instruction::Lock {
amount: 1,
target_zone: [7; 32],
target_program_id: [1; 8],
target_accounts: vec![],
payload: vec![],
ordinal: 0,
};
let words = risc0_zkvm::serde::to_vec(&lock).expect("Lock serializes");
assert_eq!(words[0], 0);
}
}
+150 -19
View File
@@ -1,10 +1,16 @@
use bridge_lock_core::{Instruction, escrow_account_id, escrow_seed};
use bridge_lock_core::{
Instruction, config_account_id, config_bytes, config_seed, escrow_account_id, escrow_seed,
read_config,
};
use cross_zone_outbox_core::Instruction as OutboxInstruction;
use lee_core::{
account::AccountWithMetadata,
program::{AccountPostState, ChainedCall, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
account::{Account, AccountWithMetadata},
program::{
AccountPostState, ChainedCall, Claim, ProgramId, ProgramInput, ProgramOutput,
read_lee_inputs,
},
};
use wrapped_token_core::Instruction as WrappedInstruction;
use wrapped_token_core::{Instruction as WrappedInstruction, MAX_MINT_AMOUNT};
fn main() {
let (
@@ -22,20 +28,74 @@ fn main() {
"bridge_lock is only invoked as a top-level user transaction"
);
let Instruction::Lock {
amount,
target_zone,
target_program_id,
target_accounts,
payload,
outbox_program_id,
ordinal,
} = instruction;
match instruction {
Instruction::Lock {
amount,
target_zone,
target_program_id,
target_accounts,
payload,
ordinal,
} => lock(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
amount,
target_zone,
target_program_id,
target_accounts,
payload,
ordinal,
),
Instruction::InitConfig {
outbox_program_id,
target_program_id,
} => init_config(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
outbox_program_id,
target_program_id,
),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the emission fields are passed through verbatim"
)]
fn lock(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
amount: u128,
target_zone: [u8; 32],
target_program_id: ProgramId,
target_accounts: Vec<[u8; 32]>,
payload: Vec<u8>,
ordinal: u32,
) {
// pre_states: [config PDA, holder holding (authorized), escrow PDA, outbox PDA].
let [config, holder, escrow, outbox] = <[AccountWithMetadata; 4]>::try_from(pre_states)
.expect("Lock requires config, holder, escrow, and outbox accounts");
// Pinned rather than caller-named: chaining elsewhere would debit the escrow
// and leave no record of what it was for.
assert_eq!(
config.account_id,
config_account_id(self_program_id),
"first account must be the bridge-lock config PDA"
);
let (outbox_program_id, pinned_target) = read_config(&config.account.data.clone().into_inner())
.expect("config account holds an outbox and a mint target");
// Value conservation: the forwarded payload must mint exactly what is locked.
let WrappedInstruction::Mint {
recipient,
amount: mint_amount,
..
} = decode_mint(&payload)
else {
panic!("bridge_lock payload must be a wrapped-token mint");
@@ -45,9 +105,25 @@ fn main() {
"locked amount must equal the wrapped mint amount"
);
// pre_states: [holder holding (authorized), escrow PDA, outbox PDA].
let [holder, escrow, outbox] = <[AccountWithMetadata; 3]>::try_from(pre_states)
.expect("Lock requires holder, escrow, and outbox accounts");
// All before the debit: nothing releases an escrow, so a message the
// destination refuses is a burn. `target_zone` is not checkable here, so a
// lock aimed at a zone that will not route it still burns.
assert_eq!(
target_program_id, pinned_target,
"bridge_lock only mints through the wrapped token it is pinned to"
);
assert_eq!(
target_accounts,
vec![
wrapped_token_core::config_account_id(pinned_target).into_value(),
wrapped_token_core::holding_account_id(pinned_target, &recipient).into_value(),
],
"target accounts must be the mint's config and the recipient's holding"
);
assert!(
amount <= MAX_MINT_AMOUNT,
"locked amount exceeds what the wrapped token will mint"
);
assert!(holder.is_authorized, "holder must authorize the lock");
// The holder holding is bridge_lock-owned, so bridge_lock may debit its native
@@ -61,7 +137,7 @@ fn main() {
assert_eq!(
escrow.account_id,
escrow_account_id(self_program_id),
"second account must be the escrow PDA"
"third account must be the escrow PDA"
);
// Move the real native balance holder -> escrow. bridge_lock owns both accounts,
@@ -99,12 +175,15 @@ fn main() {
},
);
let config_post = AccountPostState::new(config.account.clone());
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![holder, escrow, outbox.clone()],
vec![config, holder, escrow, outbox.clone()],
vec![
config_post,
holder_post,
escrow_post,
AccountPostState::new(outbox.account),
@@ -114,6 +193,58 @@ fn main() {
.write();
}
/// Writes the outbox program and the mint target into the config PDA exactly once
/// at genesis.
fn init_config(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
outbox_program_id: ProgramId,
target_program_id: ProgramId,
) {
// pre_states: [config PDA].
let [config] = <[AccountWithMetadata; 1]>::try_from(pre_states)
.expect("InitConfig requires the config account");
assert_eq!(
config.account_id,
config_account_id(self_program_id),
"account must be the bridge-lock config PDA"
);
// Init-once, idempotent under genesis replay: a `default` config is a first
// init; an already-owned one must already pin exactly these programs, since
// genesis is replayed onto seeded state during multi-sequencer reconstruction.
// `new_claimed_if_default` alone would not stop a later self-owned rewrite.
if config.account != Account::default() {
assert_eq!(
config.account.program_owner, self_program_id,
"bridge-lock config PDA is owned by another program"
);
assert_eq!(
config.account.data.clone().into_inner(),
config_bytes(outbox_program_id, target_program_id).to_vec(),
"bridge-lock config already pins a different outbox or mint target"
);
}
let mut config_account = config.account.clone();
config_account.data = config_bytes(outbox_program_id, target_program_id)
.to_vec()
.try_into()
.expect("pinned ids fit in account data");
let config_post =
AccountPostState::new_claimed_if_default(config_account, Claim::Pda(config_seed()));
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![config],
vec![config_post],
)
.write();
}
/// Decodes the cross-zone payload (risc0 words, little-endian bytes) into the
/// wrapped-token instruction it carries.
fn decode_mint(payload: &[u8]) -> WrappedInstruction {
+195 -115
View File
@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
@@ -7,12 +7,13 @@ use lee_core::{
};
use serde::{Deserialize, Serialize};
/// Source blocks per seen-set shard, so no single seen account grows without bound.
pub const EPOCH_BLOCKS: u64 = 10_000;
const MESSAGE_KEY_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneMsgKey/00000/";
const INBOX_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/CrossZoneInboxCfg/000/";
const INBOX_SEEN_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneInboxSeen/00/";
/// `/01/` because `/00/` keyed shards by epoch: an epoch and a block id are
/// indistinguishable under one domain. Belt and braces, since the image id
/// already relocates every PDA in this crate whenever the crate changes.
const INBOX_SEEN_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneInboxSeen/01/";
const SOURCE_MARKER_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneSource/00000/";
/// Raw 32-byte zone (channel) id; the host maps it to the zone-sdk `ChannelId`.
pub type ZoneId = [u8; 32];
@@ -69,6 +70,13 @@ pub struct CrossZoneConfig {
pub struct CrossZoneMessage {
pub src_zone: ZoneId,
pub src_block_id: u64,
/// The source block's recomputed hash, never the `header.hash` it declares.
///
/// The signature does not cover that field, so a correctly signed block can
/// carry a bogus one. Both the watcher and the verifier hash the block's
/// contents themselves and fill this from that, so the two agree on it
/// without either trusting what the peer wrote.
pub src_block_hash: [u8; 32],
pub src_tx_index: u32,
pub src_program_id: ProgramId,
pub target_program_id: ProgramId,
@@ -77,32 +85,20 @@ pub struct CrossZoneMessage {
pub l1_inclusion_witness: Option<Vec<u8>>,
}
/// Per-peer delivery routes, plus this inbox's own zone id.
/// This inbox's own zone id.
///
/// It no longer decides who may deliver what. Each target program authorizes its
/// own sources against the marker the inbox passes, so the only thing the inbox
/// still needs to know is which zone it is, to refuse a message addressed to
/// itself.
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
pub struct InboxConfig {
pub self_zone: ZoneId,
/// Which deliveries each peer may make. A peer absent from this map may
/// deliver nothing.
pub allowed_routes: BTreeMap<ZoneId, Vec<CrossZoneRoute>>,
}
impl InboxConfig {
/// Whether `src_zone` may deliver from `src_program_id` to
/// `target_program_id`. A peer with no routes may deliver nothing.
#[must_use]
pub fn permits(
&self,
src_zone: &ZoneId,
src_program_id: ProgramId,
target_program_id: ProgramId,
) -> bool {
self.allowed_routes
.get(src_zone)
.is_some_and(|routes| routes_permit(routes, src_program_id, target_program_id))
}
/// Borsh-encoded form stored in the inbox config account.
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
@@ -115,12 +111,37 @@ impl InboxConfig {
}
}
/// The replay keys seen for one `(src_zone, epoch)` shard.
/// What one peer block has already delivered.
///
/// Indices, not message keys: the shard's address already binds
/// `(src_zone, src_block_id)`, so a key stored inside it adds nothing.
///
/// A shard costs an account plus a 36-byte header and breaks even against a
/// shared shard at about five deliveries. What that buys is saturation
/// resistance: at 32 bytes per delivery one peer block could overflow the
/// account, and the guest's only answer is a panic that costs the message.
#[derive(Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct SeenShard(pub BTreeSet<MessageKey>);
pub struct SeenShard {
/// Recomputed hash of the peer block this shard records deliveries from.
/// All-zero until the first delivery claims it.
pub src_block_hash: [u8; 32],
/// Indices of that block's transactions already delivered.
pub delivered: BTreeSet<u32>,
}
impl SeenShard {
/// Decodes a shard from account data; empty data is an empty shard.
/// Deliveries one shard can hold before it exceeds `DATA_MAX_LENGTH`.
///
/// Borsh is 32 bytes of hash, a 4-byte count, then 4 bytes per index, so
/// this is exactly the 100 KiB an account may carry.
///
/// Out of reach only because of the L1 inscription cap: a block inscribes as
/// one op near 1.75 MiB and a minimal emitting transaction is about 257
/// bytes, capping a peer block near 7,100 deliveries. Raising that L1 cap
/// past roughly 6.3 MiB puts this back in reach.
pub const MAX_DELIVERIES: usize = 25_591;
/// Decodes a shard from account data; empty data is an unclaimed shard.
pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result<Self> {
if bytes.is_empty() {
return Ok(Self::default());
@@ -133,14 +154,32 @@ impl SeenShard {
borsh::to_vec(self).expect("SeenShard serializes")
}
/// Whether a delivery from the block with this hash may be recorded here.
///
/// An unclaimed shard binds to its first claimant. Unclaimed is the whole
/// value being default, not the hash being zero, so a shard holding any
/// delivery can never read as unclaimed.
#[must_use]
pub fn contains(&self, key: &MessageKey) -> bool {
self.0.contains(key)
pub fn binds(&self, src_block_hash: &[u8; 32]) -> bool {
*self == Self::default() || self.src_block_hash == *src_block_hash
}
/// Inserts a key; returns true if it was newly inserted.
pub fn insert(&mut self, key: MessageKey) -> bool {
self.0.insert(key)
#[must_use]
pub fn contains(&self, src_tx_index: u32) -> bool {
self.delivered.contains(&src_tx_index)
}
/// Binds the shard if unclaimed and records the delivery; true if new.
///
/// A non-binding hash records nothing. The guest already asserts
/// [`Self::binds`], so this is a backstop against a future caller rebinding
/// a claimed shard and erasing which peer block delivered what.
pub fn insert(&mut self, src_block_hash: [u8; 32], src_tx_index: u32) -> bool {
if !self.binds(&src_block_hash) {
return false;
}
self.src_block_hash = src_block_hash;
self.delivered.insert(src_tx_index)
}
}
@@ -154,25 +193,6 @@ pub enum Instruction {
InitConfig(InboxConfig),
}
/// Whether `routes` authorize a delivery from `src_program_id` to
/// `target_program_id`.
///
/// The one place the rule lives. The inbox guest decides with it and the
/// sequencer's watcher drops unroutable messages with it, and those two must
/// agree: a watcher stricter than the guest loses messages silently, and one
/// looser records deliveries the guest will refuse, which production then feeds
/// in and gives up on.
#[must_use]
pub fn routes_permit(
routes: &[CrossZoneRoute],
src_program_id: ProgramId,
target_program_id: ProgramId,
) -> bool {
routes.iter().any(|route| {
route.src_program_id == src_program_id && route.target_program_id == target_program_id
})
}
/// Content-addressed replay key for a delivered message.
///
/// Hashes `(src_zone, src_block_id, src_tx_index)` under a domain separator.
@@ -207,7 +227,7 @@ pub const fn inbox_config_seed() -> PdaSeed {
PdaSeed::new(INBOX_CONFIG_SEED)
}
/// The seen-set shard for the `(src_zone, epoch)` the message falls in.
/// The seen-set shard for the peer block the message came from.
#[must_use]
pub fn inbox_seen_shard_account_id(
inbox_id: ProgramId,
@@ -218,15 +238,59 @@ pub fn inbox_seen_shard_account_id(
}
/// Seed of the seen-shard PDA, exposed so the guest can claim the account.
///
/// One shard per peer block, so a peer cannot accumulate deliveries from many
/// blocks into one account.
#[must_use]
pub fn inbox_seen_shard_seed(src_zone: &ZoneId, src_block_id: u64) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let src_epoch = src_block_id.wrapping_div(EPOCH_BLOCKS);
let mut bytes = [0_u8; 72];
bytes[..32].copy_from_slice(&INBOX_SEEN_SEED_DOMAIN);
bytes[32..64].copy_from_slice(src_zone);
bytes[64..].copy_from_slice(&src_epoch.to_le_bytes());
bytes[64..].copy_from_slice(&src_block_id.to_le_bytes());
let seed: [u8; 32] = Impl::hash_bytes(&bytes)
.as_bytes()
.try_into()
.unwrap_or_else(|_| unreachable!());
PdaSeed::new(seed)
}
/// The account naming who sent a delivery, which the inbox passes at position 0
/// of the chained call so the target can authenticate its own sources.
///
/// Nothing writes or claims it, so the state machine's uninitialized-account rule
/// skips it for being unchanged rather than for being default: anyone may send it
/// balance, and the inbox and the targets all round-trip it untouched.
///
/// The address is derivable by anyone, so it is not a secret and not a
/// capability. What makes it mean something is that a target checks it only after
/// pinning its caller to the inbox, and only the inbox can be that caller.
#[must_use]
pub fn inbox_source_marker_account_id(
inbox_id: ProgramId,
src_zone: &ZoneId,
src_program_id: ProgramId,
) -> AccountId {
AccountId::for_public_pda(
&inbox_id,
&inbox_source_marker_seed(src_zone, src_program_id),
)
}
/// Seed of the source marker, exposed so a target can re-derive the address of
/// the one source it accepts and compare.
#[must_use]
pub fn inbox_source_marker_seed(src_zone: &ZoneId, src_program_id: ProgramId) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let mut bytes = [0_u8; 96];
bytes[..32].copy_from_slice(&SOURCE_MARKER_SEED_DOMAIN);
bytes[32..64].copy_from_slice(src_zone);
for (word, chunk) in src_program_id.iter().zip(bytes[64..].chunks_exact_mut(4)) {
chunk.copy_from_slice(&word.to_le_bytes());
}
let seed: [u8; 32] = Impl::hash_bytes(&bytes)
.as_bytes()
@@ -236,69 +300,14 @@ pub fn inbox_seen_shard_seed(src_zone: &ZoneId, src_block_id: u64) -> PdaSeed {
}
#[cfg(test)]
mod tests {
use lee_core::account::data::DATA_MAX_LENGTH;
use super::*;
fn zone(b: u8) -> ZoneId {
[b; 32]
}
fn program(n: u32) -> ProgramId {
[n; 8]
}
/// The route is the pair. Two entries that are each reasonable on their own,
/// a lock program that may mint and a ping emitter that may reach a
/// receiver, must not compose into the lock program's target being
/// reachable from the ping emitter: that emitter lets its caller choose the
/// target, so it would mint with nothing locked behind it.
#[test]
fn a_route_authorizes_one_pair_and_does_not_compose() {
let lock = program(1);
let wrapped_token = program(2);
let ping_sender = program(3);
let ping_receiver = program(4);
let mut allowed_routes = BTreeMap::new();
allowed_routes.insert(
zone(9),
vec![
CrossZoneRoute {
src_program_id: lock,
target_program_id: wrapped_token,
},
CrossZoneRoute {
src_program_id: ping_sender,
target_program_id: ping_receiver,
},
],
);
let config = InboxConfig {
self_zone: zone(1),
allowed_routes,
};
assert!(config.permits(&zone(9), lock, wrapped_token));
assert!(config.permits(&zone(9), ping_sender, ping_receiver));
assert!(
!config.permits(&zone(9), ping_sender, wrapped_token),
"an emitter whose caller picks the target must not reach the bridge's target"
);
assert!(
!config.permits(&zone(9), lock, ping_receiver),
"a route grants its own target, not every target the peer has"
);
}
#[test]
fn a_peer_with_no_routes_may_deliver_nothing() {
let config = InboxConfig {
self_zone: zone(1),
allowed_routes: BTreeMap::new(),
};
assert!(!config.permits(&zone(9), program(1), program(2)));
}
#[test]
fn message_key_is_stable_and_content_addressed() {
assert_eq!(message_key(&zone(1), 7, 3), message_key(&zone(1), 7, 3));
@@ -308,15 +317,86 @@ mod tests {
}
#[test]
fn seen_shards_split_on_epoch_boundary() {
fn every_peer_block_gets_its_own_seen_shard() {
let id: ProgramId = [9; 8];
assert_eq!(
inbox_seen_shard_account_id(id, &zone(1), 0),
inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS - 1),
inbox_seen_shard_account_id(id, &zone(1), 7),
inbox_seen_shard_account_id(id, &zone(1), 7),
);
assert_ne!(
inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS - 1),
inbox_seen_shard_account_id(id, &zone(1), EPOCH_BLOCKS),
inbox_seen_shard_account_id(id, &zone(1), 7),
inbox_seen_shard_account_id(id, &zone(1), 8),
);
assert_ne!(
inbox_seen_shard_account_id(id, &zone(1), 7),
inbox_seen_shard_account_id(id, &zone(2), 7),
);
}
#[test]
fn a_shard_binds_to_the_first_block_that_claims_it() {
let mut shard = SeenShard::default();
assert!(shard.binds(&[1; 32]), "an unclaimed shard binds to anyone");
assert!(shard.binds(&[2; 32]));
shard.insert([1; 32], 0);
assert!(shard.binds(&[1; 32]), "and to that block thereafter");
assert!(
!shard.binds(&[2; 32]),
"a second block claiming the same block id cannot share this shard"
);
}
#[test]
fn a_shard_records_deliveries_by_transaction_index() {
let mut shard = SeenShard::default();
assert!(!shard.contains(3));
assert!(shard.insert([1; 32], 3));
assert!(shard.contains(3));
assert!(
!shard.insert([1; 32], 3),
"a replay of the same delivery records nothing new"
);
assert!(shard.insert([1; 32], 4));
}
#[test]
fn an_unclaimed_shard_reads_as_empty_and_round_trips() {
assert_eq!(
SeenShard::from_bytes(&[]).expect("empty data decodes"),
SeenShard::default(),
"an absent account is an unclaimed shard, not a decode failure"
);
let mut shard = SeenShard::default();
shard.insert([5; 32], 1);
shard.insert([5; 32], 9);
assert_eq!(
SeenShard::from_bytes(&shard.to_bytes()).expect("shard decodes"),
shard
);
}
#[test]
fn a_full_shard_fits_in_account_data() {
let mut shard = SeenShard::default();
for index in 0..SeenShard::MAX_DELIVERIES {
shard.insert([5; 32], u32::try_from(index).expect("index fits"));
}
let max = usize::try_from(DATA_MAX_LENGTH.as_u64()).expect("cap fits in usize");
assert_eq!(
shard.to_bytes().len(),
max,
"MAX_DELIVERIES is exactly what an account can carry"
);
shard.insert(
[5; 32],
u32::try_from(SeenShard::MAX_DELIVERIES).expect("index fits"),
);
assert!(
shard.to_bytes().len() > max,
"and one more does not fit, so the guest would fail rather than truncate"
);
}
}
+38 -23
View File
@@ -1,6 +1,7 @@
use cross_zone_inbox_core::{
CrossZoneMessage, InboxConfig, Instruction, SeenShard, inbox_config_account_id,
inbox_config_seed, inbox_seen_shard_account_id, inbox_seen_shard_seed, message_key,
inbox_config_seed, inbox_seen_shard_account_id, inbox_seen_shard_seed,
inbox_source_marker_account_id,
};
use lee_core::{
account::{Account, AccountWithMetadata},
@@ -61,10 +62,11 @@ fn dispatch(
"l1_inclusion_witness must be None in v1"
);
// pre_states layout: [config, seen_shard, then the target accounts].
// pre_states layout: [config, seen_shard, source marker, then the target accounts].
let mut accounts = pre_states.into_iter();
let config = accounts.next().expect("config account required");
let seen = accounts.next().expect("seen shard account required");
let marker = accounts.next().expect("source marker account required");
let target_accounts: Vec<AccountWithMetadata> = accounts.collect();
assert_eq!(
@@ -77,6 +79,14 @@ fn dispatch(
inbox_seen_shard_account_id(self_program_id, &msg.src_zone, msg.src_block_id),
"Second account must be the seen-shard PDA"
);
// The one value the chained call carries about where the message came from.
// The target re-derives this address from the source it accepts, so binding it
// here is what makes a target's own check meaningful.
assert_eq!(
marker.account_id,
inbox_source_marker_account_id(self_program_id, &msg.src_zone, msg.src_program_id),
"Third account must be the source marker PDA for this message"
);
let cfg = InboxConfig::from_bytes(&config.account.data.clone().into_inner())
.expect("inbox config decodes");
@@ -85,25 +95,28 @@ fn dispatch(
msg.src_zone != cfg.self_zone,
"Source zone must not be this zone"
);
// Checked as a pair. The emitting program is as much a part of the
// authorization as the target: an emitter whose caller chooses the target
// reaches everything the peer may reach, so a target allowlist on its own
// lets any such emitter stand in for every other one.
assert!(
cfg.permits(&msg.src_zone, msg.src_program_id, msg.target_program_id),
"No route from this source program to this target program for this peer"
);
let key = message_key(&msg.src_zone, msg.src_block_id, msg.src_tx_index);
let mut shard =
SeenShard::from_bytes(&seen.account.data.clone().into_inner()).expect("seen shard decodes");
let already_seen = shard.contains(&key);
// One block id, one delivering block. The address binds the zone and block
// id but not which block claimed them, so an equivocating peer's two blocks
// at one id land here; the first binds the shard and the second aborts.
//
// Before the replay check, not after: reaching the replay branch first would
// turn a wrong-block delivery into a silent no-op, which the indexer's
// already-seen short circuit would then wave through.
assert!(
shard.binds(&msg.src_block_hash),
"Seen shard is bound to a different peer block at this block id"
);
let already_seen = shard.contains(msg.src_tx_index);
// On replay this is a no-op: the seen shard is untouched and no call is made.
let (seen_post, chained_calls) = if already_seen {
(unchanged(&seen), vec![])
} else {
shard.insert(key);
shard.insert(msg.src_block_hash, msg.src_tx_index);
let mut seen_account = seen.account.clone();
seen_account.data = shard
.to_bytes()
@@ -125,19 +138,23 @@ fn dispatch(
.map(|c| u32::from_le_bytes(c.try_into().unwrap_or_else(|_| unreachable!())))
.collect();
// The marker leads, so a target reads its source at a fixed position
// without knowing anything about the accounts that follow it.
let mut call_pre_states = vec![marker.clone()];
call_pre_states.extend(target_accounts.clone());
let call = ChainedCall {
program_id: msg.target_program_id,
pre_states: target_accounts.clone(),
pre_states: call_pre_states,
instruction_data,
pda_seeds: vec![],
};
(seen_post, vec![call])
};
let mut post_states = vec![unchanged(&config), seen_post];
let mut post_states = vec![unchanged(&config), seen_post, unchanged(&marker)];
post_states.extend(target_accounts.iter().map(unchanged));
let mut output_pre_states = vec![config, seen];
let mut output_pre_states = vec![config, seen, marker];
output_pre_states.extend(target_accounts);
ProgramOutput::new(
@@ -151,8 +168,7 @@ fn dispatch(
.write();
}
/// Writes the inbox config (peer + target allowlists) into the config PDA exactly
/// once at genesis.
/// Writes the inbox config into the config PDA exactly once at genesis.
fn init_config(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
@@ -169,9 +185,8 @@ fn init_config(
"account must be the inbox config PDA"
);
// Init-once, idempotent under genesis replay: a `default` config is a first
// init; an already-owned config must already hold exactly these allowlists (the
// genesis block is replayed onto seeded state during multi-sequencer
// reconstruction), otherwise reject a post-genesis attempt to change them.
// init; an already-owned config must already hold exactly this, since genesis
// is replayed onto seeded state during multi-sequencer reconstruction.
// `new_claimed_if_default` alone would not stop the owning program from
// rewriting its own config data on a later call.
if config_meta.account != Account::default() {
@@ -182,7 +197,7 @@ fn init_config(
assert_eq!(
config_meta.account.data.clone().into_inner(),
config.to_bytes(),
"inbox config already initialized with different allowlists"
"inbox config already initialized differently"
);
}
+86 -16
View File
@@ -5,7 +5,11 @@ use lee_core::{
};
use serde::{Deserialize, Serialize};
const OUTBOX_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneOutbox/00000/";
/// Versions the seed layout: bump on any change to its field list or offsets,
/// so slots under an old layout can never be re-derived. Redundant with the
/// image id, which relocates every PDA in this crate whenever the crate changes,
/// but the two version different things.
const OUTBOX_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/CrossZoneOutbox/00001/";
/// Raw 32-byte zone (channel) id; the host maps it to the zone-sdk `ChannelId`.
pub type ZoneId = [u8; 32];
@@ -14,6 +18,10 @@ pub type ZoneId = [u8; 32];
pub enum Instruction {
/// Records an outbound cross-zone message as a write to a self-owned PDA.
///
/// The slot is written once: a second `Emit` at the same
/// `(emitter, target_zone, ordinal)` fails the transaction rather than
/// replacing the record.
///
/// Required accounts (1):
/// - Outbox PDA account
Emit {
@@ -28,12 +36,21 @@ pub enum Instruction {
},
}
/// The message as stored in an outbox PDA. The destination zone's watcher reads
/// this from the inscribed block; the source coordinates are filled by the
/// watcher, not stored here.
/// One emitted message, as stored in its outbox PDA.
///
/// Carries the slot it occupies as well as the message, so a reader holding the
/// bytes knows who wrote them and where without inverting the address, which is
/// a hash. The source zone and block coordinates are filled by the destination's
/// watcher and are not stored here.
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct OutboxRecord {
/// The program that called `Emit`, which is the immediate chained caller.
/// Cross-zone discovery names the top-level program instead, so joining a
/// record against a delivery is only sound while every emitter refuses to be
/// called by another program.
pub emitter: ProgramId,
pub target_zone: ZoneId,
pub ordinal: u32,
pub target_program_id: ProgramId,
pub target_accounts: Vec<[u8; 32]>,
pub payload: Vec<u8>,
@@ -52,22 +69,34 @@ impl OutboxRecord {
}
}
/// PDA holding one emitted message, keyed by destination zone and a per-zone
/// ordinal.
/// PDA holding one emitted message, keyed by the emitting program, the
/// destination zone, and a per-emitter per-zone ordinal.
///
/// `emitter` is the program that called `Emit`, which the guest takes from
/// `caller_program_id` rather than from the instruction. Without it in the
/// address two programs share a slot and one overwrites the other.
#[must_use]
pub fn outbox_pda(outbox_id: ProgramId, target_zone: &ZoneId, ordinal: u32) -> AccountId {
AccountId::for_public_pda(&outbox_id, &outbox_pda_seed(target_zone, ordinal))
pub fn outbox_pda(
outbox_id: ProgramId,
emitter: ProgramId,
target_zone: &ZoneId,
ordinal: u32,
) -> AccountId {
AccountId::for_public_pda(&outbox_id, &outbox_pda_seed(emitter, target_zone, ordinal))
}
/// Seed of an outbox message PDA, exposed so the guest can claim the account.
#[must_use]
pub fn outbox_pda_seed(target_zone: &ZoneId, ordinal: u32) -> PdaSeed {
pub fn outbox_pda_seed(emitter: ProgramId, target_zone: &ZoneId, ordinal: u32) -> PdaSeed {
use risc0_zkvm::sha::{Impl, Sha256 as _};
let mut bytes = [0_u8; 68];
let mut bytes = [0_u8; 100];
bytes[..32].copy_from_slice(&OUTBOX_SEED_DOMAIN);
bytes[32..64].copy_from_slice(target_zone);
bytes[64..].copy_from_slice(&ordinal.to_le_bytes());
for (word, chunk) in emitter.iter().zip(bytes[32..64].chunks_exact_mut(4)) {
chunk.copy_from_slice(&word.to_le_bytes());
}
bytes[64..96].copy_from_slice(target_zone);
bytes[96..].copy_from_slice(&ordinal.to_le_bytes());
let seed: [u8; 32] = Impl::hash_bytes(&bytes)
.as_bytes()
@@ -80,14 +109,55 @@ pub fn outbox_pda_seed(target_zone: &ZoneId, ordinal: u32) -> PdaSeed {
mod tests {
use super::*;
const OUTBOX: ProgramId = [3; 8];
const EMITTER: ProgramId = [4; 8];
#[test]
fn outbox_pda_is_unique_per_zone_and_ordinal() {
let id: ProgramId = [3; 8];
let zone_a = [1; 32];
let zone_b = [2; 32];
assert_eq!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_a, 0));
assert_ne!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_a, 1));
assert_ne!(outbox_pda(id, &zone_a, 0), outbox_pda(id, &zone_b, 0));
assert_eq!(
outbox_pda(OUTBOX, EMITTER, &zone_a, 0),
outbox_pda(OUTBOX, EMITTER, &zone_a, 0)
);
assert_ne!(
outbox_pda(OUTBOX, EMITTER, &zone_a, 0),
outbox_pda(OUTBOX, EMITTER, &zone_a, 1)
);
assert_ne!(
outbox_pda(OUTBOX, EMITTER, &zone_a, 0),
outbox_pda(OUTBOX, EMITTER, &zone_b, 0)
);
}
/// Two programs emitting to the same zone and ordinal must not share a slot,
/// or the second silently overwrites the first.
#[test]
fn outbox_pda_is_unique_per_emitter() {
let zone = [1; 32];
let other: ProgramId = [5; 8];
assert_ne!(
outbox_pda(OUTBOX, EMITTER, &zone, 0),
outbox_pda(OUTBOX, other, &zone, 0)
);
}
#[test]
fn outbox_record_round_trips() {
let record = OutboxRecord {
emitter: EMITTER,
target_zone: [1; 32],
ordinal: 7,
target_program_id: [6; 8],
target_accounts: vec![[9; 32]],
payload: b"payload".to_vec(),
};
assert_eq!(
OutboxRecord::from_bytes(&record.to_bytes()).expect("record decodes"),
record
);
}
}
+33 -9
View File
@@ -1,6 +1,6 @@
use cross_zone_outbox_core::{Instruction, OutboxRecord, outbox_pda, outbox_pda_seed};
use lee_core::{
account::AccountWithMetadata,
account::{Account, AccountWithMetadata},
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
};
@@ -15,10 +15,14 @@ fn main() {
instruction_words,
) = read_lee_inputs::<Instruction>();
assert!(
caller_program_id.is_some(),
"Outbox is only callable through a chain call from a user program"
);
// The emitter, and the only identity here the state machine verifies: it
// checks a guest's claimed caller against the real one. Note this is the
// immediate chained caller, not the top-level program that cross-zone
// discovery names; the two coincide only while every emitter refuses to be
// called by another program, which both do today.
let Some(emitter) = caller_program_id else {
panic!("Outbox is only callable through a chain call from a user program");
};
let (target_zone, target_program_id, target_accounts, payload, ordinal) = match instruction {
Instruction::Emit {
@@ -41,13 +45,32 @@ fn main() {
assert_eq!(
outbox.account_id,
outbox_pda(self_program_id, &target_zone, ordinal),
"Account must be the outbox PDA for (target_zone, ordinal)"
outbox_pda(self_program_id, emitter, &target_zone, ordinal),
"Account must be the outbox PDA for (emitter, target_zone, ordinal)"
);
// A slot holds one message for ever. Identity first, so a wrong account that
// happens to be free is reported as the wrong account rather than as a used
// slot.
//
// This is the same predicate the state machine already requires of a first
// write, so guest and host agree by construction rather than by coincidence.
//
// It also means a slot can be denied to its intended writer: the ordinal is
// caller-chosen in a namespace every user of an emitter shares, and an
// emission needs no signature, so anyone can occupy one. A client must pick
// an ordinal the chain does not already hold rather than counting from zero.
assert_eq!(
outbox.account,
Account::default(),
"Outbox slot already written: one Emit per (emitter, target_zone, ordinal)"
);
let mut post_account = outbox.account.clone();
post_account.data = OutboxRecord {
emitter,
target_zone,
ordinal,
target_program_id,
target_accounts,
payload,
@@ -56,9 +79,10 @@ fn main() {
.try_into()
.expect("OutboxRecord fits in account data");
let post = AccountPostState::new_claimed_if_default(
// Unconditional, since the pre-state is provably default by the assert above.
let post = AccountPostState::new_claimed(
post_account,
Claim::Pda(outbox_pda_seed(&target_zone, ordinal)),
Claim::Pda(outbox_pda_seed(emitter, &target_zone, ordinal)),
);
ProgramOutput::new(
+4
View File
@@ -8,5 +8,9 @@ license = { workspace = true }
workspace = true
[dependencies]
borsh.workspace = true
lee_core.workspace = true
serde = { workspace = true, features = ["alloc"] }
[dev-dependencies]
risc0-zkvm.workspace = true
+155 -3
View File
@@ -1,3 +1,4 @@
use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
account::AccountId,
program::{PdaSeed, ProgramId},
@@ -5,24 +6,79 @@ use lee_core::{
use serde::{Deserialize, Serialize};
const PING_RECORD_SEED: [u8; 32] = *b"/LEZ/v0.3/PingRecord/0000000000/";
const SENDER_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/PingSenderCfg/0000000/";
const RECEIVER_CONFIG_SEED: [u8; 32] = *b"/LEZ/v0.3/PingReceiverCfg/00000/";
/// Raw 32-byte zone (channel) id, matching the inbox's.
pub type ZoneId = [u8; 32];
/// Instruction delivered to `ping_receiver` by the inbox: record the payload.
/// Instruction to `ping_receiver`.
///
/// Variants are append-only, for the same reason `SenderInstruction`'s are.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReceiverInstruction {
/// Record the payload, delivered by the inbox on behalf of a peer source
/// this receiver authorizes.
///
/// Required accounts (3): the source marker, the receiver config PDA, then
/// the record PDA.
Record { payload: Vec<u8> },
/// Pins the deliverer and the peer sources it may deliver from, written once
/// into a default config PDA at genesis. A re-run holding anything different
/// is refused; an identical one is a no-op, which is what genesis replay does.
///
/// Required accounts (1): the receiver config PDA.
InitConfig(ReceiverConfig),
}
/// Instruction to `ping_sender`: forwarded verbatim into `cross_zone_outbox::Instruction::Emit`.
/// Who may deliver to this receiver, and which peer sources they may deliver from.
///
/// `ping_receiver` holds nothing worth stealing, so this is not about value. It is
/// about the record meaning something: without it any program on any configured
/// peer can overwrite the record, and a delivery proves only that some peer sent
/// it.
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct ReceiverConfig {
/// The program allowed to call `Record`: the cross-zone inbox.
pub deliverer: ProgramId,
/// The `(src_zone, src_program_id)` pairs a delivery may originate from.
pub sources: Vec<(ZoneId, ProgramId)>,
}
impl ReceiverConfig {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
borsh::to_vec(self).expect("receiver config serializes")
}
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
borsh::from_slice(bytes).ok()
}
}
/// Instruction to `ping_sender`. `Send`'s emission fields are forwarded verbatim
/// into `cross_zone_outbox::Instruction::Emit`.
///
/// Variants are append-only. risc0 serde encodes the variant as a bare leading
/// tag word, so inserting one ahead of `Send` shifts every existing encoding.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SenderInstruction {
/// Emit a cross-zone message through the pinned outbox.
///
/// Required accounts (2): the sender config PDA, then the outbox PDA.
Send {
outbox_program_id: ProgramId,
target_zone: [u8; 32],
target_program_id: ProgramId,
target_accounts: Vec<[u8; 32]>,
payload: Vec<u8>,
ordinal: u32,
},
/// Pins the outbox program, written once into a default config PDA at
/// genesis. A re-run naming a different outbox is refused; an identical one
/// is a no-op, which is what genesis replay does.
///
/// Required accounts (1): the sender config PDA.
InitConfig { outbox_program_id: ProgramId },
}
/// The account a `ping_receiver` records the latest delivered payload into.
@@ -36,3 +92,99 @@ pub fn ping_record_pda(receiver_id: ProgramId) -> AccountId {
pub const fn ping_record_seed() -> PdaSeed {
PdaSeed::new(PING_RECORD_SEED)
}
/// PDA holding the outbox program id, seeded at genesis so the guest can pin the
/// program it chains into without importing the outbox image id.
#[must_use]
pub fn sender_config_account_id(sender_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&sender_id, &sender_config_seed())
}
#[must_use]
pub const fn sender_config_seed() -> PdaSeed {
PdaSeed::new(SENDER_CONFIG_SEED)
}
/// PDA holding the sources `ping_receiver` accepts a delivery from.
#[must_use]
pub fn receiver_config_account_id(receiver_id: ProgramId) -> AccountId {
AccountId::for_public_pda(&receiver_id, &receiver_config_seed())
}
#[must_use]
pub const fn receiver_config_seed() -> PdaSeed {
PdaSeed::new(RECEIVER_CONFIG_SEED)
}
/// Encodes the pinned outbox program id for the config account's data.
#[must_use]
pub fn outbox_bytes(outbox_program_id: ProgramId) -> [u8; 32] {
let mut bytes = [0_u8; 32];
for (word, chunk) in outbox_program_id.iter().zip(bytes.chunks_exact_mut(4)) {
chunk.copy_from_slice(&word.to_le_bytes());
}
bytes
}
/// Decodes the pinned outbox program id from the config account's data.
#[must_use]
pub fn read_outbox(data: &[u8]) -> Option<ProgramId> {
if data.len() < 32 {
return None;
}
let mut outbox_program_id = [0_u32; 8];
for (word, chunk) in outbox_program_id.iter_mut().zip(data[..32].chunks_exact(4)) {
*word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!()));
}
Some(outbox_program_id)
}
#[cfg(test)]
mod tests {
use super::*;
/// `extract_emission` decodes `Send` off peer transactions, so its tag word is
/// wire format: a variant inserted ahead of it would silently shift every
/// existing encoding.
#[test]
fn send_is_the_first_variant() {
let send = SenderInstruction::Send {
target_zone: [7; 32],
target_program_id: [1; 8],
target_accounts: vec![],
payload: vec![],
ordinal: 0,
};
let words = risc0_zkvm::serde::to_vec(&send).expect("Send serializes");
assert_eq!(words[0], 0);
}
/// `Record` is serialized by the source zone into the emission payload and
/// decoded by the destination, so its tag word is wire format.
#[test]
fn record_is_the_first_variant() {
let record = ReceiverInstruction::Record { payload: vec![] };
let words = risc0_zkvm::serde::to_vec(&record).expect("Record serializes");
assert_eq!(words[0], 0);
}
#[test]
fn an_empty_receiver_config_does_not_decode() {
assert_eq!(ReceiverConfig::from_bytes(&[]), None);
}
#[test]
fn receiver_config_round_trips() {
let config = ReceiverConfig {
deliverer: [1; 8],
sources: vec![([7; 32], [9; 8])],
};
assert_eq!(ReceiverConfig::from_bytes(&config.to_bytes()), Some(config));
}
#[test]
fn outbox_id_round_trips() {
let outbox: ProgramId = [9; 8];
assert_eq!(read_outbox(&outbox_bytes(outbox)), Some(outbox));
}
}
+1
View File
@@ -8,5 +8,6 @@ license = { workspace = true }
workspace = true
[dependencies]
cross_zone_inbox_core.workspace = true
lee_core.workspace = true
ping_core.workspace = true
+120 -14
View File
@@ -1,8 +1,12 @@
use cross_zone_inbox_core::inbox_source_marker_account_id;
use lee_core::{
account::AccountWithMetadata,
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
account::{Account, AccountWithMetadata},
program::{AccountPostState, Claim, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs},
};
use ping_core::{
ReceiverConfig, ReceiverInstruction, ping_record_pda, ping_record_seed,
receiver_config_account_id, receiver_config_seed,
};
use ping_core::{ReceiverInstruction, ping_record_pda, ping_record_seed};
fn main() {
let (
@@ -15,21 +19,61 @@ fn main() {
instruction_words,
) = read_lee_inputs::<ReceiverInstruction>();
match instruction {
ReceiverInstruction::Record { payload } => record(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
payload,
),
ReceiverInstruction::InitConfig(config) => init_config(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
&config,
),
}
}
fn record(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
payload: Vec<u8>,
) {
// pre_states: [source marker, config PDA, record PDA].
let [marker, config, record] = <[AccountWithMetadata; 3]>::try_from(pre_states)
.expect("Record requires the source marker, config, and record accounts");
assert_eq!(
config.account_id,
receiver_config_account_id(self_program_id),
"Second account must be the receiver config PDA"
);
let cfg = ReceiverConfig::from_bytes(&config.account.data.clone().into_inner())
.expect("config account holds a receiver config");
assert_eq!(
caller_program_id,
Some(cfg.deliverer),
"Record is only callable by the authorized deliverer (the cross-zone inbox)"
);
// Which peer sent it is this program's own business. Without this the record
// says only that some program on some configured peer wrote it.
assert!(
caller_program_id.is_some(),
"ping_receiver is only callable through a chained call"
cfg.sources.iter().any(|(src_zone, src_program_id)| {
marker.account_id
== inbox_source_marker_account_id(cfg.deliverer, src_zone, *src_program_id)
}),
"Record is only callable for a peer source this receiver authorizes"
);
let payload = match instruction {
ReceiverInstruction::Record { payload } => payload,
};
let [record] = <[AccountWithMetadata; 1]>::try_from(pre_states)
.expect("Record requires exactly 1 account");
assert_eq!(
record.account_id,
ping_record_pda(self_program_id),
"Account must be the ping record PDA"
"Third account must be the ping record PDA"
);
let mut post_account = record.account.clone();
@@ -41,8 +85,70 @@ fn main() {
self_program_id,
caller_program_id,
instruction_words,
vec![record],
vec![post],
vec![marker.clone(), config.clone(), record],
vec![
AccountPostState::new(marker.account),
AccountPostState::new(config.account),
post,
],
)
.write();
}
/// Writes the deliverer and the authorized peer sources into the config PDA
/// exactly once at genesis.
fn init_config(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
config_value: &ReceiverConfig,
) {
assert!(
caller_program_id.is_none(),
"InitConfig is a top-level genesis transaction"
);
// pre_states: [config PDA].
let [config] = <[AccountWithMetadata; 1]>::try_from(pre_states)
.expect("InitConfig requires the config account");
assert_eq!(
config.account_id,
receiver_config_account_id(self_program_id),
"account must be the receiver config PDA"
);
// Init-once, idempotent under genesis replay: a `default` config is a first
// init; an already-owned one must already hold exactly this, since genesis is
// replayed onto seeded state during multi-sequencer reconstruction.
// `new_claimed_if_default` alone would not stop a later self-owned rewrite.
if config.account != Account::default() {
assert_eq!(
config.account.program_owner, self_program_id,
"receiver config PDA is owned by another program"
);
assert_eq!(
config.account.data.clone().into_inner(),
config_value.to_bytes(),
"receiver config already initialized differently"
);
}
let mut config_account = config.account.clone();
config_account.data = config_value
.to_bytes()
.try_into()
.expect("receiver config fits in account data");
let config_post = AccountPostState::new_claimed_if_default(
config_account,
Claim::Pda(receiver_config_seed()),
);
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![config],
vec![config_post],
)
.write();
}
+118 -17
View File
@@ -1,9 +1,14 @@
use cross_zone_outbox_core::Instruction as OutboxInstruction;
use lee_core::{
account::AccountWithMetadata,
program::{AccountPostState, ChainedCall, ProgramInput, ProgramOutput, read_lee_inputs},
account::{Account, AccountWithMetadata},
program::{
AccountPostState, ChainedCall, Claim, ProgramId, ProgramInput, ProgramOutput,
read_lee_inputs,
},
};
use ping_core::{
SenderInstruction, outbox_bytes, read_outbox, sender_config_account_id, sender_config_seed,
};
use ping_core::SenderInstruction;
fn main() {
let (
@@ -21,19 +26,63 @@ fn main() {
"ping_sender is only invoked as a top-level user transaction"
);
let SenderInstruction::Send {
outbox_program_id,
target_zone,
target_program_id,
target_accounts,
payload,
ordinal,
} = instruction;
match instruction {
SenderInstruction::Send {
target_zone,
target_program_id,
target_accounts,
payload,
ordinal,
} => send(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
target_zone,
target_program_id,
target_accounts,
payload,
ordinal,
),
SenderInstruction::InitConfig { outbox_program_id } => init_config(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
outbox_program_id,
),
}
}
// The single account is the outbox PDA the chained call writes into; the
// outbox claims it, so ping_sender forwards it unchanged.
let [outbox] =
<[AccountWithMetadata; 1]>::try_from(pre_states).expect("Send requires exactly 1 account");
#[expect(
clippy::too_many_arguments,
reason = "the emission fields are passed through verbatim"
)]
fn send(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
target_zone: [u8; 32],
target_program_id: ProgramId,
target_accounts: Vec<[u8; 32]>,
payload: Vec<u8>,
ordinal: u32,
) {
// pre_states: [config PDA, outbox PDA]. The outbox claims its own slot, so
// ping_sender forwards it unchanged.
let [config, outbox] = <[AccountWithMetadata; 2]>::try_from(pre_states)
.expect("Send requires the config and outbox accounts");
// Pinned rather than caller-named: chaining elsewhere would let an emission
// skip the real outbox and leave no record of itself.
assert_eq!(
config.account_id,
sender_config_account_id(self_program_id),
"first account must be the ping-sender config PDA"
);
let outbox_program_id = read_outbox(&config.account.data.clone().into_inner())
.expect("config account holds an outbox program id");
let call = ChainedCall::new(
outbox_program_id,
@@ -47,13 +96,65 @@ fn main() {
},
);
let config_post = AccountPostState::new(config.account.clone());
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![outbox.clone()],
vec![AccountPostState::new(outbox.account)],
vec![config, outbox.clone()],
vec![config_post, AccountPostState::new(outbox.account)],
)
.with_chained_calls(vec![call])
.write();
}
/// Writes the outbox program id into the config PDA exactly once at genesis.
fn init_config(
self_program_id: ProgramId,
caller_program_id: Option<ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
outbox_program_id: ProgramId,
) {
// pre_states: [config PDA].
let [config] = <[AccountWithMetadata; 1]>::try_from(pre_states)
.expect("InitConfig requires the config account");
assert_eq!(
config.account_id,
sender_config_account_id(self_program_id),
"account must be the ping-sender config PDA"
);
// Init-once, idempotent under genesis replay: a `default` config is a first
// init; an already-owned one must already pin exactly this outbox, since
// genesis is replayed onto seeded state during multi-sequencer reconstruction.
// `new_claimed_if_default` alone would not stop a later self-owned rewrite.
if config.account != Account::default() {
assert_eq!(
config.account.program_owner, self_program_id,
"ping-sender config PDA is owned by another program"
);
assert_eq!(
config.account.data.clone().into_inner(),
outbox_bytes(outbox_program_id).to_vec(),
"ping-sender config already pins a different outbox"
);
}
let mut config_account = config.account.clone();
config_account.data = outbox_bytes(outbox_program_id)
.to_vec()
.try_into()
.expect("outbox id fits in account data");
let config_post =
AccountPostState::new_claimed_if_default(config_account, Claim::Pda(sender_config_seed()));
ProgramOutput::new(
self_program_id,
caller_program_id,
instruction_words,
vec![config],
vec![config_post],
)
.write();
}
+1
View File
@@ -8,5 +8,6 @@ license = { workspace = true }
workspace = true
[dependencies]
cross_zone_inbox_core.workspace = true
lee_core.workspace = true
wrapped_token_core.workspace = true
@@ -8,6 +8,7 @@ license = { workspace = true }
workspace = true
[dependencies]
borsh.workspace = true
lee_core.workspace = true
serde = { workspace = true, features = ["alloc"] }
risc0-zkvm.workspace = true
+76 -33
View File
@@ -2,29 +2,70 @@
//! cross-zone bridge. Only the cross-zone inbox may mint; the guest enforces
//! this by reading the authorized minter from a genesis-seeded config account.
use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
account::AccountId,
program::{PdaSeed, ProgramId},
};
use serde::{Deserialize, Serialize};
/// The most one mint may credit.
///
/// The peer zone chooses the amount and the balance is a `u128`, so unbounded
/// one delivery pins a holding near the maximum, every later honest mint
/// overflows into a guest panic, and the holding is bricked for inbound
/// transfers at a cost of one message. The cap does not remove that ceiling, it
/// makes reaching it cost 2^64 deliveries instead of one.
///
/// `u64::MAX` is the bridge's bound, not one native balances obey. `bridge_lock`
/// refuses a larger amount at the source so it fails before escrowing.
pub const MAX_MINT_AMOUNT: u128 = 0xFFFF_FFFF_FFFF_FFFF;
const CONFIG_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenConfig/00/";
const HOLDING_SEED_DOMAIN: [u8; 32] = *b"/LEZ/v0.3/WrappedTokenHold/00000";
/// Raw 32-byte zone (channel) id, matching the inbox's.
pub type ZoneId = [u8; 32];
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Instruction {
/// Credit `amount` wrapped tokens to `recipient`'s holding. Delivered only by
/// the cross-zone inbox.
/// the cross-zone inbox, and only for a peer source this token authorizes.
///
/// Required accounts (2): the wrapped-token config PDA, then the recipient's
/// holding PDA.
/// Required accounts (3): the source marker, the wrapped-token config PDA,
/// then the recipient's holding PDA.
Mint { recipient: [u8; 32], amount: u128 },
/// Pins `minter` (the cross-zone inbox) as the authorized minter, written once
/// into a default config PDA at genesis. The guest refuses a non-default
/// pre-state, so it cannot be re-run to hijack the minter.
/// Pins the minter and the peer sources it may mint for, written once into a
/// default config PDA at genesis. A re-run holding anything different is
/// refused; an identical one is a no-op, which is what genesis replay does.
///
/// Required accounts (1): the wrapped-token config PDA.
InitConfig { minter: ProgramId },
InitConfig(WrappedTokenConfig),
}
/// Who may mint, and which peer sources they may mint for.
///
/// The source list is what makes this token authorize its own inbound value
/// rather than trusting a central route table to have done it. Borsh because the
/// list is variable length.
#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct WrappedTokenConfig {
/// The program allowed to call `Mint`: the cross-zone inbox.
pub minter: ProgramId,
/// The `(src_zone, src_program_id)` pairs a mint may originate from. Empty on
/// a zone with no peers, which authorizes nothing.
pub sources: Vec<(ZoneId, ProgramId)>,
}
impl WrappedTokenConfig {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
borsh::to_vec(self).expect("wrapped-token config serializes")
}
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
borsh::from_slice(bytes).ok()
}
}
/// PDA holding the authorized minter program id (the cross-zone inbox), seeded at
@@ -59,29 +100,6 @@ pub fn holding_seed(recipient: &[u8; 32]) -> PdaSeed {
PdaSeed::new(seed)
}
/// Encodes the authorized minter program id for the config account's data.
#[must_use]
pub fn minter_bytes(minter: ProgramId) -> [u8; 32] {
let mut bytes = [0_u8; 32];
for (word, chunk) in minter.iter().zip(bytes.chunks_exact_mut(4)) {
chunk.copy_from_slice(&word.to_le_bytes());
}
bytes
}
/// Decodes the authorized minter program id from the config account's data.
#[must_use]
pub fn read_minter(data: &[u8]) -> Option<ProgramId> {
if data.len() < 32 {
return None;
}
let mut minter = [0_u32; 8];
for (word, chunk) in minter.iter_mut().zip(data[..32].chunks_exact(4)) {
*word = u32::from_le_bytes(chunk.try_into().unwrap_or_else(|_| unreachable!()));
}
Some(minter)
}
/// Reads a wrapped-token balance from account data; empty data is a zero balance.
#[must_use]
pub fn read_balance(data: &[u8]) -> u128 {
@@ -101,9 +119,34 @@ mod tests {
use super::*;
#[test]
fn minter_round_trips() {
let minter: ProgramId = [1, 2, 3, 4, 5, 6, 7, 8];
assert_eq!(read_minter(&minter_bytes(minter)), Some(minter));
fn config_round_trips() {
let config = WrappedTokenConfig {
minter: [1, 2, 3, 4, 5, 6, 7, 8],
sources: vec![([7; 32], [9; 8]), ([8; 32], [4; 8])],
};
assert_eq!(
WrappedTokenConfig::from_bytes(&config.to_bytes()),
Some(config)
);
}
/// An unclaimed config reads as empty, which must not decode to a config that
/// authorizes anything.
#[test]
fn an_empty_config_does_not_decode() {
assert_eq!(WrappedTokenConfig::from_bytes(&[]), None);
}
/// The peer's `bridge_lock` serializes `Mint` into the emission payload, so
/// its tag word is wire format.
#[test]
fn mint_is_the_first_variant() {
let mint = Instruction::Mint {
recipient: [3; 32],
amount: 1,
};
let words = risc0_zkvm::serde::to_vec(&mint).expect("Mint serializes");
assert_eq!(words[0], 0);
}
#[test]
+43 -21
View File
@@ -1,10 +1,11 @@
use cross_zone_inbox_core::inbox_source_marker_account_id;
use lee_core::{
account::{Account, AccountWithMetadata},
program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs},
};
use wrapped_token_core::{
Instruction, balance_bytes, config_account_id, config_seed, holding_account_id, holding_seed,
minter_bytes, read_balance, read_minter,
Instruction, MAX_MINT_AMOUNT, WrappedTokenConfig, balance_bytes, config_account_id,
config_seed, holding_account_id, holding_seed, read_balance,
};
fn main() {
@@ -27,12 +28,12 @@ fn main() {
recipient,
amount,
),
Instruction::InitConfig { minter } => init_config(
Instruction::InitConfig(config) => init_config(
self_program_id,
caller_program_id,
pre_states,
instruction_words,
minter,
&config,
),
}
}
@@ -45,31 +46,47 @@ fn mint(
recipient: [u8; 32],
amount: u128,
) {
// pre_states: [config PDA, recipient holding PDA].
let [config, holding] = <[AccountWithMetadata; 2]>::try_from(pre_states)
.expect("Mint requires the config and recipient holding accounts");
// pre_states: [source marker, config PDA, recipient holding PDA].
let [marker, config, holding] = <[AccountWithMetadata; 3]>::try_from(pre_states)
.expect("Mint requires the source marker, config, and recipient holding accounts");
// The config PDA is genesis-seeded with the authorized minter (the cross-zone
// inbox). Pin the caller to it, since the guest cannot import the inbox id.
assert_eq!(
config.account_id,
config_account_id(self_program_id),
"first account must be the wrapped-token config PDA"
"second account must be the wrapped-token config PDA"
);
let minter = read_minter(&config.account.data.clone().into_inner())
.expect("config account holds an authorized minter id");
let cfg = WrappedTokenConfig::from_bytes(&config.account.data.clone().into_inner())
.expect("config account holds a wrapped-token config");
assert_eq!(
caller_program_id,
Some(minter),
Some(cfg.minter),
"Mint is only callable by the authorized minter (the cross-zone inbox)"
);
// The inbox vouches only that the message arrived; which peer sent it is this
// token's own business, and unbacked value is what gets minted if it takes
// anyone's word for it. The marker's address is the source, so re-deriving it
// from an authorized pair is the whole check.
assert!(
cfg.sources.iter().any(|(src_zone, src_program_id)| {
marker.account_id
== inbox_source_marker_account_id(cfg.minter, src_zone, *src_program_id)
}),
"Mint is only callable for a peer source this token authorizes"
);
assert_eq!(
holding.account_id,
holding_account_id(self_program_id, &recipient),
"second account must be the recipient holding PDA"
"third account must be the recipient holding PDA"
);
assert!(
amount <= MAX_MINT_AMOUNT,
"mint amount exceeds the per-mint cap"
);
// The backstop against accumulation, which the per-mint cap does not bound.
let new_balance = read_balance(&holding.account.data.clone().into_inner())
.checked_add(amount)
.expect("wrapped-token balance overflow");
@@ -88,19 +105,24 @@ fn mint(
self_program_id,
caller_program_id,
instruction_words,
vec![config, holding],
vec![config_post, holding_post],
vec![marker.clone(), config, holding],
vec![
AccountPostState::new(marker.account),
config_post,
holding_post,
],
)
.write();
}
/// Writes the authorized minter into the config PDA exactly once at genesis.
/// Writes the minter and the authorized peer sources into the config PDA exactly
/// once at genesis.
fn init_config(
self_program_id: lee_core::program::ProgramId,
caller_program_id: Option<lee_core::program::ProgramId>,
pre_states: Vec<AccountWithMetadata>,
instruction_words: Vec<u32>,
minter: lee_core::program::ProgramId,
config_value: &WrappedTokenConfig,
) {
assert!(
caller_program_id.is_none(),
@@ -128,16 +150,16 @@ fn init_config(
);
assert_eq!(
config.account.data.clone().into_inner(),
minter_bytes(minter).to_vec(),
"wrapped-token config already initialized with a different minter"
config_value.to_bytes(),
"wrapped-token config already initialized differently"
);
}
let mut config_account = config.account.clone();
config_account.data = minter_bytes(minter)
.to_vec()
config_account.data = config_value
.to_bytes()
.try_into()
.expect("minter id fits in account data");
.expect("wrapped-token config fits in account data");
let config_post =
AccountPostState::new_claimed_if_default(config_account, Claim::Pda(config_seed()));
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "sequencer_executor_actor"
version = "0.1.0"
edition = "2024"
license = { workspace = true }
[lints]
workspace = true
[dependencies]
sequencer_core.workspace = true
common.workspace = true
lee_core.workspace = true
mempool.workspace = true
storage.workspace = true
kameo.workspace = true
tokio.workspace = true
tokio-util.workspace = true
log.workspace = true
anyhow.workspace = true
thiserror.workspace = true
hex.workspace = true
[dev-dependencies]
lee.workspace = true
sequencer_core = { workspace = true, features = ["mock"] }
test_programs.workspace = true
env_logger.workspace = true
tempfile.workspace = true
bytesize.workspace = true
num-bigint.workspace = true
+336
View File
@@ -0,0 +1,336 @@
use common::{block::Block, transaction::LeeTransaction};
use kameo::{
Actor,
actor::{ActorRef, WeakActorRef},
error::ActorStopReason,
mailbox::{MailboxReceiver, Signal},
message::{Context, Message},
};
use lee_core::{
BlockId,
account::{Balance, Nonce},
};
use log::{info, warn};
use mempool::MemPoolHandle;
use sequencer_core::{
SequencerCore, TransactionOrigin,
block_publisher::{BlockPublisherTrait, Ed25519Key},
config::SequencerConfig,
task_group::TaskGroup,
};
use tokio::select;
use tokio_util::sync::CancellationToken;
use crate::{
Result,
error::Error,
protocol::{
GetAccount, GetAccountBalance, GetAccountNonces, GetAccountReply, GetBlock, GetBlockRange,
GetChannelId, GetChannelIdReply, GetCrossZoneDeadLetters, GetCrossZoneDeadLettersReply,
GetLastBlockId, GetProofsAndRoot, GetTransaction, ProduceBlock, Transaction,
},
};
// TODO: Remove `BP` once this part is moved to a separate actor
pub struct ExecutorActor<BP: BlockPublisherTrait> {
sequencer: SequencerCore<BP>,
mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>,
// --- TODO: Remove these fields below ---
/// Cancelled when the publisher's drive task terminates (e.g. a panicked
/// persist sink); no channel events are processed past that point.
driver_cancellation: CancellationToken,
/// The core's background tasks, taken before the core was shared. This
/// handle owns no reference to the core itself, so without these there is
/// nothing to wait on: aborting the main loop only starts the teardown.
background_tasks: Vec<TaskGroup>,
}
impl<BP: BlockPublisherTrait> ExecutorActor<BP> {
pub async fn new(config: SequencerConfig) -> Self {
let (sequencer, mempool_handle) = SequencerCore::<BP>::start_from_config(config).await;
let driver_cancellation = sequencer.block_publisher().driver_cancellation();
let background_tasks = sequencer.background_tasks();
Self {
sequencer,
mempool_handle,
driver_cancellation,
background_tasks,
}
}
/// Handle to the sequencer's mempool, for feeding externally-received
/// (e.g. gossiped) transactions in.
#[must_use]
pub fn mempool_handle(&self) -> MemPoolHandle<(TransactionOrigin, LeeTransaction)> {
self.mempool_handle.clone()
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Actor for ExecutorActor<BP> {
type Args = Self;
type Error = Error;
async fn on_start(args: Self::Args, _actor_ref: ActorRef<Self>) -> Result<Self> {
Ok(args)
}
#[expect(
clippy::integer_division_remainder_used,
reason = "Generated by select! macro, can't be easily rewritten to avoid this lint"
)]
async fn next(
&mut self,
_actor_ref: WeakActorRef<Self>,
mailbox_rx: &mut MailboxReceiver<Self>,
) -> Result<Option<Signal<Self>>> {
// TODO: Remove this please
for task in &self.background_tasks {
if task.any_finished() {
return Err(Error::BackgroundTaskFinishedUnexpectedly);
}
}
select! {
signal = mailbox_rx.recv() => {
Ok(signal)
}
() = self.driver_cancellation.cancelled() => {
Err(Error::BlockPublisherFinishedUnexpectedly)
}
}
}
async fn on_stop(
&mut self,
_actor_ref: WeakActorRef<Self>,
_reason: ActorStopReason,
) -> Result<()> {
for tasks in &self.background_tasks {
tasks.shutdown().await;
}
Ok(())
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<ProduceBlock> for ExecutorActor<BP> {
type Reply = Result<()>;
async fn handle(
&mut self,
ProduceBlock: ProduceBlock,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
// Only produce on our turn.
if !self.sequencer.is_our_turn() {
info!("Not our turn to produce a block, skipping");
return Ok(());
}
// Never inscribe a second block at a height we already published: the
// channel would carry two chains from there and nothing resolves that.
// The head rewinds under us when the sdk orphans our own unfinalized
// blocks, and recovers once they finalize, so this is a wait.
if let Some(high_water) = self.sequencer.rewound_below_published() {
warn!(
"Skipping turn: head rewound to {} but block {high_water} is already inscribed; \
waiting for the channel to restore it",
self.sequencer.next_block_height().saturating_sub(1),
);
return Ok(());
}
info!("Our turn: collecting transactions from mempool, creating block");
let id = self
.sequencer
.produce_new_block()
.await
.map_err(Error::BlockProductionFailed)?;
let author_identity = hex::encode(
Ed25519Key::from_bytes(&self.sequencer.sequencer_config().signing_key)
.public_key()
.as_bytes(),
);
log::info!("Block with id {id} created by {author_identity:?}");
Ok(())
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<Transaction> for ExecutorActor<BP> {
type Reply = Result<()>;
async fn handle(
&mut self,
Transaction { transaction }: Transaction,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.mempool_handle
.try_push((TransactionOrigin::User, transaction))
.map_err(|_err| Error::MempoolIsFull)
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetBlock> for ExecutorActor<BP> {
type Reply = Result<Option<Block>>;
async fn handle(
&mut self,
GetBlock { block_id }: GetBlock,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.sequencer
.block_store()
.get_block_at_id(block_id)
.map_err(Into::into)
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetBlockRange> for ExecutorActor<BP> {
type Reply = Result<Vec<Block>>;
async fn handle(
&mut self,
GetBlockRange { range }: GetBlockRange,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
range
.map_while(|block_id| {
self.sequencer
.block_store()
.get_block_at_id(block_id)
.map_err(Into::into)
.transpose()
})
.collect::<Result<Vec<_>>>()
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetLastBlockId> for ExecutorActor<BP> {
type Reply = Result<BlockId>;
async fn handle(
&mut self,
GetLastBlockId: GetLastBlockId,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
Ok(self.sequencer.chain_height())
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetAccountBalance> for ExecutorActor<BP> {
type Reply = Balance;
async fn handle(
&mut self,
GetAccountBalance { account_id }: GetAccountBalance,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.sequencer
.with_state(|state| state.get_account_by_id(account_id).balance)
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetTransaction> for ExecutorActor<BP> {
type Reply = Option<(LeeTransaction, BlockId)>;
async fn handle(
&mut self,
GetTransaction { tx_hash }: GetTransaction,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.sequencer
.block_store()
.get_transaction_by_hash(tx_hash)
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetAccountNonces> for ExecutorActor<BP> {
type Reply = Vec<Nonce>;
async fn handle(
&mut self,
GetAccountNonces { account_ids }: GetAccountNonces,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.sequencer.with_state(|state| {
account_ids
.into_iter()
.map(|account_id| state.get_account_by_id(account_id).nonce)
.collect()
})
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetProofsAndRoot> for ExecutorActor<BP> {
type Reply = (
Vec<Option<lee_core::MembershipProof>>,
lee_core::CommitmentSetDigest,
);
async fn handle(
&mut self,
GetProofsAndRoot { commitments }: GetProofsAndRoot,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
self.sequencer.with_state(|state| {
let proofs = commitments
.iter()
.map(|commitment| state.get_proof_for_commitment(commitment))
.collect();
(proofs, state.commitment_root())
})
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetAccount> for ExecutorActor<BP> {
type Reply = GetAccountReply;
async fn handle(
&mut self,
GetAccount { account_id }: GetAccount,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
GetAccountReply {
account: self
.sequencer
.with_state(|state| state.get_account_by_id(account_id)),
}
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetChannelId> for ExecutorActor<BP> {
type Reply = GetChannelIdReply;
async fn handle(
&mut self,
GetChannelId: GetChannelId,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
GetChannelIdReply {
channel_id: *self.sequencer.block_publisher().channel_id().as_ref(),
}
}
}
impl<BP: BlockPublisherTrait + Send + 'static> Message<GetCrossZoneDeadLetters>
for ExecutorActor<BP>
{
type Reply = Result<GetCrossZoneDeadLettersReply>;
async fn handle(
&mut self,
GetCrossZoneDeadLetters: GetCrossZoneDeadLetters,
_ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
let (total_retired, retained) = self.sequencer.cross_zone_dead_letters()?;
Ok(GetCrossZoneDeadLettersReply {
total_retired,
retained,
})
}
}
@@ -0,0 +1,17 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("One of the sequencer's background tasks has finished unexpectedly")]
BackgroundTaskFinishedUnexpectedly,
#[error("The sequencer's block publisher has finished unexpectedly")]
BlockPublisherFinishedUnexpectedly,
#[error("The mempool is full")]
MempoolIsFull,
#[error("Storage error")]
StorageError(#[from] storage::error::DbError),
#[error(transparent)]
BlockProductionFailed(anyhow::Error),
}
+11
View File
@@ -0,0 +1,11 @@
//! Executor Actor performs the main logic of the Sequencer.
pub use actor::ExecutorActor;
pub mod actor;
pub mod error;
pub mod protocol;
#[cfg(test)]
mod tests;
pub type Result<T> = std::result::Result<T, error::Error>;

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