WIP on (no branch)

test: bridge guard account flags modification
This commit is contained in:
Roman 2026-07-09 11:35:21 +02:00
parent d1637b65e2
commit 5eb26152d5
No known key found for this signature in database
GPG Key ID: 583BDF43C238B83E
6 changed files with 189 additions and 4 deletions

View File

@ -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 {

View File

@ -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<AccountId, Account>,
) -> ValidatedStateDiff {
ValidatedStateDiff::new_unchecked(StateDiff {
signer_account_ids: Vec::new(),
public_diff,
new_commitments: Vec::new(),
new_nullifiers: Vec::new(),
program: None,
})
}

View File

@ -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,

View File

@ -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"] }

View File

@ -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.

View File

@ -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() {