From 4832cfbc9a024633051b1bec01bcdbfb82b7bf40 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 9 Jul 2026 11:35:21 +0200 Subject: [PATCH 1/2] test: bridge guard account flags modification --- lee/state_machine/src/lib.rs | 2 + lee/state_machine/src/test_utils.rs | 28 +++++ lee/state_machine/src/validated_state_diff.rs | 18 ++- lez/common/Cargo.toml | 3 + lez/common/src/test_utils.rs | 35 ++++++ lez/common/src/transaction.rs | 107 +++++++++++++++++- 6 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 lee/state_machine/src/test_utils.rs diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index f8cc034a..0bd2c901 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -30,6 +30,8 @@ pub mod program_deployment_transaction; pub mod public_transaction; mod signature; mod state; +#[cfg(feature = "test-utils")] +pub mod test_utils; mod validated_state_diff; mod privacy_preserving_circuit { diff --git a/lee/state_machine/src/test_utils.rs b/lee/state_machine/src/test_utils.rs new file mode 100644 index 00000000..f9325ddd --- /dev/null +++ b/lee/state_machine/src/test_utils.rs @@ -0,0 +1,28 @@ +//! Test-only constructors for otherwise-opaque state types. +//! +//! A [`ValidatedStateDiff`] can normally only be produced by the transaction validation +//! functions, which guarantees it has been checked before any state mutation. These +//! helpers let downstream crates unit-test *post-execution* validation logic — e.g. the +//! system-account and bridge guards in `common` — against a hand-built diff, without +//! running a program in the zkVM. + +use std::collections::HashMap; + +use crate::{ + Account, AccountId, + validated_state_diff::{StateDiff, ValidatedStateDiff}, +}; + +/// Builds a [`ValidatedStateDiff`] carrying only the given public-account changes. +#[must_use] +pub const fn validated_state_diff_from_public_diff( + public_diff: HashMap, +) -> ValidatedStateDiff { + ValidatedStateDiff::new_unchecked(StateDiff { + signer_account_ids: Vec::new(), + public_diff, + new_commitments: Vec::new(), + new_nullifiers: Vec::new(), + program: None, + }) +} diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs index 44a307af..4b754dd9 100644 --- a/lee/state_machine/src/validated_state_diff.rs +++ b/lee/state_machine/src/validated_state_diff.rs @@ -35,10 +35,24 @@ pub struct StateDiff { /// The validated output of executing or verifying a transaction, ready to be applied to the state. /// -/// Can only be constructed by the transaction validation functions inside this crate, ensuring the -/// diff has been checked before any state mutation occurs. +/// It can only be constructed by the transaction validation functions inside this crate, ensuring +/// the diff has been checked before any state mutation occurs. Under the `test-utils` feature the +/// [`crate::test_utils`] module additionally exposes a hand-rolled constructor for unit-testing +/// downstream validation logic; that feature must never be enabled in a production build. pub struct ValidatedStateDiff(StateDiff); +#[cfg(feature = "test-utils")] +impl ValidatedStateDiff { + /// Test-only constructor that wraps an already-built [`StateDiff`] **without validating it**. + /// + /// Kept in this module so the wrapped field can stay private: in a normal build (feature off) + /// the only ways to obtain a `ValidatedStateDiff` remain the `from_*_transaction` validators. + #[must_use] + pub const fn new_unchecked(state_diff: StateDiff) -> Self { + Self(state_diff) + } +} + impl ValidatedStateDiff { pub fn from_public_transaction( tx: &PublicTransaction, diff --git a/lez/common/Cargo.toml b/lez/common/Cargo.toml index 8b2aa322..7582e885 100644 --- a/lez/common/Cargo.toml +++ b/lez/common/Cargo.toml @@ -25,3 +25,6 @@ log.workspace = true hex.workspace = true borsh.workspace = true logos-blockchain-common-http-client.workspace = true + +[dev-dependencies] +lee = { workspace = true, features = ["test-utils"] } diff --git a/lez/common/src/test_utils.rs b/lez/common/src/test_utils.rs index 7afda3dd..4a9ab992 100644 --- a/lez/common/src/test_utils.rs +++ b/lez/common/src/test_utils.rs @@ -1,4 +1,12 @@ +// Backs the hand-built state/diff helpers below, which are compiled only for `common`'s own +// unit tests. They rely on `lee::test_utils`, gated behind `lee`'s `test-utils` feature and +// enabled here via dev-dependencies, so it never reaches a production build. +#[cfg(test)] +use std::collections::HashMap; + use lee::AccountId; +#[cfg(test)] +use lee::{Account, PrivateKey, PublicKey, V03State, ValidatedStateDiff}; use crate::{ HashType, @@ -13,6 +21,33 @@ pub fn sequencer_sign_key_for_testing() -> lee::PrivateKey { lee::PrivateKey::try_new([37; 32]).unwrap() } +/// A syntactically valid `Public` transaction. Its contents are irrelevant to the +/// bridge guard, which only branches on the transaction *variant* and the diff. +#[cfg(test)] +#[must_use] +pub fn any_public_transaction() -> LeeTransaction { + let sender_key = PrivateKey::try_new([9_u8; 32]).expect("valid key"); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let recipient_key = PrivateKey::try_new([8_u8; 32]).expect("valid key"); + let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); + create_transaction_native_token_transfer(sender_id, 0, recipient_id, 1, &sender_key) +} + +/// Builds a state whose only entry is `account_id` (set to `pre`) and a single-entry diff +/// that maps `account_id` to `post`, so the validation guards can be exercised in isolation. +#[cfg(test)] +#[must_use] +pub fn state_and_diff( + account_id: AccountId, + pre: Account, + post: Account, +) -> (V03State, ValidatedStateDiff) { + let state = V03State::new().with_public_accounts([(account_id, pre)]); + let diff = + lee::test_utils::validated_state_diff_from_public_diff(HashMap::from([(account_id, post)])); + (state, diff) +} + // Dummy producers /// Produce dummy block with provided transactions + clock transaction an the end. diff --git a/lez/common/src/transaction.rs b/lez/common/src/transaction.rs index b5aee648..64725738 100644 --- a/lez/common/src/transaction.rs +++ b/lez/common/src/transaction.rs @@ -260,9 +260,112 @@ fn validate_doesnt_modify_account( #[cfg(test)] mod tests { - use lee::{AccountId, PrivateKey, PublicKey, V03State}; + use lee::{Account, AccountId, PrivateKey, PublicKey, V03State}; + use lee_core::account::Nonce; - use crate::test_utils::create_transaction_native_token_transfer; + use super::validate_doesnt_modify_account; + use crate::test_utils::{ + any_public_transaction, create_transaction_native_token_transfer, state_and_diff, + }; + + #[test] + fn bridge_guard_allows_balance_only_increase() { + // A diff that *only* increases the bridge balance (the legitimate deposit shape) + // must be accepted. + let bridge_id = system_accounts::bridge_account_id(); + let pre = Account { + balance: 500, + nonce: Nonce(7), + ..Account::default() + }; + let post = Account { + balance: 600, + ..pre.clone() + }; + let (state, diff) = state_and_diff(bridge_id, pre, post); + + let tx = any_public_transaction(); + assert!( + tx.validate_bridge_account_modification(&state, &diff) + .is_ok(), + "a balance-only increase of the bridge account must be allowed", + ); + } + + #[test] + fn bridge_guard_rejects_data_modification_even_when_balance_increases() { + // A diff that changes the bridge account's data (here: the nonce) while *also* + // increasing its balance must be rejected. + let bridge_id = system_accounts::bridge_account_id(); + let pre = Account { + balance: 500, + nonce: Nonce(7), + ..Account::default() + }; + let post = Account { + balance: 600, + nonce: Nonce(8), + ..pre.clone() + }; + let (state, diff) = state_and_diff(bridge_id, pre, post); + + let tx = any_public_transaction(); + assert!( + tx.validate_bridge_account_modification(&state, &diff) + .is_err(), + "modifying bridge account data must be rejected even if the balance increases", + ); + } + + #[test] + fn bridge_guard_rejects_zero_value_deposit() { + // A diff that touches the bridge account without *strictly* increasing its balance + // must be rejected — a zero-value deposit is not a real credit. + let bridge_id = system_accounts::bridge_account_id(); + let pre = Account { + balance: 500, + nonce: Nonce(7), + ..Account::default() + }; + let post = pre.clone(); + let (state, diff) = state_and_diff(bridge_id, pre, post); + + let tx = any_public_transaction(); + assert!( + tx.validate_bridge_account_modification(&state, &diff) + .is_err(), + "a bridge diff that does not strictly increase the balance must be rejected", + ); + } + + #[test] + fn validate_doesnt_modify_account_flags_a_changed_account() { + // Directly exercise the system-account guard with a diff that genuinely changes a + // clock account, then with one that leaves it untouched. The inverted comparison would + // treat a changed account as unchanged and wave it through (and would flag an *unchanged* + // account instead). + let clock_id = system_accounts::clock_account_ids()[0]; + let pre = Account { + balance: 1_000, + ..Account::default() + }; + + let changed = Account { + balance: 2_000, + ..Account::default() + }; + let (state, diff) = state_and_diff(clock_id, pre.clone(), changed); + assert!( + validate_doesnt_modify_account(&state, &diff, clock_id).is_err(), + "a diff that changes a system account must be rejected", + ); + + let (unchanged_state, unchanged_diff) = state_and_diff(clock_id, pre.clone(), pre); + assert!( + validate_doesnt_modify_account(&unchanged_state, &unchanged_diff, clock_id).is_ok(), + "a diff that leaves a system account unchanged must be accepted", + ); + } #[test] fn system_account_ids_are_distinct_and_non_default() { From cc3dc9cdb6cca73ae50b1a40272c517c824e7604 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 9 Jul 2026 14:26:36 +0200 Subject: [PATCH 2/2] chore: update crossbeam-epoch to 0.9.20 for RUSTSEC-2026-0204 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84b5e1d6..7cef4c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1854,9 +1854,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ]