From f786aca73e8576e91b092d35396dccce311693d4 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:23:21 -0400 Subject: [PATCH] refactor(lez): clean up long functions (#541) * add helper functions * CI fixes * additional refactors * fix: clarify doc comment on apply_mempool_transaction * chore: apply nightly fmt * fix: import missing ML_KEM_768_CIPHERTEXT_LEN in auth_transfer private test --- .../tests/auth_transfer/private.rs | 4 +- .../src/components/account_nonce_list.rs | 58 + lez/explorer_service/src/components/mod.rs | 8 + .../src/components/search_results.rs | 93 + .../src/components/transaction_details.rs | 150 ++ lez/explorer_service/src/pages/main_page.rs | 93 +- .../src/pages/transaction_page.rs | 262 +-- lez/indexer/core/src/block_store.rs | 97 +- lez/indexer/service/src/mock_service.rs | 132 +- lez/keycard_wallet/src/lib.rs | 109 +- lez/keycard_wallet/src/python_path.rs | 26 +- lez/sequencer/core/src/lib.rs | 1398 +------------ lez/sequencer/core/src/tests.rs | 1233 +++++++++++ lez/storage/src/indexer/mod.rs | 603 +----- lez/storage/src/indexer/tests.rs | 433 ++++ lez/storage/src/indexer/write_atomic.rs | 38 +- lez/wallet/src/account_manager.rs | 188 +- lez/wallet/src/cli/account.rs | 632 +++--- lez/wallet/src/cli/config.rs | 242 ++- lez/wallet/src/cli/group.rs | 245 +-- lez/wallet/src/cli/keycard.rs | 282 +-- lez/wallet/src/cli/mod.rs | 20 + lez/wallet/src/cli/programs/amm.rs | 365 ++-- lez/wallet/src/cli/programs/ata.rs | 306 +-- .../src/cli/programs/native_token_transfer.rs | 562 ++--- lez/wallet/src/cli/programs/pinata.rs | 35 +- lez/wallet/src/cli/programs/token.rs | 1840 +++++++++-------- lez/wallet/src/cli/programs/vault.rs | 65 +- lez/wallet/src/lib.rs | 26 + 29 files changed, 5006 insertions(+), 4539 deletions(-) create mode 100644 lez/explorer_service/src/components/account_nonce_list.rs create mode 100644 lez/explorer_service/src/components/search_results.rs create mode 100644 lez/explorer_service/src/components/transaction_details.rs create mode 100644 lez/sequencer/core/src/tests.rs create mode 100644 lez/storage/src/indexer/tests.rs diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index d8aa0ad3..49c8af37 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,8 +11,8 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, Nullifier, - NullifierPublicKey, + DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity, + ML_KEM_768_CIPHERTEXT_LEN, Nullifier, NullifierPublicKey, account::{Account, AccountWithMetadata}, compute_digest_for_path, encryption::{EphemeralPublicKey, ViewingPublicKey}, diff --git a/lez/explorer_service/src/components/account_nonce_list.rs b/lez/explorer_service/src/components/account_nonce_list.rs new file mode 100644 index 00000000..d8b30aff --- /dev/null +++ b/lez/explorer_service/src/components/account_nonce_list.rs @@ -0,0 +1,58 @@ +use indexer_service_protocol::AccountId; +use itertools::{EitherOrBoth, Itertools as _}; +use leptos::prelude::*; +use leptos_router::components::A; + +#[component] +pub fn AccountNonceList(account_ids: Vec, nonces: Vec) -> impl IntoView { + view! { +
+ {account_ids + .into_iter() + .zip_longest(nonces.into_iter()) + .map(|maybe_pair| { + match maybe_pair { + EitherOrBoth::Both(account_id, nonce) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Left(account_id) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Right(_) => { + view! { + + } + } + } + }) + .collect::>()} +
+ } +} diff --git a/lez/explorer_service/src/components/mod.rs b/lez/explorer_service/src/components/mod.rs index 306c79a8..3d0a4dae 100644 --- a/lez/explorer_service/src/components/mod.rs +++ b/lez/explorer_service/src/components/mod.rs @@ -1,7 +1,15 @@ +pub use account_nonce_list::AccountNonceList; pub use account_preview::AccountPreview; pub use block_preview::BlockPreview; +pub use search_results::SearchResultsView; +pub use transaction_details::{ + PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails, +}; pub use transaction_preview::TransactionPreview; +pub mod account_nonce_list; pub mod account_preview; pub mod block_preview; +pub mod search_results; +pub mod transaction_details; pub mod transaction_preview; diff --git a/lez/explorer_service/src/components/search_results.rs b/lez/explorer_service/src/components/search_results.rs new file mode 100644 index 00000000..1033e515 --- /dev/null +++ b/lez/explorer_service/src/components/search_results.rs @@ -0,0 +1,93 @@ +use leptos::prelude::*; + +use super::{AccountPreview, BlockPreview, TransactionPreview}; +use crate::api::SearchResults; + +/// Search results view component +#[component] +pub fn SearchResultsView(results: SearchResults) -> impl IntoView { + let SearchResults { + blocks, + transactions, + accounts, + } = results; + let has_results = !blocks.is_empty() || !transactions.is_empty() || !accounts.is_empty(); + + view! { +
+

"Search Results"

+ {if has_results { + view! { +
+ {if blocks.is_empty() { + ().into_any() + } else { + view! { +
+

"Blocks"

+
+ {blocks + .into_iter() + .map(|block| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if transactions.is_empty() { + ().into_any() + } else { + view! { +
+

"Transactions"

+
+ {transactions + .into_iter() + .map(|tx| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if accounts.is_empty() { + ().into_any() + } else { + view! { +
+

"Accounts"

+
+ {accounts + .into_iter() + .map(|(id, account)| { + view! { + + } + }) + .collect::>()} +
+
+ } + .into_any() + }} + +
+ } + .into_any() + } else { + view! {
"No results found"
} + .into_any() + }} +
+ } +} diff --git a/lez/explorer_service/src/components/transaction_details.rs b/lez/explorer_service/src/components/transaction_details.rs new file mode 100644 index 00000000..c82f7d80 --- /dev/null +++ b/lez/explorer_service/src/components/transaction_details.rs @@ -0,0 +1,150 @@ +use indexer_service_protocol::{ + PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, PublicMessage, PublicTransaction, WitnessSet, +}; +use leptos::prelude::*; + +use super::AccountNonceList; + +/// Public transaction details component +#[component] +pub fn PublicTxDetails(tx: PublicTransaction) -> impl IntoView { + let PublicTransaction { + hash: _, + message, + witness_set, + } = tx; + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = message; + let WitnessSet { + signatures_and_public_keys, + proof, + } = witness_set; + + let program_id_str = program_id.to_string(); + let proof_len = proof.map_or(0, |p| p.0.len()); + let signatures_count = signatures_and_public_keys.len(); + + view! { +
+

"Public Transaction Details"

+
+
+ "Program ID:" + {program_id_str} +
+
+ "Instruction Data:" + + {format!("{} u32 values", instruction_data.len())} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Signatures:" + {signatures_count.to_string()} +
+
+ +

"Accounts"

+ +
+ } +} + +/// Privacy-preserving transaction details component +#[component] +pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl IntoView { + let PrivacyPreservingTransaction { + hash: _, + message, + witness_set, + } = tx; + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states: _, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + block_validity_window, + timestamp_validity_window, + } = message; + let WitnessSet { + signatures_and_public_keys: _, + proof, + } = witness_set; + let proof_len = proof.map_or(0, |p| p.0.len()); + + view! { +
+

"Privacy-Preserving Transaction Details"

+
+
+ "Public Accounts:" + + {public_account_ids.len().to_string()} + +
+
+ "New Commitments:" + {new_commitments.len().to_string()} +
+
+ "Nullifiers:" + {new_nullifiers.len().to_string()} +
+
+ "Encrypted States:" + + {encrypted_private_post_states.len().to_string()} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()} +
+
+ +

"Public Accounts"

+ +
+ } +} + +/// Program deployment transaction details component +#[component] +pub fn ProgramDeploymentTxDetails(tx: ProgramDeploymentTransaction) -> impl IntoView { + let ProgramDeploymentTransaction { hash: _, message } = tx; + let ProgramDeploymentMessage { bytecode } = message; + + let bytecode_len = bytecode.len(); + view! { +
+

"Program Deployment Transaction Details"

+
+
+ "Bytecode Size:" + + {format!("{bytecode_len} bytes")} + +
+
+
+ } +} diff --git a/lez/explorer_service/src/pages/main_page.rs b/lez/explorer_service/src/pages/main_page.rs index 7e26e794..831182ce 100644 --- a/lez/explorer_service/src/pages/main_page.rs +++ b/lez/explorer_service/src/pages/main_page.rs @@ -6,8 +6,8 @@ use leptos_router::{ use web_sys::SubmitEvent; use crate::{ - api::{self, SearchResults}, - components::{AccountPreview, BlockPreview, TransactionPreview}, + api, + components::{BlockPreview, SearchResultsView}, }; const RECENT_BLOCKS_LIMIT: u64 = 10; @@ -138,93 +138,8 @@ pub fn MainPage() -> impl IntoView { .get() .and_then(|opt_results| opt_results) .map(|results| { - let SearchResults { - blocks, - transactions, - accounts, - } = results; - let has_results = !blocks.is_empty() - || !transactions.is_empty() - || !accounts.is_empty(); - view! { -
-

"Search Results"

- {if has_results { - view! { -
- {if blocks.is_empty() { - ().into_any() - } else { - view! { -
-

"Blocks"

-
- {blocks - .into_iter() - .map(|block| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if transactions.is_empty() { - ().into_any() - } else { - view! { -
-

"Transactions"

-
- {transactions - .into_iter() - .map(|tx| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if accounts.is_empty() { - ().into_any() - } else { - view! { -
-

"Accounts"

-
- {accounts - .into_iter() - .map(|(id, account)| { - view! { - - } - }) - .collect::>()} -
-
- } - .into_any() - }} - -
- } - .into_any() - } else { - view! {
"No results found"
} - .into_any() - }} -
- } - .into_any() - }) + view! { }.into_any() + }) }} diff --git a/lez/explorer_service/src/pages/transaction_page.rs b/lez/explorer_service/src/pages/transaction_page.rs index 0a3fc8e2..a99c269f 100644 --- a/lez/explorer_service/src/pages/transaction_page.rs +++ b/lez/explorer_service/src/pages/transaction_page.rs @@ -1,14 +1,13 @@ use std::str::FromStr as _; -use indexer_service_protocol::{ - HashType, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Transaction, WitnessSet, -}; -use itertools::{EitherOrBoth, Itertools as _}; +use indexer_service_protocol::{HashType, Transaction}; use leptos::prelude::*; -use leptos_router::{components::A, hooks::use_params_map}; +use leptos_router::hooks::use_params_map; -use crate::api; +use crate::{ + api, + components::{PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails}, +}; /// Transaction page component #[component] @@ -66,244 +65,21 @@ pub fn TransactionPage() -> impl IntoView { { match tx { - Transaction::Public(ptx) => { - let PublicTransaction { - hash: _, - message, - witness_set, - } = ptx; - let PublicMessage { - program_id, - account_ids, - nonces, - instruction_data, - } = message; - let WitnessSet { - signatures_and_public_keys, - proof, - } = witness_set; + Transaction::Public(ptx) => { + view! { }.into_any() + } + Transaction::PrivacyPreserving(pptx) => { + view! { }.into_any() + } + Transaction::ProgramDeployment(pdtx) => { + view! { }.into_any() + } + } + } - let program_id_str = program_id.to_string(); - let proof_len = proof.map_or(0, |p| p.0.len()); - let signatures_count = signatures_and_public_keys.len(); - - view! { -
-

"Public Transaction Details"

-
-
- "Program ID:" - {program_id_str} -
-
- "Instruction Data:" - - {format!("{} u32 values", instruction_data.len())} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Signatures:" - {signatures_count.to_string()} -
-
- -

"Accounts"

-
- {account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() + } - Transaction::PrivacyPreserving(pptx) => { - let PrivacyPreservingTransaction { - hash: _, - message, - witness_set, - } = pptx; - let PrivacyPreservingMessage { - public_account_ids, - nonces, - public_post_states: _, - encrypted_private_post_states, - new_commitments, - new_nullifiers, - block_validity_window, - timestamp_validity_window, - } = message; - let WitnessSet { - signatures_and_public_keys: _, - proof, - } = witness_set; - let proof_len = proof.map_or(0, |p| p.0.len()); - view! { -
-

"Privacy-Preserving Transaction Details"

-
-
- "Public Accounts:" - - {public_account_ids.len().to_string()} - -
-
- "New Commitments:" - {new_commitments.len().to_string()} -
-
- "Nullifiers:" - {new_nullifiers.len().to_string()} -
-
- "Encrypted States:" - - {encrypted_private_post_states.len().to_string()} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Block Validity Window:" - {block_validity_window.to_string()} -
-
- "Timestamp Validity Window:" - {timestamp_validity_window.to_string()} -
-
- -

"Public Accounts"

-
- {public_account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() - } - Transaction::ProgramDeployment(pdtx) => { - let ProgramDeploymentTransaction { - hash: _, - message, - } = pdtx; - let ProgramDeploymentMessage { bytecode } = message; - - let bytecode_len = bytecode.len(); - view! { -
-

"Program Deployment Transaction Details"

-
-
- "Bytecode Size:" - - {format!("{bytecode_len} bytes")} - -
-
-
- } - .into_any() - } - }} - - - } - .into_any() + .into_any() } Err(e) => { view! { diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index f00c94c5..c67148bd 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -211,6 +211,60 @@ mod tests { use super::*; + struct TestFixture { + storage: IndexerStore, + from: AccountId, + to: AccountId, + _home: tempfile::TempDir, + } + + #[expect( + clippy::arithmetic_side_effects, + reason = "test helper with bounded inputs" + )] + async fn store_with_transfer_blocks( + block_count: u64, + prev_hash: Option, + ) -> TestFixture { + let home = tempdir().unwrap(); + let storage = IndexerStore::open_db(home.path()).unwrap(); + + let initial_accounts = initial_pub_accounts_private_keys(); + let from = initial_accounts[0].account_id; + let to = initial_accounts[1].account_id; + let sign_key = initial_accounts[0].pub_sign_key.clone(); + + let mut prev_hash = prev_hash; + for i in 0..block_count { + let tx = common::test_utils::create_transaction_native_token_transfer( + from, + u128::from(i), + to, + 10, + &sign_key, + ); + let block_id = i + 1; + + let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); + prev_hash = Some(next_block.header.hash); + + storage + .put_block( + next_block, + HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), + ) + .await + .unwrap(); + } + + TestFixture { + storage, + from, + to, + _home: home, + } + } + #[test] fn correct_startup() { let home = tempdir().unwrap(); @@ -225,7 +279,6 @@ mod tests { #[tokio::test] async fn state_transition() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); let initial_accounts = initial_pub_accounts_private_keys(); @@ -233,7 +286,6 @@ mod tests { let to = initial_accounts[1].account_id; let sign_key = initial_accounts[0].pub_sign_key.clone(); - // Submit genesis block let clock_tx = LeeTransaction::Public(clock_invocation(0)); let genesis_block_data = HashableBlockData { block_id: 1, @@ -249,15 +301,13 @@ mod tests { .await .unwrap(); - for i in 0..10 { + for i in 0..10_u128 { let tx = common::test_utils::create_transaction_native_token_transfer( from, i, to, 10, &sign_key, ); let block_id = u64::try_from(i + 1).unwrap(); - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); prev_hash = Some(next_block.header.hash); - storage .put_block( next_block, @@ -276,48 +326,23 @@ mod tests { #[tokio::test] async fn account_state_at_block() { - let home = tempdir().unwrap(); + let TestFixture { + storage, + from, + to, + _home, + } = store_with_transfer_blocks(10, None).await; - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); - - let mut prev_hash = None; - - let initial_accounts = initial_pub_accounts_private_keys(); - let from = initial_accounts[0].account_id; - let to = initial_accounts[1].account_id; - let sign_key = initial_accounts[0].pub_sign_key.clone(); - - for i in 0..10 { - let tx = common::test_utils::create_transaction_native_token_transfer( - from, i, to, 10, &sign_key, - ); - let block_id = u64::try_from(i + 1).unwrap(); - - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); - prev_hash = Some(next_block.header.hash); - - storage - .put_block( - next_block, - HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), - ) - .await - .unwrap(); - } - - // Genesis block: no transfers applied yet. let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap(); let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap(); assert_eq!(acc1_at_1.balance, 9990); assert_eq!(acc2_at_1.balance, 20010); - // After block 5: 4 transfers of 10 applied (one each in blocks 2..=5). let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap(); let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap(); assert_eq!(acc1_at_5.balance, 9950); assert_eq!(acc2_at_5.balance, 20050); - // After final block 9: 8 transfers applied; should match current state. let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap(); let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap(); assert_eq!(acc1_at_9.balance, 9910); diff --git a/lez/indexer/service/src/mock_service.rs b/lez/indexer/service/src/mock_service.rs index d9ab9484..7bf6c528 100644 --- a/lez/indexer/service/src/mock_service.rs +++ b/lez/indexer/service/src/mock_service.rs @@ -330,6 +330,76 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { } } +fn mock_public_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: ProgramId([1_u32; 8]), + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: None, + }, + }) +} + +fn mock_privacy_preserving_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: Some(indexer_service_protocol::Proof(vec![0; 32])), + }, + }) +} + +fn mock_program_deployment_tx(tx_hash: HashType) -> Transaction { + Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], + }, + }) +} + fn build_mock_block( block_id: BlockId, prev_hash: HashType, @@ -344,7 +414,6 @@ fn build_mock_block( HashType(hash) }; - // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment) let num_txs = 2 + (block_id % 3); let mut block_transactions = Vec::new(); @@ -356,65 +425,10 @@ fn build_mock_block( HashType(hash) }; - // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment let tx = match (block_id + tx_idx) % 5 { - // Public transactions (most common) - 0 | 1 => Transaction::Public(PublicTransaction { - hash: tx_hash, - message: PublicMessage { - program_id: ProgramId([1_u32; 8]), - account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - account_ids[(tx_idx as usize + 1) % account_ids.len()], - ], - nonces: vec![block_id as u128, (block_id + 1) as u128], - instruction_data: vec![1, 2, 3, 4], - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: None, - }, - }), - // PrivacyPreserving transactions - 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { - hash: tx_hash, - message: PrivacyPreservingMessage { - public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], - nonces: vec![block_id as u128], - public_post_states: vec![Account { - program_owner: ProgramId([1_u32; 8]), - balance: 500, - data: Data(vec![0xdd, 0xee]), - nonce: block_id as u128, - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: indexer_service_protocol::Ciphertext(vec![ - 0x01, 0x02, 0x03, 0x04, - ]), - epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), - view_tag: 42, - }], - new_commitments: vec![Commitment([block_id as u8; 32])], - new_nullifiers: vec![( - indexer_service_protocol::Nullifier([tx_idx as u8; 32]), - CommitmentSetDigest([0xff; 32]), - )], - block_validity_window: ValidityWindow((None, None)), - timestamp_validity_window: ValidityWindow((None, None)), - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: Some(indexer_service_protocol::Proof(vec![0; 32])), - }, - }), - // ProgramDeployment transactions (rare) - _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { - hash: tx_hash, - message: ProgramDeploymentMessage { - bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic - * number */ - }, - }), + 0 | 1 => mock_public_tx(tx_hash, block_id, tx_idx, account_ids), + 2 | 3 => mock_privacy_preserving_tx(tx_hash, block_id, tx_idx, account_ids), + _ => mock_program_deployment_tx(tx_hash), }; block_transactions.push(tx); diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index 73486392..a3ecd140 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -134,10 +134,6 @@ impl KeycardWallet { }) } - #[expect( - clippy::arithmetic_side_effects, - reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" - )] pub fn sign_message_for_path( &self, py: Python, @@ -150,33 +146,9 @@ impl KeycardWallet { .call_method1("sign_message_for_path", (message, path))? .extract()?; - // The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. - // Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). - let py_signature = if py_signature.len() < 64 { - if py_signature.len() < 32 { - return Err(PyErr::new::(format!( - "signature from keycard too short: {} bytes", - py_signature.len() - ))); - } - let s_stripped = &py_signature[32..]; - let mut padded = [0_u8; 64]; - padded[..32].copy_from_slice(&py_signature[..32]); - padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); - padded.to_vec() - } else { - py_signature + let sig = Signature { + value: normalize_keycard_signature(py_signature)?, }; - - let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", - vec.len(), - vec - )) - })?; - - let sig = Signature { value: signature }; let pub_key = self.get_public_key_for_path(py, path)?; if !sig.is_valid_for(message, &pub_key) { return Err(PyErr::new::( @@ -224,32 +196,8 @@ impl KeycardWallet { .call_method1("get_private_keys_for_path", (path,))? .extract()?; - let raw_nsk = Zeroizing::new(raw_nsk); - let raw_vsk = Zeroizing::new(raw_vsk); - - let nsk = { - if raw_nsk.len() != 32 { - return Err(PyErr::new::(format!( - "expected 32-byte NSK from keycard, got {} bytes", - raw_nsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 32]); - arr.copy_from_slice(&raw_nsk); - arr - }; - - let vsk = { - if raw_vsk.len() != 64 { - return Err(PyErr::new::(format!( - "expected 64-byte VSK from keycard, got {} bytes", - raw_vsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 64]); - arr.copy_from_slice(&raw_vsk); - arr - }; + let nsk = zeroizing_fixed_bytes::<32>("nullifier secret key", Zeroizing::new(raw_nsk))?; + let vsk = zeroizing_fixed_bytes::<64>("view secret key", Zeroizing::new(raw_vsk))?; Ok((nsk, vsk)) } @@ -269,6 +217,55 @@ impl KeycardWallet { } } +/// The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. +/// Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). +#[expect( + clippy::arithmetic_side_effects, + reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" +)] +fn normalize_keycard_signature(py_signature: Vec) -> PyResult<[u8; 64]> { + if py_signature.len() < 64 { + if py_signature.len() < 32 { + return Err(PyErr::new::(format!( + "signature from keycard too short: {} bytes", + py_signature.len() + ))); + } + let s_stripped = &py_signature[32..]; + let mut padded = [0_u8; 64]; + padded[..32].copy_from_slice(&py_signature[..32]); + padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); + Ok(padded) + } else { + py_signature.try_into().map_err(|vec: Vec| { + PyErr::new::(format!( + "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", + vec.len(), + vec + )) + }) + } +} + +#[expect( + clippy::needless_pass_by_value, + reason = "Zeroizing> is consumed to ensure the source is zeroed on drop" +)] +fn zeroizing_fixed_bytes( + label: &str, + raw: Zeroizing>, +) -> PyResult> { + if raw.len() != N { + return Err(PyErr::new::(format!( + "expected {N}-byte {label} from keycard, got {} bytes", + raw.len() + ))); + } + let mut arr = Zeroizing::new([0_u8; N]); + arr.copy_from_slice(&raw); + Ok(arr) +} + fn pairing_file_path() -> Option { let home = std::env::var("LEE_WALLET_HOME_DIR") .map(PathBuf::from) diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs index 99ed936e..61196ad5 100644 --- a/lez/keycard_wallet/src/python_path.rs +++ b/lez/keycard_wallet/src/python_path.rs @@ -2,8 +2,7 @@ use std::{env, path::PathBuf}; use pyo3::{prelude::*, types::PyList}; -/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. -pub fn add_python_path(py: Python<'_>) -> PyResult<()> { +fn collect_python_paths() -> Vec { let current_dir = env::current_dir().expect("Failed to get current working directory"); let python_base = env::var("VIRTUAL_ENV") @@ -11,7 +10,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .and_then(|v| PathBuf::from(v).parent().map(PathBuf::from)) .unwrap_or_else(|| current_dir.clone()); - let mut paths_to_add: Vec = vec![ + let mut paths = vec![ python_base .join("lez") .join("keycard_wallet") @@ -23,24 +22,28 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .join("keycard-py"), ]; - // If a virtualenv is active, add its site-packages so that dependencies - // installed in the venv (e.g. smartcard, ecdsa) are importable by the - // pyo3 embedded interpreter, which does not inherit sys.path from the - // shell's `python3` executable. + // pyo3's embedded interpreter does not inherit sys.path from the shell, + // so venv site-packages must be added explicitly. if let Ok(venv) = env::var("VIRTUAL_ENV") { let lib = PathBuf::from(&venv).join("lib"); if let Ok(entries) = std::fs::read_dir(&lib) { for entry in entries.flatten() { let site_packages = entry.path().join("site-packages"); if site_packages.exists() { - paths_to_add.push(site_packages); + paths.push(site_packages); } } } } - // Sanity check — warns early if a path doesn't exist - for path in &paths_to_add { + paths +} + +/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. +pub fn add_python_path(py: Python<'_>) -> PyResult<()> { + let paths = collect_python_paths(); + + for path in &paths { if !path.exists() { log::info!("Warning: Python path does not exist: {}", path.display()); } @@ -50,10 +53,9 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { let binding = sys.getattr("path")?; let sys_path = binding.cast::()?; - for path in &paths_to_add { + for path in &paths { let path_str = path.to_str().expect("Invalid path"); - // Avoid duplicating the path let already_present = sys_path .iter() .any(|p| p.extract::<&str>().is_ok_and(|s| s == path_str)); diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 35c0c33b..75dcc1a9 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -36,6 +36,7 @@ pub mod config; pub mod mock; /// The origin of a transaction. +#[derive(Clone, Copy)] pub enum TransactionOrigin { /// Basic transactions submitted by users via RPC. User, @@ -69,24 +70,17 @@ impl SequencerCore { /// assumed to represent the correct latest state consistent with Bedrock-finalized data. /// If no database is found, the sequencer performs a fresh start from genesis, /// initializing its state with the accounts defined in the configuration file. - pub async fn start_from_config( - config: SequencerConfig, - ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + fn open_or_create_store(config: &SequencerConfig) -> (SequencerStore, lee::V03State, Block) { let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - - let bedrock_signing_key = - load_or_create_signing_key(&config.home.join("bedrock_signing_key")) - .expect("Failed to load or create bedrock signing key"); - let db_path = config.home.join("rocksdb"); - let (store, state, genesis_block) = if db_path.exists() { - let store = - SequencerStore::open_db(&db_path, signing_key.clone()).unwrap_or_else(|err| { - panic!( - "Failed to open database at {} with error: {err}", - db_path.display() - ) - }); + + if db_path.exists() { + let store = SequencerStore::open_db(&db_path, signing_key).unwrap_or_else(|err| { + panic!( + "Failed to open database at {} with error: {err}", + db_path.display() + ) + }); let state = store .get_lee_state() .expect("Failed to read state from store"); @@ -101,7 +95,7 @@ impl SequencerCore { db_path.display() ); - let (genesis_state, genesis_txs) = build_genesis_state(&config); + let (genesis_state, genesis_txs) = build_genesis_state(config); let hashable_data = HashableBlockData { block_id: GENESIS_BLOCK_ID, @@ -120,7 +114,17 @@ impl SequencerCore { .expect("Failed to create database with genesis block"); (store, genesis_state, genesis_block) - }; + } + } + + pub async fn start_from_config( + config: SequencerConfig, + ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + let bedrock_signing_key = + load_or_create_signing_key(&config.home.join("bedrock_signing_key")) + .expect("Failed to load or create bedrock signing key"); + + let (store, state, genesis_block) = Self::open_or_create_store(&config); let latest_block_meta = store .latest_block_meta() @@ -331,8 +335,60 @@ impl SequencerCore { Ok(self.chain_height) } - /// Builds a new block from transactions in the mempool. - /// Does NOT publish or store the block — the caller is responsible for that. + /// Validates and applies a single mempool transaction to the current state. + /// Returns `Ok(true)` if the transaction was valid and applied, `Ok(false)` if + /// it was skipped due to validation failure. + fn apply_mempool_transaction( + &mut self, + origin: TransactionOrigin, + tx: &LeeTransaction, + block_height: u64, + timestamp: u64, + deposit_event_ids: &mut Vec, + withdrawals: &mut Vec, + ) -> Result { + let tx_hash = tx.hash(); + match origin { + TransactionOrigin::User => { + let validated_diff = match tx.validate_on_state( + &self.state, + block_height, + timestamp, + ) { + Ok(diff) => diff, + Err(err) => { + error!( + "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", + ); + return Ok(false); + } + }; + + if let Some(withdraw_data) = extract_bridge_withdraw_data(tx) { + withdrawals.push(withdraw_data); + } + + self.state.apply_state_diff(validated_diff); + } + TransactionOrigin::Sequencer => { + let LeeTransaction::Public(public_tx) = tx else { + panic!("Sequencer may only generate Public transactions, found {tx:#?}"); + }; + + if let Some(deposit_op_id) = extract_bridge_deposit_id(tx) { + deposit_event_ids.push(deposit_op_id); + } + + self.state + .transition_from_public_transaction(public_tx, block_height, timestamp) + .context("Failed to execute sequencer-generated transaction")?; + } + } + + info!("Validated transaction with hash {tx_hash}, including it in block"); + Ok(true) + } + fn build_block_from_mempool(&mut self) -> Result { let now = Instant::now(); @@ -353,14 +409,12 @@ impl SequencerCore { let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis()) .expect("Timestamp must be positive"); - // Pre-create the mandatory clock tx so its size is included in the block size check. let clock_tx = clock_invocation(new_block_timestamp); let clock_lee_tx = LeeTransaction::Public(clock_tx.clone()); while let Some((origin, tx)) = self.mempool.pop() { let tx_hash = tx.hash(); - // Check if block size exceeds limit (including the mandatory clock tx). let temp_valid_transactions = [ valid_transactions.as_slice(), std::slice::from_ref(&tx), @@ -379,66 +433,30 @@ impl SequencerCore { .len(); if block_size > max_block_size { - // Block would exceed size limit, remove last transaction and push back warn!( "Transaction with hash {tx_hash} deferred to next block: \ block size {block_size} bytes would exceed limit of {max_block_size} bytes", ); - self.mempool.push_front((origin, tx)); break; } - match origin { - TransactionOrigin::User => { - let validated_diff = match tx.validate_on_state( - &self.state, - new_block_height, - new_block_timestamp, - ) { - Ok(diff) => diff, - Err(err) => { - error!( - "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", - ); - continue; - } - }; - - if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) { - withdrawals.push(withdraw_data); - } - - self.state.apply_state_diff(validated_diff); - } - TransactionOrigin::Sequencer => { - let LeeTransaction::Public(public_tx) = &tx else { - panic!("Sequencer may only generate Public transactions, found {tx:#?}"); - }; - - if let Some(deposit_op_id) = extract_bridge_deposit_id(&tx) { - deposit_event_ids.push(deposit_op_id); - } - - self.state - .transition_from_public_transaction( - public_tx, - new_block_height, - new_block_timestamp, - ) - .context("Failed to execute sequencer-generated transaction")?; - } + if self.apply_mempool_transaction( + origin, + &tx, + new_block_height, + new_block_timestamp, + &mut deposit_event_ids, + &mut withdrawals, + )? { + valid_transactions.push(tx); } - valid_transactions.push(tx); - - info!("Validated transaction with hash {tx_hash}, including it in block"); if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { break; } } - // Append the Clock Program invocation as the mandatory last transaction. self.state .transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp) .context("Clock transaction failed. Aborting block production.")?; @@ -803,1248 +821,4 @@ fn load_or_create_signing_key(path: &Path) -> Result { #[cfg(test)] #[cfg(feature = "mock")] -mod tests { - #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] - - use std::{pin::pin, time::Duration}; - - use common::{ - HashType, - block::HashableBlockData, - test_utils::sequencer_sign_key_for_testing, - transaction::{LeeTransaction, clock_invocation}, - }; - use key_protocol::key_management::KeyChain; - use lee::{ - Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction, PrivateKey, - PublicKey, PublicTransaction, SharedSecretKey, V03State, - error::LeeError, - execute_and_prove, - privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, - program::Program, - }; - use lee_core::{ - Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, - account::{AccountWithMetadata, Nonce}, - program::PdaSeed, - }; - use logos_blockchain_core::mantle::ops::channel::ChannelId; - use mempool::MemPoolHandle; - use storage::sequencer::sequencer_cells::PendingDepositEventRecord; - use tempfile::tempdir; - use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts}; - - use crate::{ - TransactionOrigin, - block_store::SequencerStore, - build_genesis_state, - config::{BedrockConfig, SequencerConfig}, - mock::SequencerCoreWithMockClients, - }; - - #[derive(borsh::BorshSerialize)] - struct DepositMetadataForEncoding { - recipient_id: lee::AccountId, - } - - fn setup_sequencer_config() -> SequencerConfig { - let tempdir = tempfile::tempdir().unwrap(); - let home = tempdir.path().to_path_buf(); - - SequencerConfig { - home, - max_num_tx_in_block: 10, - max_block_size: bytesize::ByteSize::mib(1), - mempool_max_size: 10000, - block_create_timeout: Duration::from_secs(1), - signing_key: *sequencer_sign_key_for_testing().value(), - bedrock_config: BedrockConfig { - channel_id: ChannelId::from([0; 32]), - node_url: "http://not-used-in-unit-tests".parse().unwrap(), - auth: None, - }, - retry_pending_blocks_timeout: Duration::from_mins(4), - genesis: vec![], - } - } - - fn create_signing_key_for_account1() -> lee::PrivateKey { - initial_pub_accounts_private_keys()[0].pub_sign_key.clone() - } - - fn create_signing_key_for_account2() -> lee::PrivateKey { - initial_pub_accounts_private_keys()[1].pub_sign_key.clone() - } - - async fn common_setup() -> ( - SequencerCoreWithMockClients, - MemPoolHandle<(TransactionOrigin, LeeTransaction)>, - ) { - let config = setup_sequencer_config(); - common_setup_with_config(config).await - } - - async fn common_setup_with_config( - config: SequencerConfig, - ) -> ( - SequencerCoreWithMockClients, - MemPoolHandle<(TransactionOrigin, LeeTransaction)>, - ) { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - sequencer.produce_new_block().await.unwrap(); - - (sequencer, mempool_handle) - } - - fn tx_is_bridge_deposit( - tx: &LeeTransaction, - deposit_op_id: [u8; 32], - expected_amount: u64, - ) -> bool { - let LeeTransaction::Public(public_tx) = tx else { - return false; - }; - - if public_tx.message.program_id != programs::bridge().id() { - return false; - } - - let instruction: bridge_core::Instruction = - match risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) { - Ok(instruction) => instruction, - Err(_err) => return false, - }; - - matches!( - instruction, - bridge_core::Instruction::Deposit { - l1_deposit_op_id, - amount, - .. - } if l1_deposit_op_id == deposit_op_id && amount == expected_amount - ) - } - - #[tokio::test] - async fn start_from_config() { - let config = setup_sequencer_config(); - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - assert_eq!(sequencer.chain_height, 1); - assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); - - let acc1_account_id = initial_public_user_accounts()[0].account_id; - let acc2_account_id = initial_public_user_accounts()[1].account_id; - - let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; - let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; - - assert_eq!(10000, balance_acc_1); - assert_eq!(20000, balance_acc_2); - } - - #[tokio::test] - async fn start_from_config_opens_existing_db_if_it_exists() { - let config = setup_sequencer_config(); - let temp_dir = tempdir().unwrap(); - let mut config = config; - config.home = temp_dir.path().to_path_buf(); - - let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - let (genesis_state, genesis_txs) = build_genesis_state(&config); - let genesis_hashable_data = HashableBlockData { - block_id: 1, - transactions: genesis_txs, - prev_block_hash: HashType([0; 32]), - timestamp: 0, - }; - let genesis_block = genesis_hashable_data.into_pending_block(&signing_key); - - SequencerStore::create_db_with_genesis( - &config.home.join("rocksdb"), - &genesis_block, - &genesis_state, - signing_key, - ) - .unwrap(); - - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - assert_eq!(sequencer.chain_height, 1); - assert!(sequencer.store.latest_block_meta().is_ok()); - } - - #[should_panic(expected = "Failed to open database")] - #[tokio::test] - async fn start_from_config_panics_when_db_open_returns_non_not_found_error() { - let mut config = setup_sequencer_config(); - let temp_dir = tempdir().unwrap(); - config.home = temp_dir.path().to_path_buf(); - - let db_path = config.home.join("rocksdb"); - - std::fs::create_dir_all(&config.home).unwrap(); - // Force RocksDB open to fail with an IO error by placing a file at DB path. - std::fs::write(&db_path, b"not-a-directory").unwrap(); - - let _ = SequencerCoreWithMockClients::start_from_config(config).await; - } - - #[tokio::test] - async fn start_from_config_replays_unfulfilled_deposit_events_from_db() { - let config = setup_sequencer_config(); - let deposit_op_id = [13_u8; 32]; - let expected_amount = 1_u64; - let recipient_id = initial_public_user_accounts()[0].account_id; - - { - let (_sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - } - - let pending_event = PendingDepositEventRecord { - deposit_op_id: HashType(deposit_op_id), - source_tx_hash: HashType([7_u8; 32]), - amount: expected_amount, - metadata: borsh::to_vec(&DepositMetadataForEncoding { recipient_id }).unwrap(), - submitted_in_block_id: None, - }; - - { - let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - let store = SequencerStore::open_db(&config.home.join("rocksdb"), signing_key).unwrap(); - - let inserted = store - .dbio() - .add_pending_deposit_event(pending_event) - .unwrap(); - assert!(inserted); - } - - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - - let (origin, tx) = tokio::time::timeout(Duration::from_secs(5), async { - loop { - if let Some((origin, tx)) = sequencer.mempool.pop() { - return (origin, tx); - } - - tokio::time::sleep(Duration::from_millis(100)).await; - } - }) - .await - .expect("Timed out waiting for pending deposit event to be replayed into mempool"); - - match origin { - TransactionOrigin::Sequencer => {} - TransactionOrigin::User => { - panic!("Unexpected user transaction in empty mempool replay test") - } - } - - assert!(tx_is_bridge_deposit(&tx, deposit_op_id, expected_amount)); - - let pending_events = sequencer.store.get_unfulfilled_deposit_events().unwrap(); - let replayed_event = pending_events - .into_iter() - .find(|event| event.deposit_op_id == HashType(deposit_op_id)) - .expect("Pending deposit event should remain in DB until included in a block"); - assert!(replayed_event.submitted_in_block_id.is_none()); - } - - #[test] - fn transaction_pre_check_pass() { - let tx = common::test_utils::produce_dummy_empty_transaction(); - let result = tx.transaction_stateless_check(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_valid() { - let (_sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10, &sign_key1, - ); - let result = tx.transaction_stateless_check(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_other_signature() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key2 = create_signing_key_for_account2(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10, &sign_key2, - ); - - // Signature is valid, stateless check pass - let tx = tx.transaction_stateless_check().unwrap(); - - // Signature is not from sender. Execution fails - let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0); - - assert!(matches!( - result, - Err(lee::error::LeeError::ProgramExecutionFailed(_)) - )); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_sent_too_much() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10_000_000, &sign_key1, - ); - - let result = tx.transaction_stateless_check(); - - // Passed pre-check - assert!(result.is_ok()); - - let result = result - .unwrap() - .execute_check_on_state(&mut sequencer.state, 0, 0); - let is_failed_at_balance_mismatch = matches!( - result.err().unwrap(), - lee::error::LeeError::ProgramExecutionFailed(_) - ); - - assert!(is_failed_at_balance_mismatch); - } - - #[tokio::test] - async fn transaction_execute_native_transfer() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - tx.execute_check_on_state(&mut sequencer.state, 0, 0) - .unwrap(); - - let bal_from = sequencer.state.get_account_by_id(acc1).balance; - let bal_to = sequencer.state.get_account_by_id(acc2).balance; - - assert_eq!(bal_from, 9900); - assert_eq!(bal_to, 20100); - } - - #[tokio::test] - async fn push_tx_into_mempool_blocks_until_mempool_is_full() { - let config = SequencerConfig { - mempool_max_size: 1, - ..setup_sequencer_config() - }; - let (mut sequencer, mempool_handle) = common_setup_with_config(config).await; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - - // Fill the mempool - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - - // Check that pushing another transaction will block - let mut push_fut = pin!(mempool_handle.push((TransactionOrigin::User, tx.clone()))); - let poll = futures::poll!(push_fut.as_mut()); - assert!(poll.is_pending()); - - // Empty the mempool by producing a block - sequencer.produce_new_block().await.unwrap(); - - // Resolve the pending push - assert!(push_fut.await.is_ok()); - } - - #[tokio::test] - async fn build_block_from_mempool() { - let (mut sequencer, mempool_handle) = common_setup().await; - let genesis_height = sequencer.chain_height; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - let result = sequencer.build_block_from_mempool(); - assert!(result.is_ok()); - assert_eq!(sequencer.chain_height, genesis_height + 1); - } - - #[tokio::test] - async fn replay_transactions_are_rejected_in_the_same_block() { - let (mut sequencer, mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - let tx_original = tx.clone(); - let tx_replay = tx.clone(); - // Pushing two copies of the same tx to the mempool - mempool_handle - .push((TransactionOrigin::User, tx_original)) - .await - .unwrap(); - mempool_handle - .push((TransactionOrigin::User, tx_replay)) - .await - .unwrap(); - - // Create block - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // Only one user tx should be included; the clock tx is always appended last. - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - } - - #[tokio::test] - async fn replay_transactions_are_rejected_in_different_blocks() { - let (mut sequencer, mempool_handle) = common_setup().await; - - let acc1 = initial_public_user_accounts()[0].account_id; - let acc2 = initial_public_user_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - // The transaction should be included the first time - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - - // Add same transaction should fail - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - // The replay is rejected, so only the clock tx is in the block. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn restart_from_storage() { - let config = setup_sequencer_config(); - let acc1_account_id = initial_public_user_accounts()[0].account_id; - let acc2_account_id = initial_public_user_accounts()[1].account_id; - let balance_to_move = 13; - - // In the following code block a transaction will be processed that moves `balance_to_move` - // from `acc_1` to `acc_2`. The block created with that transaction will be kept stored in - // the temporary directory for the block storage of this test. - { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let signing_key = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 0, - acc2_account_id, - balance_to_move, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - } - - // Instantiating a new sequencer from the same config. This should load the existing block - // with the above transaction and update the state to reflect that. - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; - let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; - - // Balances should be consistent with the stored block - assert_eq!( - balance_acc_1, - initial_public_user_accounts()[0].balance - balance_to_move - ); - assert_eq!( - balance_acc_2, - initial_public_user_accounts()[1].balance + balance_to_move - ); - } - - #[tokio::test] - async fn get_pending_blocks() { - let config = setup_sequencer_config(); - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4); - } - - #[tokio::test] - async fn delete_blocks() { - let config = setup_sequencer_config(); - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let last_finalized_block = 3; - sequencer - .clean_finalized_blocks_from_db(last_finalized_block) - .unwrap(); - - assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1); - } - - #[tokio::test] - async fn produce_block_with_correct_prev_meta_after_restart() { - let config = setup_sequencer_config(); - let acc1_account_id = initial_public_user_accounts()[0].account_id; - let acc2_account_id = initial_public_user_accounts()[1].account_id; - - // Step 1: Create initial database with some block metadata - let expected_prev_meta = { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - let signing_key = create_signing_key_for_account1(); - - // Add a transaction and produce a block to set up block metadata - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 0, - acc2_account_id, - 100, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - // Get the metadata of the last block produced - sequencer.store.latest_block_meta().unwrap() - }; - - // Step 2: Restart sequencer from the same storage - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - // Step 3: Submit a new transaction - let signing_key = create_signing_key_for_account1(); - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 1, // Next nonce - acc2_account_id, - 50, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - - // Step 4: Produce new block - sequencer.produce_new_block().await.unwrap(); - - // Step 5: Verify the new block has correct previous block metadata - let new_block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - assert_eq!( - new_block.header.prev_block_hash, expected_prev_meta.hash, - "New block's prev_block_hash should match the stored metadata hash" - ); - assert_eq!( - new_block.body.transactions, - vec![ - tx, - LeeTransaction::Public(clock_invocation(new_block.header.timestamp)) - ], - "New block should contain the submitted transaction and the clock invocation" - ); - } - - #[tokio::test] - async fn transactions_touching_clock_account_are_dropped_from_block() { - let (mut sequencer, mempool_handle) = common_setup().await; - - // Canonical clock invocation and a crafted variant with a different timestamp — both must - // be dropped because their diffs touch the clock accounts. - let crafted_clock_tx = { - let message = lee::public_transaction::Message::try_new( - programs::clock().id(), - system_accounts::clock_account_ids().to_vec(), - vec![], - 42_u64, - ) - .unwrap(); - LeeTransaction::Public(lee::PublicTransaction::new( - message, - lee::public_transaction::WitnessSet::from_raw_parts(vec![]), - )) - }; - mempool_handle - .push(( - TransactionOrigin::User, - LeeTransaction::Public(clock_invocation(0)), - )) - .await - .unwrap(); - mempool_handle - .push((TransactionOrigin::User, crafted_clock_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // Both transactions were dropped. Only the system-appended clock tx remains. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn user_tx_that_chain_calls_clock_is_dropped() { - let (mut sequencer, mempool_handle) = common_setup().await; - - let clock_chain_caller = test_programs::clock_chain_caller(); - // Deploy the clock_chain_caller test program. - let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new(clock_chain_caller.elf().to_owned()), - )); - mempool_handle - .push((TransactionOrigin::User, deploy_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the - // clock program with the clock accounts. The sequencer should detect that the resulting - // state diff modifies clock accounts and drop the transaction. - let clock_chain_caller_id = test_programs::clock_chain_caller().id(); - let clock_program_id = programs::clock().id(); - let timestamp: u64 = 0; - - let message = lee::public_transaction::Message::try_new( - clock_chain_caller_id, - system_accounts::clock_account_ids().to_vec(), - vec![], // no signers - (clock_program_id, timestamp), - ) - .unwrap(); - let user_tx = LeeTransaction::Public(lee::PublicTransaction::new( - message, - lee::public_transaction::WitnessSet::from_raw_parts(vec![]), - )); - - mempool_handle - .push((TransactionOrigin::User, user_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // The user tx must have been dropped; only the mandatory clock invocation remains. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn block_production_aborts_when_clock_account_data_is_corrupted() { - let (mut sequencer, mempool_handle) = common_setup().await; - - // Corrupt the clock 01 account data so the clock program panics on deserialization. - let clock_account_id = system_accounts::clock_account_ids()[0]; - let mut corrupted = sequencer.state.get_account_by_id(clock_account_id); - corrupted.data = vec![0xff; 3].try_into().unwrap(); - sequencer - .state - .force_insert_account(clock_account_id, corrupted); - - // Push a dummy transaction so the mempool is non-empty. - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - // Block production must fail because the appended clock tx cannot execute. - let result = sequencer.produce_new_block().await; - assert!( - result.is_err(), - "Block production should abort when clock account data is corrupted" - ); - } - - #[test] - fn private_bridge_withdraw_invocation_is_dropped() { - let sender_keys = KeyChain::new_os_random(); - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.nullifier_public_key, 0); - let sender_private_account = Account { - program_owner: programs::authenticated_transfer().id(), - balance: 100, - nonce: Nonce(0xdead_beef), - data: Data::default(), - }; - let bridge_account_id = system_accounts::bridge_account_id(); - - let mut state = V03State::new() - .with_public_accounts([(bridge_account_id, system_accounts::bridge_account())]) - .with_private_accounts([( - Commitment::new(&sender_account_id, &sender_private_account), - Nullifier::for_account_initialization(&sender_account_id), - )]); - - let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); - - let sender_pre = AccountWithMetadata::new( - sender_private_account, - true, - (&sender_keys.nullifier_public_key, 0), - ); - let bridge_pre = AccountWithMetadata::new( - state.get_account_by_id(bridge_account_id), - false, - bridge_account_id, - ); - - let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0; - - let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { - amount: 1, - bedrock_account_pk: [0; 32], - }) - .unwrap(); - - let program_with_deps = ProgramWithDependencies::new( - programs::bridge(), - [( - programs::authenticated_transfer().id(), - programs::authenticated_transfer(), - )] - .into(), - ); - - let (output, proof) = execute_and_prove( - vec![sender_pre, bridge_pre], - instruction, - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - epk: EphemeralPublicKey(vec![12_u8; 1088]), - view_tag: EncryptedAccountData::compute_view_tag( - &sender_keys.nullifier_public_key, - &sender_keys.viewing_public_key, - ), - ssk: shared_secret, - nsk: sender_keys.private_key_holder.nullifier_secret_key, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::Public, - ], - &program_with_deps, - ) - .expect("Execution should succeed"); - - let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) - .expect("Message construction should succeed"); - let witness_set = - lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); - let tx = LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new( - message, - witness_set, - )); - let res = tx.execute_check_on_state(&mut state, 1, 0); - - assert!( - matches!(res, Err(LeeError::InvalidInput(_))), - "Bridge withdraw invocation should be rejected in private execution" - ); - } - - /// Builds a [`V03State`] with the clock program and `program` registered, the three clock - /// accounts initialized, and the clock advanced to `clock_timestamp` so that reads of the - /// `CLOCK_01` account observe it. - fn state_with_clock_and_program(program: Program, clock_timestamp: u64) -> V03State { - let mut state = V03State::new().with_programs([programs::clock(), program]); - for clock_id in system_accounts::clock_account_ids() { - state.force_insert_account(clock_id, system_accounts::clock_account()); - } - state - .transition_from_public_transaction( - &clock_invocation(clock_timestamp), - 1, - clock_timestamp, - ) - .expect("Clock invocation should advance the clock"); - state - } - - fn time_locked_transfer_transaction( - from: AccountId, - from_key: &PrivateKey, - from_nonce: u128, - to: AccountId, - clock_account_id: AccountId, - amount: u128, - deadline: u64, - ) -> PublicTransaction { - let program_id = test_programs::time_locked_transfer().id(); - let message = lee::public_transaction::Message::try_new( - program_id, - vec![from, to, clock_account_id], - vec![Nonce(from_nonce)], - (amount, deadline), - ) - .unwrap(); - let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[from_key]); - PublicTransaction::new(message, witness_set) - } - - #[test] - fn time_locked_transfer_succeeds_when_deadline_has_passed() { - let clock_timestamp = 600; - let mut state = - state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); - - // The recipient must be a non-default account so the program may credit it without - // claiming it. - let recipient_id = AccountId::new([42; 32]); - state.force_insert_account( - recipient_id, - Account { - program_owner: programs::authenticated_transfer().id(), - ..Account::default() - }, - ); - - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); - state.force_insert_account( - sender_id, - Account { - program_owner: test_programs::time_locked_transfer().id(), - balance: 100, - ..Account::default() - }, - ); - - let amount = 100; - // Deadline is in the past relative to the clock, so the transfer is unlocked. - let deadline = 0; - - let tx = time_locked_transfer_transaction( - sender_id, - &key1, - 0, - recipient_id, - system_accounts::clock_account_ids()[0], - amount, - deadline, - ); - - state - .transition_from_public_transaction(&tx, 2, clock_timestamp) - .unwrap(); - - // Balances changed. - assert_eq!(state.get_account_by_id(sender_id).balance, 0); - assert_eq!(state.get_account_by_id(recipient_id).balance, 100); - } - - #[test] - fn time_locked_transfer_fails_when_deadline_is_in_the_future() { - let clock_timestamp = 600; - let mut state = - state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); - - let recipient_id = AccountId::new([42; 32]); - state.force_insert_account( - recipient_id, - Account { - program_owner: programs::authenticated_transfer().id(), - ..Account::default() - }, - ); - - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); - state.force_insert_account( - sender_id, - Account { - program_owner: test_programs::time_locked_transfer().id(), - balance: 100, - ..Account::default() - }, - ); - - let amount = 100; - // Far-future deadline: the program panics because the clock has not reached it. - let deadline = u64::MAX; - - let tx = time_locked_transfer_transaction( - sender_id, - &key1, - 0, - recipient_id, - system_accounts::clock_account_ids()[0], - amount, - deadline, - ); - - let result = state.transition_from_public_transaction(&tx, 2, clock_timestamp); - - assert!( - result.is_err(), - "Transfer should fail when deadline is in the future" - ); - // Balances unchanged. - assert_eq!(state.get_account_by_id(sender_id).balance, 100); - assert_eq!(state.get_account_by_id(recipient_id).balance, 0); - } - - fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec { - let mut buf = Vec::with_capacity(32); - buf.extend_from_slice(&prize.to_le_bytes()); - buf.extend_from_slice(&cooldown_ms.to_le_bytes()); - buf.extend_from_slice(&last_claim_timestamp.to_le_bytes()); - buf - } - - fn pinata_cooldown_transaction( - pinata_id: AccountId, - winner_id: AccountId, - clock_account_id: AccountId, - ) -> PublicTransaction { - let program_id = test_programs::pinata_cooldown().id(); - let message = lee::public_transaction::Message::try_new( - program_id, - vec![pinata_id, winner_id, clock_account_id], - vec![], - (), - ) - .unwrap(); - let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); - PublicTransaction::new(message, witness_set) - } - - #[test] - fn pinata_cooldown_claim_succeeds_after_cooldown() { - let winner_id = AccountId::new([11; 32]); - let pinata_id = AccountId::new([99; 32]); - - let genesis_timestamp = 1000; - let prize = 50; - let cooldown_ms = 500; - // Last claim was at genesis, so any timestamp >= genesis + cooldown should work. - let last_claim_timestamp = genesis_timestamp; - - // Advance the clock so the cooldown check reads an updated timestamp. - let block_timestamp = genesis_timestamp + cooldown_ms; - let mut state = - state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); - - // The winner must be a non-default account so the program may credit it without claiming. - state.force_insert_account( - winner_id, - Account { - program_owner: programs::authenticated_transfer().id(), - ..Account::default() - }, - ); - state.force_insert_account( - pinata_id, - Account { - program_owner: test_programs::pinata_cooldown().id(), - balance: 1000, - data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) - .try_into() - .unwrap(), - ..Account::default() - }, - ); - - let tx = pinata_cooldown_transaction( - pinata_id, - winner_id, - system_accounts::clock_account_ids()[0], - ); - - state - .transition_from_public_transaction(&tx, 2, block_timestamp) - .unwrap(); - - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); - assert_eq!(state.get_account_by_id(winner_id).balance, prize); - } - - #[test] - fn pinata_cooldown_claim_fails_during_cooldown() { - let winner_id = AccountId::new([11; 32]); - let pinata_id = AccountId::new([99; 32]); - - let genesis_timestamp = 1000; - let prize = 50; - let cooldown_ms = 500; - let last_claim_timestamp = genesis_timestamp; - - // Timestamp is only 100ms after the last claim, well within the 500ms cooldown. - let block_timestamp = genesis_timestamp + 100; - let mut state = - state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); - - state.force_insert_account( - winner_id, - Account { - program_owner: programs::authenticated_transfer().id(), - ..Account::default() - }, - ); - state.force_insert_account( - pinata_id, - Account { - program_owner: test_programs::pinata_cooldown().id(), - balance: 1000, - data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) - .try_into() - .unwrap(), - ..Account::default() - }, - ); - - let tx = pinata_cooldown_transaction( - pinata_id, - winner_id, - system_accounts::clock_account_ids()[0], - ); - - let result = state.transition_from_public_transaction(&tx, 2, block_timestamp); - - assert!(result.is_err(), "Claim should fail during cooldown period"); - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); - assert_eq!(state.get_account_by_id(winner_id).balance, 0); - } - - #[test] - fn pda_mechanism_with_pinata_token_program() { - let pinata_token = programs::pinata_token(); - let token = programs::token(); - - let pinata_definition_id = AccountId::new([1; 32]); - let pinata_token_definition_id = AccountId::new([2; 32]); - // Total supply of pinata token will be in an account under a PDA. - let pinata_token_holding_id = - AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32])); - let winner_token_holding_id = AccountId::new([3; 32]); - - let expected_winner_account_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: 150, - }; - let expected_winner_token_holding_post = Account { - program_owner: token.id(), - data: Data::from(&expected_winner_account_holding), - ..Account::default() - }; - - // Register the pinata-token and token programs and create the pinata definition account. - // This replaces the removed `add_pinata_token_program` helper. - let mut state = V03State::new().with_programs([pinata_token.clone(), token.clone()]); - state.force_insert_account( - pinata_definition_id, - Account { - program_owner: pinata_token.id(), - // Difficulty: 3 - data: vec![3; 33].try_into().unwrap(), - ..Account::default() - }, - ); - - // Set up the token accounts directly (bypassing public transactions which - // would require signers for Claim::Authorized). The focus of this test is - // the PDA mechanism in the pinata program's chained call, not token creation. - let total_supply: u128 = 10_000_000; - let token_definition = token_core::TokenDefinition::Fungible { - name: String::from("PINATA"), - total_supply, - metadata_id: None, - }; - let token_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: total_supply, - }; - let winner_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: 0, - }; - state.force_insert_account( - pinata_token_definition_id, - Account { - program_owner: token.id(), - data: Data::from(&token_definition), - ..Account::default() - }, - ); - state.force_insert_account( - pinata_token_holding_id, - Account { - program_owner: token.id(), - data: Data::from(&token_holding), - ..Account::default() - }, - ); - state.force_insert_account( - winner_token_holding_id, - Account { - program_owner: token.id(), - data: Data::from(&winner_holding), - ..Account::default() - }, - ); - - // Submit a solution to the pinata program to claim the prize - let solution: u128 = 989_106; - let message = lee::public_transaction::Message::try_new( - pinata_token.id(), - vec![ - pinata_definition_id, - pinata_token_holding_id, - winner_token_holding_id, - ], - vec![], - solution, - ) - .unwrap(); - let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); - assert_eq!( - winner_token_holding_post, - expected_winner_token_holding_post - ); - } -} +mod tests; diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs new file mode 100644 index 00000000..a5c535a0 --- /dev/null +++ b/lez/sequencer/core/src/tests.rs @@ -0,0 +1,1233 @@ +#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] + +use std::{pin::pin, time::Duration}; + +use common::{ + HashType, + block::HashableBlockData, + test_utils::sequencer_sign_key_for_testing, + transaction::{LeeTransaction, clock_invocation}, +}; +use key_protocol::key_management::KeyChain; +use lee::{ + Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction, PrivateKey, + PublicKey, PublicTransaction, SharedSecretKey, V03State, + error::LeeError, + execute_and_prove, + privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, + program::Program, +}; +use lee_core::{ + Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, + account::{AccountWithMetadata, Nonce}, + program::PdaSeed, +}; +use logos_blockchain_core::mantle::ops::channel::ChannelId; +use mempool::MemPoolHandle; +use storage::sequencer::sequencer_cells::PendingDepositEventRecord; +use tempfile::tempdir; +use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts}; + +use crate::{ + TransactionOrigin, + block_store::SequencerStore, + build_genesis_state, + config::{BedrockConfig, SequencerConfig}, + mock::SequencerCoreWithMockClients, +}; + +#[derive(borsh::BorshSerialize)] +struct DepositMetadataForEncoding { + recipient_id: lee::AccountId, +} + +fn setup_sequencer_config() -> SequencerConfig { + let tempdir = tempfile::tempdir().unwrap(); + let home = tempdir.path().to_path_buf(); + + SequencerConfig { + home, + max_num_tx_in_block: 10, + max_block_size: bytesize::ByteSize::mib(1), + mempool_max_size: 10000, + block_create_timeout: Duration::from_secs(1), + signing_key: *sequencer_sign_key_for_testing().value(), + bedrock_config: BedrockConfig { + channel_id: ChannelId::from([0; 32]), + node_url: "http://not-used-in-unit-tests".parse().unwrap(), + auth: None, + }, + retry_pending_blocks_timeout: Duration::from_mins(4), + genesis: vec![], + } +} + +fn create_signing_key_for_account1() -> lee::PrivateKey { + initial_pub_accounts_private_keys()[0].pub_sign_key.clone() +} + +fn create_signing_key_for_account2() -> lee::PrivateKey { + initial_pub_accounts_private_keys()[1].pub_sign_key.clone() +} + +async fn common_setup() -> ( + SequencerCoreWithMockClients, + MemPoolHandle<(TransactionOrigin, LeeTransaction)>, +) { + let config = setup_sequencer_config(); + common_setup_with_config(config).await +} + +async fn common_setup_with_config( + config: SequencerConfig, +) -> ( + SequencerCoreWithMockClients, + MemPoolHandle<(TransactionOrigin, LeeTransaction)>, +) { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + sequencer.produce_new_block().await.unwrap(); + + (sequencer, mempool_handle) +} + +fn tx_is_bridge_deposit( + tx: &LeeTransaction, + deposit_op_id: [u8; 32], + expected_amount: u64, +) -> bool { + let LeeTransaction::Public(public_tx) = tx else { + return false; + }; + + if public_tx.message.program_id != programs::bridge().id() { + return false; + } + + let instruction: bridge_core::Instruction = + match risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) { + Ok(instruction) => instruction, + Err(_err) => return false, + }; + + matches!( + instruction, + bridge_core::Instruction::Deposit { + l1_deposit_op_id, + amount, + .. + } if l1_deposit_op_id == deposit_op_id && amount == expected_amount + ) +} + +#[tokio::test] +async fn start_from_config() { + let config = setup_sequencer_config(); + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + assert_eq!(sequencer.chain_height, 1); + assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); + + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + + let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; + let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; + + assert_eq!(10000, balance_acc_1); + assert_eq!(20000, balance_acc_2); +} + +#[tokio::test] +async fn start_from_config_opens_existing_db_if_it_exists() { + let config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + let mut config = config; + config.home = temp_dir.path().to_path_buf(); + + let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); + let (genesis_state, genesis_txs) = build_genesis_state(&config); + let genesis_hashable_data = HashableBlockData { + block_id: 1, + transactions: genesis_txs, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + }; + let genesis_block = genesis_hashable_data.into_pending_block(&signing_key); + + SequencerStore::create_db_with_genesis( + &config.home.join("rocksdb"), + &genesis_block, + &genesis_state, + signing_key, + ) + .unwrap(); + + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + assert_eq!(sequencer.chain_height, 1); + assert!(sequencer.store.latest_block_meta().is_ok()); +} + +#[should_panic(expected = "Failed to open database")] +#[tokio::test] +async fn start_from_config_panics_when_db_open_returns_non_not_found_error() { + let mut config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + config.home = temp_dir.path().to_path_buf(); + + let db_path = config.home.join("rocksdb"); + + std::fs::create_dir_all(&config.home).unwrap(); + // Force RocksDB open to fail with an IO error by placing a file at DB path. + std::fs::write(&db_path, b"not-a-directory").unwrap(); + + let _ = SequencerCoreWithMockClients::start_from_config(config).await; +} + +#[tokio::test] +async fn start_from_config_replays_unfulfilled_deposit_events_from_db() { + let config = setup_sequencer_config(); + let deposit_op_id = [13_u8; 32]; + let expected_amount = 1_u64; + let recipient_id = initial_public_user_accounts()[0].account_id; + + { + let (_sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + } + + let pending_event = PendingDepositEventRecord { + deposit_op_id: HashType(deposit_op_id), + source_tx_hash: HashType([7_u8; 32]), + amount: expected_amount, + metadata: borsh::to_vec(&DepositMetadataForEncoding { recipient_id }).unwrap(), + submitted_in_block_id: None, + }; + + { + let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); + let store = SequencerStore::open_db(&config.home.join("rocksdb"), signing_key).unwrap(); + + let inserted = store + .dbio() + .add_pending_deposit_event(pending_event) + .unwrap(); + assert!(inserted); + } + + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + let (origin, tx) = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some((origin, tx)) = sequencer.mempool.pop() { + return (origin, tx); + } + + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timed out waiting for pending deposit event to be replayed into mempool"); + + match origin { + TransactionOrigin::Sequencer => {} + TransactionOrigin::User => { + panic!("Unexpected user transaction in empty mempool replay test") + } + } + + assert!(tx_is_bridge_deposit(&tx, deposit_op_id, expected_amount)); + + let pending_events = sequencer.store.get_unfulfilled_deposit_events().unwrap(); + let replayed_event = pending_events + .into_iter() + .find(|event| event.deposit_op_id == HashType(deposit_op_id)) + .expect("Pending deposit event should remain in DB until included in a block"); + assert!(replayed_event.submitted_in_block_id.is_none()); +} + +#[test] +fn transaction_pre_check_pass() { + let tx = common::test_utils::produce_dummy_empty_transaction(); + let result = tx.transaction_stateless_check(); + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_valid() { + let (_sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = + common::test_utils::create_transaction_native_token_transfer(acc1, 0, acc2, 10, &sign_key1); + let result = tx.transaction_stateless_check(); + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_other_signature() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key2 = create_signing_key_for_account2(); + + let tx = + common::test_utils::create_transaction_native_token_transfer(acc1, 0, acc2, 10, &sign_key2); + + // Signature is valid, stateless check pass + let tx = tx.transaction_stateless_check().unwrap(); + + // Signature is not from sender. Execution fails + let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0); + + assert!(matches!( + result, + Err(lee::error::LeeError::ProgramExecutionFailed(_)) + )); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_sent_too_much() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 10_000_000, &sign_key1, + ); + + let result = tx.transaction_stateless_check(); + + // Passed pre-check + assert!(result.is_ok()); + + let result = result + .unwrap() + .execute_check_on_state(&mut sequencer.state, 0, 0); + let is_failed_at_balance_mismatch = matches!( + result.err().unwrap(), + lee::error::LeeError::ProgramExecutionFailed(_) + ); + + assert!(is_failed_at_balance_mismatch); +} + +#[tokio::test] +async fn transaction_execute_native_transfer() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + tx.execute_check_on_state(&mut sequencer.state, 0, 0) + .unwrap(); + + let bal_from = sequencer.state.get_account_by_id(acc1).balance; + let bal_to = sequencer.state.get_account_by_id(acc2).balance; + + assert_eq!(bal_from, 9900); + assert_eq!(bal_to, 20100); +} + +#[tokio::test] +async fn push_tx_into_mempool_blocks_until_mempool_is_full() { + let config = SequencerConfig { + mempool_max_size: 1, + ..setup_sequencer_config() + }; + let (mut sequencer, mempool_handle) = common_setup_with_config(config).await; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + + // Fill the mempool + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + + // Check that pushing another transaction will block + let mut push_fut = pin!(mempool_handle.push((TransactionOrigin::User, tx.clone()))); + let poll = futures::poll!(push_fut.as_mut()); + assert!(poll.is_pending()); + + // Empty the mempool by producing a block + sequencer.produce_new_block().await.unwrap(); + + // Resolve the pending push + assert!(push_fut.await.is_ok()); +} + +#[tokio::test] +async fn build_block_from_mempool() { + let (mut sequencer, mempool_handle) = common_setup().await; + let genesis_height = sequencer.chain_height; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + let result = sequencer.build_block_from_mempool(); + assert!(result.is_ok()); + assert_eq!(sequencer.chain_height, genesis_height + 1); +} + +#[tokio::test] +async fn replay_transactions_are_rejected_in_the_same_block() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + let tx_original = tx.clone(); + let tx_replay = tx.clone(); + // Pushing two copies of the same tx to the mempool + mempool_handle + .push((TransactionOrigin::User, tx_original)) + .await + .unwrap(); + mempool_handle + .push((TransactionOrigin::User, tx_replay)) + .await + .unwrap(); + + // Create block + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // Only one user tx should be included; the clock tx is always appended last. + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); +} + +#[tokio::test] +async fn replay_transactions_are_rejected_in_different_blocks() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + // The transaction should be included the first time + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); + + // Add same transaction should fail + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + // The replay is rejected, so only the clock tx is in the block. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn restart_from_storage() { + let config = setup_sequencer_config(); + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + let balance_to_move = 13; + + // In the following code block a transaction will be processed that moves `balance_to_move` + // from `acc_1` to `acc_2`. The block created with that transaction will be kept stored in + // the temporary directory for the block storage of this test. + { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + let signing_key = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 0, + acc2_account_id, + balance_to_move, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); + } + + // Instantiating a new sequencer from the same config. This should load the existing block + // with the above transaction and update the state to reflect that. + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; + let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; + + // Balances should be consistent with the stored block + assert_eq!( + balance_acc_1, + initial_public_user_accounts()[0].balance - balance_to_move + ); + assert_eq!( + balance_acc_2, + initial_public_user_accounts()[1].balance + balance_to_move + ); +} + +#[tokio::test] +async fn get_pending_blocks() { + let config = setup_sequencer_config(); + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4); +} + +#[tokio::test] +async fn delete_blocks() { + let config = setup_sequencer_config(); + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let last_finalized_block = 3; + sequencer + .clean_finalized_blocks_from_db(last_finalized_block) + .unwrap(); + + assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1); +} + +#[tokio::test] +async fn produce_block_with_correct_prev_meta_after_restart() { + let config = setup_sequencer_config(); + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + + // Step 1: Create initial database with some block metadata + let expected_prev_meta = { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + let signing_key = create_signing_key_for_account1(); + + // Add a transaction and produce a block to set up block metadata + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 0, + acc2_account_id, + 100, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + // Get the metadata of the last block produced + sequencer.store.latest_block_meta().unwrap() + }; + + // Step 2: Restart sequencer from the same storage + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + // Step 3: Submit a new transaction + let signing_key = create_signing_key_for_account1(); + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 1, // Next nonce + acc2_account_id, + 50, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + + // Step 4: Produce new block + sequencer.produce_new_block().await.unwrap(); + + // Step 5: Verify the new block has correct previous block metadata + let new_block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + assert_eq!( + new_block.header.prev_block_hash, expected_prev_meta.hash, + "New block's prev_block_hash should match the stored metadata hash" + ); + assert_eq!( + new_block.body.transactions, + vec![ + tx, + LeeTransaction::Public(clock_invocation(new_block.header.timestamp)) + ], + "New block should contain the submitted transaction and the clock invocation" + ); +} + +#[tokio::test] +async fn transactions_touching_clock_account_are_dropped_from_block() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Canonical clock invocation and a crafted variant with a different timestamp — both must + // be dropped because their diffs touch the clock accounts. + let crafted_clock_tx = { + let message = lee::public_transaction::Message::try_new( + programs::clock().id(), + system_accounts::clock_account_ids().to_vec(), + vec![], + 42_u64, + ) + .unwrap(); + LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )) + }; + mempool_handle + .push(( + TransactionOrigin::User, + LeeTransaction::Public(clock_invocation(0)), + )) + .await + .unwrap(); + mempool_handle + .push((TransactionOrigin::User, crafted_clock_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // Both transactions were dropped. Only the system-appended clock tx remains. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn user_tx_that_chain_calls_clock_is_dropped() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let clock_chain_caller = test_programs::clock_chain_caller(); + // Deploy the clock_chain_caller test program. + let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( + lee::program_deployment_transaction::Message::new(clock_chain_caller.elf().to_owned()), + )); + mempool_handle + .push((TransactionOrigin::User, deploy_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the + // clock program with the clock accounts. The sequencer should detect that the resulting + // state diff modifies clock accounts and drop the transaction. + let clock_chain_caller_id = test_programs::clock_chain_caller().id(); + let clock_program_id = programs::clock().id(); + let timestamp: u64 = 0; + + let message = lee::public_transaction::Message::try_new( + clock_chain_caller_id, + system_accounts::clock_account_ids().to_vec(), + vec![], // no signers + (clock_program_id, timestamp), + ) + .unwrap(); + let user_tx = LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )); + + mempool_handle + .push((TransactionOrigin::User, user_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // The user tx must have been dropped; only the mandatory clock invocation remains. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn block_production_aborts_when_clock_account_data_is_corrupted() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Corrupt the clock 01 account data so the clock program panics on deserialization. + let clock_account_id = system_accounts::clock_account_ids()[0]; + let mut corrupted = sequencer.state.get_account_by_id(clock_account_id); + corrupted.data = vec![0xff; 3].try_into().unwrap(); + sequencer + .state + .force_insert_account(clock_account_id, corrupted); + + // Push a dummy transaction so the mempool is non-empty. + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + // Block production must fail because the appended clock tx cannot execute. + let result = sequencer.produce_new_block().await; + assert!( + result.is_err(), + "Block production should abort when clock account data is corrupted" + ); +} + +#[test] +fn private_bridge_withdraw_invocation_is_dropped() { + let sender_keys = KeyChain::new_os_random(); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.nullifier_public_key, 0); + let sender_private_account = Account { + program_owner: programs::authenticated_transfer().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + data: Data::default(), + }; + let bridge_account_id = system_accounts::bridge_account_id(); + + let mut state = V03State::new() + .with_public_accounts([(bridge_account_id, system_accounts::bridge_account())]) + .with_private_accounts([( + Commitment::new(&sender_account_id, &sender_private_account), + Nullifier::for_account_initialization(&sender_account_id), + )]); + + let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); + + let sender_pre = AccountWithMetadata::new( + sender_private_account, + true, + (&sender_keys.nullifier_public_key, 0), + ); + let bridge_pre = AccountWithMetadata::new( + state.get_account_by_id(bridge_account_id), + false, + bridge_account_id, + ); + + let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0; + + let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { + amount: 1, + bedrock_account_pk: [0; 32], + }) + .unwrap(); + + let program_with_deps = ProgramWithDependencies::new( + programs::bridge(), + [( + programs::authenticated_transfer().id(), + programs::authenticated_transfer(), + )] + .into(), + ); + + let (output, proof) = execute_and_prove( + vec![sender_pre, bridge_pre], + instruction, + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(vec![12_u8; 1088]), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.nullifier_public_key, + &sender_keys.viewing_public_key, + ), + ssk: shared_secret, + nsk: sender_keys.private_key_holder.nullifier_secret_key, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .expect("Execution should succeed"); + + let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) + .expect("Message construction should succeed"); + let witness_set = + lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); + let tx = + LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new(message, witness_set)); + let res = tx.execute_check_on_state(&mut state, 1, 0); + + assert!( + matches!(res, Err(LeeError::InvalidInput(_))), + "Bridge withdraw invocation should be rejected in private execution" + ); +} + +/// Builds a [`V03State`] with the clock program and `program` registered, the three clock +/// accounts initialized, and the clock advanced to `clock_timestamp` so that reads of the +/// `CLOCK_01` account observe it. +fn state_with_clock_and_program(program: Program, clock_timestamp: u64) -> V03State { + let mut state = V03State::new().with_programs([programs::clock(), program]); + for clock_id in system_accounts::clock_account_ids() { + state.force_insert_account(clock_id, system_accounts::clock_account()); + } + state + .transition_from_public_transaction(&clock_invocation(clock_timestamp), 1, clock_timestamp) + .expect("Clock invocation should advance the clock"); + state +} + +fn time_locked_transfer_transaction( + from: AccountId, + from_key: &PrivateKey, + from_nonce: u128, + to: AccountId, + clock_account_id: AccountId, + amount: u128, + deadline: u64, +) -> PublicTransaction { + let program_id = test_programs::time_locked_transfer().id(); + let message = lee::public_transaction::Message::try_new( + program_id, + vec![from, to, clock_account_id], + vec![Nonce(from_nonce)], + (amount, deadline), + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[from_key]); + PublicTransaction::new(message, witness_set) +} + +#[test] +fn time_locked_transfer_succeeds_when_deadline_has_passed() { + let clock_timestamp = 600; + let mut state = + state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); + + // The recipient must be a non-default account so the program may credit it without + // claiming it. + let recipient_id = AccountId::new([42; 32]); + state.force_insert_account( + recipient_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: test_programs::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100; + // Deadline is in the past relative to the clock, so the transfer is unlocked. + let deadline = 0; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + system_accounts::clock_account_ids()[0], + amount, + deadline, + ); + + state + .transition_from_public_transaction(&tx, 2, clock_timestamp) + .unwrap(); + + // Balances changed. + assert_eq!(state.get_account_by_id(sender_id).balance, 0); + assert_eq!(state.get_account_by_id(recipient_id).balance, 100); +} + +#[test] +fn time_locked_transfer_fails_when_deadline_is_in_the_future() { + let clock_timestamp = 600; + let mut state = + state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); + + let recipient_id = AccountId::new([42; 32]); + state.force_insert_account( + recipient_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: test_programs::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100; + // Far-future deadline: the program panics because the clock has not reached it. + let deadline = u64::MAX; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + system_accounts::clock_account_ids()[0], + amount, + deadline, + ); + + let result = state.transition_from_public_transaction(&tx, 2, clock_timestamp); + + assert!( + result.is_err(), + "Transfer should fail when deadline is in the future" + ); + // Balances unchanged. + assert_eq!(state.get_account_by_id(sender_id).balance, 100); + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); +} + +fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec { + let mut buf = Vec::with_capacity(32); + buf.extend_from_slice(&prize.to_le_bytes()); + buf.extend_from_slice(&cooldown_ms.to_le_bytes()); + buf.extend_from_slice(&last_claim_timestamp.to_le_bytes()); + buf +} + +fn pinata_cooldown_transaction( + pinata_id: AccountId, + winner_id: AccountId, + clock_account_id: AccountId, +) -> PublicTransaction { + let program_id = test_programs::pinata_cooldown().id(); + let message = lee::public_transaction::Message::try_new( + program_id, + vec![pinata_id, winner_id, clock_account_id], + vec![], + (), + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) +} + +#[test] +fn pinata_cooldown_claim_succeeds_after_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000; + let prize = 50; + let cooldown_ms = 500; + // Last claim was at genesis, so any timestamp >= genesis + cooldown should work. + let last_claim_timestamp = genesis_timestamp; + + // Advance the clock so the cooldown check reads an updated timestamp. + let block_timestamp = genesis_timestamp + cooldown_ms; + let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + + // The winner must be a non-default account so the program may credit it without claiming. + state.force_insert_account( + winner_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_id, + Account { + program_owner: test_programs::pinata_cooldown().id(), + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction( + pinata_id, + winner_id, + system_accounts::clock_account_ids()[0], + ); + + state + .transition_from_public_transaction(&tx, 2, block_timestamp) + .unwrap(); + + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); + assert_eq!(state.get_account_by_id(winner_id).balance, prize); +} + +#[test] +fn pinata_cooldown_claim_fails_during_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000; + let prize = 50; + let cooldown_ms = 500; + let last_claim_timestamp = genesis_timestamp; + + // Timestamp is only 100ms after the last claim, well within the 500ms cooldown. + let block_timestamp = genesis_timestamp + 100; + let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + + state.force_insert_account( + winner_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_id, + Account { + program_owner: test_programs::pinata_cooldown().id(), + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction( + pinata_id, + winner_id, + system_accounts::clock_account_ids()[0], + ); + + let result = state.transition_from_public_transaction(&tx, 2, block_timestamp); + + assert!(result.is_err(), "Claim should fail during cooldown period"); + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); + assert_eq!(state.get_account_by_id(winner_id).balance, 0); +} + +#[test] +fn pda_mechanism_with_pinata_token_program() { + let pinata_token = programs::pinata_token(); + let token = programs::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = + AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32])); + let winner_token_holding_id = AccountId::new([3; 32]); + + let expected_winner_account_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 150, + }; + let expected_winner_token_holding_post = Account { + program_owner: token.id(), + data: Data::from(&expected_winner_account_holding), + ..Account::default() + }; + + // Register the pinata-token and token programs and create the pinata definition account. + // This replaces the removed `add_pinata_token_program` helper. + let mut state = V03State::new().with_programs([pinata_token.clone(), token.clone()]); + state.force_insert_account( + pinata_definition_id, + Account { + program_owner: pinata_token.id(), + // Difficulty: 3 + data: vec![3; 33].try_into().unwrap(), + ..Account::default() + }, + ); + + // Set up the token accounts directly (bypassing public transactions which + // would require signers for Claim::Authorized). The focus of this test is + // the PDA mechanism in the pinata program's chained call, not token creation. + let total_supply: u128 = 10_000_000; + let token_definition = token_core::TokenDefinition::Fungible { + name: String::from("PINATA"), + total_supply, + metadata_id: None, + }; + let token_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: total_supply, + }; + let winner_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 0, + }; + state.force_insert_account( + pinata_token_definition_id, + Account { + program_owner: token.id(), + data: Data::from(&token_definition), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&token_holding), + ..Account::default() + }, + ); + state.force_insert_account( + winner_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&winner_holding), + ..Account::default() + }, + ); + + // Submit a solution to the pinata program to claim the prize + let solution: u128 = 989_106; + let message = lee::public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); + assert_eq!( + winner_token_holding_post, + expected_winner_token_holding_post + ); +} diff --git a/lez/storage/src/indexer/mod.rs b/lez/storage/src/indexer/mod.rs index a753a71a..87d586fb 100644 --- a/lez/storage/src/indexer/mod.rs +++ b/lez/storage/src/indexer/mod.rs @@ -153,97 +153,19 @@ impl RocksDBIO { } let br_id = closest_breakpoint_id(block_id); - let mut breakpoint = self.get_breakpoint(br_id)?; + let mut state = self.get_breakpoint(br_id)?; let start = u64::from(BREAKPOINT_INTERVAL) .checked_mul(br_id) .expect("Reached maximum breakpoint id"); - for mut block in self.get_block_batch_seq( + for block in self.get_block_batch_seq( start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, )? { - let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); - - let clock_tx = block.body.transactions.pop().ok_or_else(|| { - DbError::db_interaction_error( - "Block must contain clock transaction at the end".to_owned(), - ) - })?; - let user_txs = block.body.transactions; - - if clock_tx != expected_clock { - return Err(DbError::db_interaction_error( - "Last transaction in block must be the clock invocation for the block timestamp" - .to_owned(), - )); - } - for transaction in user_txs { - let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; - if is_genesis { - let genesis_tx = match transaction { - LeeTransaction::Public(public_tx) => public_tx, - LeeTransaction::PrivacyPreserving(_) - | LeeTransaction::ProgramDeployment(_) => { - return Err(DbError::db_interaction_error( - "Genesis block should contain only public transactions".to_owned(), - )); - } - }; - breakpoint - .transition_from_public_transaction( - &genesis_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "genesis transaction execution failed with err {err:?}" - )) - })?; - } else { - transaction - .transaction_stateless_check() - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction pre check failed with err {err:?}" - )) - })? - // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to - // sequencer-generated deposit tx'es; - // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions - .execute_without_system_accounts_check_on_state( - &mut breakpoint, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction execution failed with err {err:?}" - )) - })?; - } - } - - let LeeTransaction::Public(clock_public_tx) = clock_tx else { - return Err(DbError::db_interaction_error( - "Clock invocation must be a public transaction".to_owned(), - )); - }; - - breakpoint - .transition_from_public_transaction( - &clock_public_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "clock transaction execution failed with err {err:?}" - )) - })?; + apply_block_transactions(block, &mut state)?; } - Ok(breakpoint) + Ok(state) } pub fn final_state(&self) -> DbResult { @@ -252,6 +174,86 @@ impl RocksDBIO { } } +fn apply_block_transactions(mut block: Block, state: &mut V03State) -> DbResult<()> { + let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); + + let clock_tx = block.body.transactions.pop().ok_or_else(|| { + DbError::db_interaction_error("Block must contain clock transaction at the end".to_owned()) + })?; + + if clock_tx != expected_clock { + return Err(DbError::db_interaction_error( + "Last transaction in block must be the clock invocation for the block timestamp" + .to_owned(), + )); + } + + for transaction in block.body.transactions { + if block.header.block_id == GENESIS_BLOCK_ID { + let genesis_tx = match transaction { + LeeTransaction::Public(public_tx) => public_tx, + LeeTransaction::PrivacyPreserving(_) | LeeTransaction::ProgramDeployment(_) => { + return Err(DbError::db_interaction_error( + "Genesis block should contain only public transactions".to_owned(), + )); + } + }; + state + .transition_from_public_transaction( + &genesis_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "genesis transaction execution failed with err {err:?}" + )) + })?; + } else { + transaction + .transaction_stateless_check() + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction pre check failed with err {err:?}" + )) + })? + // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to + // sequencer-generated deposit tx'es; + // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions + .execute_without_system_accounts_check_on_state( + state, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction execution failed with err {err:?}" + )) + })?; + } + } + + let LeeTransaction::Public(clock_public_tx) = clock_tx else { + return Err(DbError::db_interaction_error( + "Clock invocation must be a public transaction".to_owned(), + )); + }; + + state + .transition_from_public_transaction( + &clock_public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "clock transaction execution failed with err {err:?}" + )) + })?; + + Ok(()) +} + fn closest_breakpoint_id(block_id: u64) -> u64 { block_id .saturating_sub(1) @@ -261,439 +263,4 @@ fn closest_breakpoint_id(block_id: u64) -> u64 { #[expect(clippy::shadow_unrelated, reason = "Fine for tests")] #[cfg(test)] -mod tests { - use common::test_utils::produce_dummy_block; - use lee::{Account, AccountId, PublicKey}; - use tempfile::tempdir; - - use super::*; - - fn genesis_block() -> Block { - produce_dummy_block(1, None, vec![]) - } - - fn acc1_sign_key() -> lee::PrivateKey { - lee::PrivateKey::try_new([1; 32]).unwrap() - } - - fn acc2_sign_key() -> lee::PrivateKey { - lee::PrivateKey::try_new([2; 32]).unwrap() - } - - fn acc1() -> AccountId { - AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key())) - } - - fn acc2() -> AccountId { - AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) - } - - fn initial_state() -> lee::V03State { - let mut public_accounts = [(acc1(), 10000), (acc2(), 20000)] - .into_iter() - .map(|(id, balance)| { - ( - id, - Account { - program_owner: programs::authenticated_transfer().id(), - balance, - ..Account::default() - }, - ) - }) - .collect::>(); - for clock_id in system_accounts::clock_account_ids() { - public_accounts.push((clock_id, system_accounts::clock_account())); - } - - lee::V03State::new() - .with_public_accounts(public_accounts) - .with_programs([programs::authenticated_transfer(), programs::clock()]) - } - - #[test] - fn start_db() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(1).unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, None); - assert_eq!(first_id, None); - assert_eq!(last_observed_l1_header, None); - assert!(!is_first_set); - assert_eq!(last_br_id, Some(0)); // TODO: Will be None after we remove hardcoded testnet state - assert!(last_block.is_none()); - assert_eq!( - breakpoint.get_account_by_id(acc1()), - final_state.get_account_by_id(acc1()) - ); - assert_eq!( - breakpoint.get_account_by_id(acc2()), - final_state.get_account_by_id(acc2()) - ); - } - - #[test] - fn one_block_insertion() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let genesis_block = genesis_block(); - dbio.put_block(&genesis_block, [0; 32]).unwrap(); - - let prev_hash = genesis_block.header.hash; - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let last_observed_l1_header = dbio - .get_meta_last_observed_l1_lib_header_in_db() - .unwrap() - .unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 2); - assert_eq!(first_id, Some(1)); - assert_eq!(last_observed_l1_header, [1; 32]); - assert!(is_first_set); - assert_eq!(last_br_id, Some(0)); - assert_eq!(last_block.header.hash, block.header.hash); - assert_eq!( - breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 1 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - breakpoint.get_account_by_id(acc2()).balance, - 1 - ); - } - - #[test] - fn new_breakpoint() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - for i in 1..=BREAKPOINT_INTERVAL + 1 { - let prev_hash = dbio.get_meta_last_block_id_in_db().unwrap().map(|last_id| { - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - last_block.header.hash - }); - - let transfer_tx = common::test_utils::create_transaction_native_token_transfer( - from, - (i - 1).into(), - to, - 1, - &sign_key, - ); - let block = produce_dummy_block(i.into(), prev_hash, vec![transfer_tx]); - dbio.put_block(&block, [i; 32]).unwrap(); - } - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); - let breakpoint = dbio.get_breakpoint(1).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 101); - assert_eq!(first_id, Some(1)); - assert!(is_first_set); - assert_eq!(last_br_id, Some(1)); - assert_ne!(last_block.header.hash, genesis_block().header.hash); - assert_eq!( - prev_breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 101 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - prev_breakpoint.get_account_by_id(acc2()).balance, - 101 - ); - assert_eq!( - breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 1 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - breakpoint.get_account_by_id(acc2()).balance, - 1 - ); - } - - #[test] - fn simple_maps() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(1, None, vec![transfer_tx]); - - let control_hash1 = block.header.hash; - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - let control_hash2 = block.header.hash; - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - - let control_tx_hash1 = transfer_tx.hash(); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - - let control_tx_hash2 = transfer_tx.hash(); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [4; 32]).unwrap(); - - let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); - let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); - let control_block_id3 = dbio - .get_block_id_by_tx_hash(control_tx_hash1.0) - .unwrap() - .unwrap(); - let control_block_id4 = dbio - .get_block_id_by_tx_hash(control_tx_hash2.0) - .unwrap() - .unwrap(); - - assert_eq!(control_block_id1, 1); - assert_eq!(control_block_id2, 2); - assert_eq!(control_block_id3, 3); - assert_eq!(control_block_id4, 4); - } - - #[test] - fn block_batch() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let mut block_res = vec![]; - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(1, None, vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [4; 32]).unwrap(); - - let block_hashes_mem: Vec<[u8; 32]> = - block_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - // Get blocks before ID 5 (i.e., starting from 4 going backwards), limit 4 - // This should return blocks 4, 3, 2, 1 in descending order - let mut batch_res = dbio.get_block_batch(Some(5), 4).unwrap(); - batch_res.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db: Vec<[u8; 32]> = - batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - assert_eq!(block_hashes_mem, block_hashes_db); - - let block_hashes_mem_limited = &block_hashes_mem[1..]; - - // Get blocks before ID 5, limit 3 - // This should return blocks 4, 3, 2 in descending order - let mut batch_res_limited = dbio.get_block_batch(Some(5), 3).unwrap(); - batch_res_limited.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited - .into_iter() - .map(|bl| bl.header.hash.0) - .collect(); - - assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); - - let block_batch_seq = dbio.get_block_batch_seq(1..=5).unwrap(); - let block_batch_ids = block_batch_seq - .into_iter() - .map(|block| block.header.block_id) - .collect::>(); - - assert_eq!(block_batch_ids, vec![1, 2, 3, 4]); - } - - #[test] - fn account_map() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let mut tx_hash_res = vec![]; - - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(1, None, vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 4, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 5, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); - tx_hash_res.push(transfer_tx.hash().0); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [4; 32]).unwrap(); - - let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 7).unwrap(); - let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_hashes, tx_hash_res); - - let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); - let acc1_tx_limited_hashes: Vec<[u8; 32]> = - acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..5]); - } -} +mod tests; diff --git a/lez/storage/src/indexer/tests.rs b/lez/storage/src/indexer/tests.rs new file mode 100644 index 00000000..44df23d5 --- /dev/null +++ b/lez/storage/src/indexer/tests.rs @@ -0,0 +1,433 @@ +use common::test_utils::produce_dummy_block; +use lee::{Account, AccountId, PublicKey}; +use tempfile::tempdir; + +use super::*; + +fn genesis_block() -> Block { + produce_dummy_block(1, None, vec![]) +} + +fn acc1_sign_key() -> lee::PrivateKey { + lee::PrivateKey::try_new([1; 32]).unwrap() +} + +fn acc2_sign_key() -> lee::PrivateKey { + lee::PrivateKey::try_new([2; 32]).unwrap() +} + +fn acc1() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key())) +} + +fn acc2() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) +} + +fn initial_state() -> lee::V03State { + let mut public_accounts = [(acc1(), 10000), (acc2(), 20000)] + .into_iter() + .map(|(id, balance)| { + ( + id, + Account { + program_owner: programs::authenticated_transfer().id(), + balance, + ..Account::default() + }, + ) + }) + .collect::>(); + for clock_id in system_accounts::clock_account_ids() { + public_accounts.push((clock_id, system_accounts::clock_account())); + } + + lee::V03State::new() + .with_public_accounts(public_accounts) + .with_programs([programs::authenticated_transfer(), programs::clock()]) +} + +#[test] +fn start_db() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(1).unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, None); + assert_eq!(first_id, None); + assert_eq!(last_observed_l1_header, None); + assert!(!is_first_set); + assert_eq!(last_br_id, Some(0)); // TODO: Will be None after we remove hardcoded testnet state + assert!(last_block.is_none()); + assert_eq!( + breakpoint.get_account_by_id(acc1()), + final_state.get_account_by_id(acc1()) + ); + assert_eq!( + breakpoint.get_account_by_id(acc2()), + final_state.get_account_by_id(acc2()) + ); +} + +#[test] +fn one_block_insertion() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let genesis_block = genesis_block(); + dbio.put_block(&genesis_block, [0; 32]).unwrap(); + + let prev_hash = genesis_block.header.hash; + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let last_observed_l1_header = dbio + .get_meta_last_observed_l1_lib_header_in_db() + .unwrap() + .unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 2); + assert_eq!(first_id, Some(1)); + assert_eq!(last_observed_l1_header, [1; 32]); + assert!(is_first_set); + assert_eq!(last_br_id, Some(0)); + assert_eq!(last_block.header.hash, block.header.hash); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); +} + +#[test] +fn new_breakpoint() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + for i in 1..=BREAKPOINT_INTERVAL + 1 { + let prev_hash = dbio.get_meta_last_block_id_in_db().unwrap().map(|last_id| { + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + last_block.header.hash + }); + + let transfer_tx = common::test_utils::create_transaction_native_token_transfer( + from, + (i - 1).into(), + to, + 1, + &sign_key, + ); + let block = produce_dummy_block(i.into(), prev_hash, vec![transfer_tx]); + dbio.put_block(&block, [i; 32]).unwrap(); + } + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); + let breakpoint = dbio.get_breakpoint(1).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 101); + assert_eq!(first_id, Some(1)); + assert!(is_first_set); + assert_eq!(last_br_id, Some(1)); + assert_ne!(last_block.header.hash, genesis_block().header.hash); + assert_eq!( + prev_breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 101 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - prev_breakpoint.get_account_by_id(acc2()).balance, + 101 + ); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); +} + +#[test] +fn simple_maps() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(1, None, vec![transfer_tx]); + + let control_hash1 = block.header.hash; + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + let control_hash2 = block.header.hash; + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let control_tx_hash1 = transfer_tx.hash(); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let control_tx_hash2 = transfer_tx.hash(); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [4; 32]).unwrap(); + + let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); + let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); + let control_block_id3 = dbio + .get_block_id_by_tx_hash(control_tx_hash1.0) + .unwrap() + .unwrap(); + let control_block_id4 = dbio + .get_block_id_by_tx_hash(control_tx_hash2.0) + .unwrap() + .unwrap(); + + assert_eq!(control_block_id1, 1); + assert_eq!(control_block_id2, 2); + assert_eq!(control_block_id3, 3); + assert_eq!(control_block_id4, 4); +} + +#[test] +fn block_batch() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let mut block_res = vec![]; + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(1, None, vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [4; 32]).unwrap(); + + let block_hashes_mem: Vec<[u8; 32]> = + block_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + // Get blocks before ID 5 (i.e., starting from 4 going backwards), limit 4 + // This should return blocks 4, 3, 2, 1 in descending order + let mut batch_res = dbio.get_block_batch(Some(5), 4).unwrap(); + batch_res.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db: Vec<[u8; 32]> = batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + assert_eq!(block_hashes_mem, block_hashes_db); + + let block_hashes_mem_limited = &block_hashes_mem[1..]; + + // Get blocks before ID 5, limit 3 + // This should return blocks 4, 3, 2 in descending order + let mut batch_res_limited = dbio.get_block_batch(Some(5), 3).unwrap(); + batch_res_limited.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited + .into_iter() + .map(|bl| bl.header.hash.0) + .collect(); + + assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); + + let block_batch_seq = dbio.get_block_batch_seq(1..=5).unwrap(); + let block_batch_ids = block_batch_seq + .into_iter() + .map(|block| block.header.block_id) + .collect::>(); + + assert_eq!(block_batch_ids, vec![1, 2, 3, 4]); +} + +#[test] +fn account_map() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let mut tx_hash_res = vec![]; + + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(1, None, vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 4, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 5, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); + tx_hash_res.push(transfer_tx.hash().0); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [4; 32]).unwrap(); + + let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 7).unwrap(); + let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_hashes, tx_hash_res); + + let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); + let acc1_tx_limited_hashes: Vec<[u8; 32]> = + acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..5]); +} diff --git a/lez/storage/src/indexer/write_atomic.rs b/lez/storage/src/indexer/write_atomic.rs index a88e46ef..ec28f8ec 100644 --- a/lez/storage/src/indexer/write_atomic.rs +++ b/lez/storage/src/indexer/write_atomic.rs @@ -52,44 +52,8 @@ impl RocksDBIO { acc_id: [u8; 32], tx_hashes: &[[u8; 32]], ) -> DbResult<()> { - let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); - let cf_att = self.account_id_to_tx_hash_column(); let mut write_batch = WriteBatch::new(); - - for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { - let put_id = acc_num_tx - .checked_add(tx_id.try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"); - - let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) - })?; - let suffix = borsh::to_vec(&put_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) - })?; - - prefix.extend_from_slice(&suffix); - - write_batch.put_cf( - &cf_att, - prefix, - borsh::to_vec(tx_hash).map_err(|berr| { - DbError::borsh_cast_message( - berr, - Some("Failed to serialize tx hash".to_owned()), - ) - })?, - ); - } - - self.update_acc_meta_batch( - acc_id, - acc_num_tx - .checked_add(tx_hashes.len().try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"), - &mut write_batch, - )?; - + self.put_account_transactions_dependant(acc_id, tx_hashes, &mut write_batch)?; self.db.write(write_batch).map_err(|rerr| { DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) }) diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index ae4e3945..dae8fd31 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -201,38 +201,15 @@ impl AccountManager { for account in accounts { let state = match account { AccountIdentity::Public(account_id) => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let sk = wallet.get_account_public_signing_key(account_id).cloned(); - let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - - State::Public { account, sk } + prepare_public_state(wallet, account_id, true).await? } AccountIdentity::PublicNoSign(account_id) => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let sk = None; - let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - - State::Public { account, sk } + prepare_public_state(wallet, account_id, false).await? } AccountIdentity::PublicKeycard { account_id, key_path, } => { - let acc = wallet - .get_account_public(account_id) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let account = AccountWithMetadata::new(acc.clone(), true, account_id); - if pin.is_none() { pin = Some( crate::helperfunctions::read_pin() @@ -248,66 +225,27 @@ impl AccountManager { .to_owned(), ); } - - State::PublicKeycard { account, key_path } - } - AccountIdentity::PrivateOwned(account_id) => { - let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?; - - State::Private(pre) + prepare_public_keycard_state(wallet, account_id, key_path).await? } + AccountIdentity::PrivateOwned(account_id) => State::Private( + private_key_tree_acc_preparation(wallet, account_id, false).await?, + ), AccountIdentity::PrivateForeign { npk, vpk, identifier, - } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - ssk, - epk, - is_pda: false, - }; - - State::Private(pre) - } - AccountIdentity::PrivatePdaOwned(account_id) => { - let pre = private_key_tree_acc_preparation(wallet, account_id, true).await?; - State::Private(pre) - } + } => State::Private(private_foreign_acc_preparation(npk, vpk, identifier)), + AccountIdentity::PrivatePdaOwned(account_id) => State::Private( + private_key_tree_acc_preparation(wallet, account_id, true).await?, + ), AccountIdentity::PrivatePdaForeign { account_id, npk, vpk, identifier, - } => { - let acc = lee_core::account::Account::default(); - let auth_acc = AccountWithMetadata::new(acc, false, account_id); - let eph_holder = EphemeralKeyHolder::new(&vpk); - let ssk = eph_holder.calculate_shared_secret_sender(); - let epk = eph_holder.ephemeral_public_key().clone(); - let pre = AccountPreparedData { - nsk: None, - npk, - identifier, - vpk, - pre_state: auth_acc, - proof: None, - ssk, - epk, - is_pda: true, - }; - State::Private(pre) - } + } => State::Private(private_pda_foreign_acc_preparation( + account_id, npk, vpk, identifier, + )), AccountIdentity::PrivateShared { nsk, npk, @@ -315,12 +253,12 @@ impl AccountManager { identifier, } => { let account_id = lee::AccountId::from((&npk, identifier)); - let pre = private_shared_acc_preparation( - wallet, account_id, nsk, npk, vpk, identifier, false, + State::Private( + private_shared_acc_preparation( + wallet, account_id, nsk, npk, vpk, identifier, false, + ) + .await?, ) - .await?; - - State::Private(pre) } AccountIdentity::PrivatePdaShared { account_id, @@ -328,14 +266,12 @@ impl AccountManager { npk, vpk, identifier, - } => { - let pre = private_shared_acc_preparation( + } => State::Private( + private_shared_acc_preparation( wallet, account_id, nsk, npk, vpk, identifier, true, ) - .await?; - - State::Private(pre) - } + .await?, + ), }; states.push(state); @@ -538,6 +474,37 @@ struct AccountPreparedData { is_pda: bool, } +async fn prepare_public_state( + wallet: &WalletCore, + account_id: AccountId, + lookup_signing_key: bool, +) -> Result { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + let sk = if lookup_signing_key { + wallet.get_account_public_signing_key(account_id).cloned() + } else { + None + }; + let account = AccountWithMetadata::new(acc, sk.is_some(), account_id); + Ok(State::Public { account, sk }) +} + +async fn prepare_public_keycard_state( + wallet: &WalletCore, + account_id: AccountId, + key_path: String, +) -> Result { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + let account = AccountWithMetadata::new(acc, true, account_id); + Ok(State::PublicKeycard { account, key_path }) +} + async fn private_key_tree_acc_preparation( wallet: &WalletCore, account_id: AccountId, @@ -620,6 +587,55 @@ async fn private_shared_acc_preparation( }) } +fn private_foreign_acc_preparation( + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, +) -> AccountPreparedData { + let acc = lee_core::account::Account::default(); + let pre_state = AccountWithMetadata::new(acc, false, (&npk, identifier)); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + + AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state, + proof: None, + ssk, + epk, + is_pda: false, + } +} + +fn private_pda_foreign_acc_preparation( + account_id: AccountId, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, +) -> AccountPreparedData { + let acc = lee_core::account::Account::default(); + let pre_state = AccountWithMetadata::new(acc, false, account_id); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + + AccountPreparedData { + nsk: None, + npk, + identifier, + vpk, + pre_state, + proof: None, + ssk, + epk, + is_pda: true, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index fb0db2cb..b49cfb25 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -125,75 +125,169 @@ pub enum NewSubcommand { }, } +impl NewSubcommand { + fn handle_public( + cci: Option, + label: Option