diff --git a/crates/astria-sequencer/src/api_state_ext.rs b/crates/astria-sequencer/src/api_state_ext.rs index c42510b9cb..17c6b2545c 100644 --- a/crates/astria-sequencer/src/api_state_ext.rs +++ b/crates/astria-sequencer/src/api_state_ext.rs @@ -34,28 +34,28 @@ use cnidarium::{ use prost::Message; use tracing::instrument; -fn block_hash_by_height_key(height: u64) -> String { - format!("blockhash/{height}") +fn block_hash_by_height_key(height: u64) -> Vec { + [b"blockhash/".as_slice(), &height.to_le_bytes()].concat() } -fn sequencer_block_header_by_hash_key(hash: &[u8]) -> String { - format!("blockheader/{}", crate::utils::Hex(hash)) +fn sequencer_block_header_by_hash_key(hash: &[u8]) -> Vec { + [b"blockheader/", hash].concat() } -fn rollup_data_by_hash_and_rollup_id_key(hash: &[u8], rollup_id: &RollupId) -> String { - format!("rollupdata/{}/{}", crate::utils::Hex(hash), rollup_id) +fn rollup_data_by_hash_and_rollup_id_key(hash: &[u8], rollup_id: &RollupId) -> Vec { + [b"rollupdata/", hash, rollup_id.as_ref()].concat() } -fn rollup_ids_by_hash_key(hash: &[u8]) -> String { - format!("rollupids/{}", crate::utils::Hex(hash)) +fn rollup_ids_by_hash_key(hash: &[u8]) -> Vec { + [b"rollupids/", hash].concat() } -fn rollup_transactions_proof_by_hash_key(hash: &[u8]) -> String { - format!("rolluptxsproof/{}", crate::utils::Hex(hash)) +fn rollup_transactions_proof_by_hash_key(hash: &[u8]) -> Vec { + [b"rolluptxsproof/", hash].concat() } -fn rollup_ids_proof_by_hash_key(hash: &[u8]) -> String { - format!("rollupidsproof/{}", crate::utils::Hex(hash)) +fn rollup_ids_proof_by_hash_key(hash: &[u8]) -> Vec { + [b"rollupidsproof/", hash].concat() } #[derive(BorshSerialize, BorshDeserialize)] @@ -139,7 +139,7 @@ pub(crate) trait StateReadExt: StateRead { async fn get_block_hash_by_height(&self, height: u64) -> Result<[u8; 32]> { let key = block_hash_by_height_key(height); let Some(hash) = self - .get_raw(&key) + .nonverifiable_get_raw(&key) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read block hash by height from state")? @@ -160,7 +160,7 @@ pub(crate) trait StateReadExt: StateRead { ) -> Result { let key = sequencer_block_header_by_hash_key(hash); let Some(header_bytes) = self - .get_raw(&key) + .nonverifiable_get_raw(&key) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read raw sequencer block from state")? @@ -179,7 +179,7 @@ pub(crate) trait StateReadExt: StateRead { async fn get_rollup_ids_by_block_hash(&self, hash: &[u8]) -> Result> { let key = rollup_ids_by_hash_key(hash); let Some(rollup_ids_bytes) = self - .get_raw(&key) + .nonverifiable_get_raw(&key) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup IDs by block hash from state")? @@ -195,7 +195,7 @@ pub(crate) trait StateReadExt: StateRead { #[instrument(skip_all)] async fn get_sequencer_block_by_hash(&self, hash: &[u8]) -> Result { let Some(header_bytes) = self - .get_raw(&sequencer_block_header_by_hash_key(hash)) + .nonverifiable_get_raw(&sequencer_block_header_by_hash_key(hash)) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read raw sequencer block from state")? @@ -214,10 +214,11 @@ pub(crate) trait StateReadExt: StateRead { let mut rollup_transactions = Vec::with_capacity(rollup_ids.len()); for id in &rollup_ids { let key = rollup_data_by_hash_and_rollup_id_key(hash, id); - let raw = - self.get_raw(&key).await.map_err(anyhow_to_eyre).wrap_err( - "failed to read rollup data by block hash and rollup ID from state", - )?; + let raw = self + .nonverifiable_get_raw(&key) + .await + .map_err(anyhow_to_eyre) + .context("failed to read rollup data by block hash and rollup ID from state")?; if let Some(raw) = raw { let raw = raw.as_slice(); let rollup_data = raw::RollupTransactions::decode(raw) @@ -227,7 +228,7 @@ pub(crate) trait StateReadExt: StateRead { } let Some(rollup_transactions_proof) = self - .get_raw(&rollup_transactions_proof_by_hash_key(hash)) + .nonverifiable_get_raw(&rollup_transactions_proof_by_hash_key(hash)) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup transactions proof by block hash from state")? @@ -240,7 +241,7 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("failed to decode rollup transactions proof from raw bytes")?; let Some(rollup_ids_proof) = self - .get_raw(&rollup_ids_proof_by_hash_key(hash)) + .nonverifiable_get_raw(&rollup_ids_proof_by_hash_key(hash)) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup IDs proof by block hash from state")? @@ -284,7 +285,7 @@ pub(crate) trait StateReadExt: StateRead { ) -> Result { let key = rollup_data_by_hash_and_rollup_id_key(hash, rollup_id); let Some(bytes) = self - .get_raw(&key) + .nonverifiable_get_raw(&key) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup data by block hash and rollup ID from state")? @@ -306,7 +307,7 @@ pub(crate) trait StateReadExt: StateRead { hash: &[u8], ) -> Result<(primitiveRaw::Proof, primitiveRaw::Proof)> { let Some(rollup_transactions_proof) = self - .get_raw(&rollup_transactions_proof_by_hash_key(hash)) + .nonverifiable_get_raw(&rollup_transactions_proof_by_hash_key(hash)) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup transactions proof by block hash from state")? @@ -319,7 +320,7 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("failed to decode rollup transactions proof from raw bytes")?; let Some(rollup_ids_proof) = self - .get_raw(&rollup_ids_proof_by_hash_key(hash)) + .nonverifiable_get_raw(&rollup_ids_proof_by_hash_key(hash)) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read rollup IDs proof by block hash from state")? @@ -348,7 +349,7 @@ pub(crate) trait StateWriteExt: StateWrite { // 6. block hash to rollup IDs proof let key = block_hash_by_height_key(block.height().into()); - self.put_raw(key, block.block_hash().to_vec()); + self.nonverifiable_put_raw(key, block.block_hash().to_vec()); let rollup_ids = block .rollup_transactions() @@ -359,7 +360,7 @@ pub(crate) trait StateWriteExt: StateWrite { let key = rollup_ids_by_hash_key(&block.block_hash()); - self.put_raw( + self.nonverifiable_put_raw( key, borsh::to_vec(&RollupIdSeq(rollup_ids)) .wrap_err("failed to serialize rollup IDs list")?, @@ -374,18 +375,18 @@ pub(crate) trait StateWriteExt: StateWrite { rollup_ids_proof, } = block.into_parts(); let header = header.into_raw(); - self.put_raw(key, header.encode_to_vec()); + self.nonverifiable_put_raw(key, header.encode_to_vec()); for (id, rollup_data) in rollup_transactions { let key = rollup_data_by_hash_and_rollup_id_key(&block_hash, &id); - self.put_raw(key, rollup_data.into_raw().encode_to_vec()); + self.nonverifiable_put_raw(key, rollup_data.into_raw().encode_to_vec()); } let key = rollup_transactions_proof_by_hash_key(&block_hash); - self.put_raw(key, rollup_transactions_proof.into_raw().encode_to_vec()); + self.nonverifiable_put_raw(key, rollup_transactions_proof.into_raw().encode_to_vec()); let key = rollup_ids_proof_by_hash_key(&block_hash); - self.put_raw(key, rollup_ids_proof.into_raw().encode_to_vec()); + self.nonverifiable_put_raw(key, rollup_ids_proof.into_raw().encode_to_vec()); Ok(()) } diff --git a/crates/astria-sequencer/src/app/mod.rs b/crates/astria-sequencer/src/app/mod.rs index 66a9e2c5a8..acaa7b2b9b 100644 --- a/crates/astria-sequencer/src/app/mod.rs +++ b/crates/astria-sequencer/src/app/mod.rs @@ -335,11 +335,7 @@ impl App { self.metrics .record_proposal_transactions(signed_txs_included.len()); - let deposits = self - .state - .get_block_deposits() - .await - .wrap_err("failed to get block deposits in prepare_proposal")?; + let deposits = self.state.get_cached_block_deposits(); self.metrics.record_proposal_deposits(deposits.len()); // generate commitment to sequence::Actions and deposits and commitment to the rollup IDs @@ -444,11 +440,7 @@ impl App { ); self.metrics.record_proposal_transactions(signed_txs.len()); - let deposits = self - .state - .get_block_deposits() - .await - .wrap_err("failed to get block deposits in process_proposal")?; + let deposits = self.state.get_cached_block_deposits(); self.metrics.record_proposal_deposits(deposits.len()); let GeneratedCommitments { @@ -871,21 +863,16 @@ impl App { let end_block = self.end_block(height.value(), sudo_address).await?; - // get and clear block deposits from state + // get deposits for this block from state's ephemeral cache and put them to storage. let mut state_tx = StateDelta::new(self.state.clone()); - let deposits = self - .state - .get_block_deposits() - .await - .wrap_err("failed to get block deposits in end_block")?; - state_tx - .clear_block_deposits() - .await - .wrap_err("failed to clear block deposits")?; + let deposits_in_this_block = self.state.get_cached_block_deposits(); debug!( - deposits = %telemetry::display::json(&deposits), + deposits = %telemetry::display::json(&deposits_in_this_block), "got block deposits from state" ); + state_tx + .put_deposits(&block_hash, deposits_in_this_block.clone()) + .wrap_err("failed to put deposits to state")?; let sequencer_block = SequencerBlock::try_from_block_info_and_data( block_hash, @@ -898,7 +885,7 @@ impl App { .into_iter() .map(std::convert::Into::into) .collect(), - deposits, + deposits_in_this_block, ) .wrap_err("failed to convert block info and data to SequencerBlock")?; state_tx diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap index 1d2458f1b5..52c47f2f6a 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap @@ -1,38 +1,39 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs +assertion_line: 308 expression: app.app_hash.as_bytes() --- [ - 237, + 67, + 124, + 63, + 240, 228, - 62, - 229, - 45, + 207, + 78, + 64, + 191, + 89, + 84, + 121, + 150, + 21, + 207, + 248, + 173, + 132, 77, - 247, + 126, + 148, + 252, 239, - 251, - 224, - 244, - 97, - 68, - 46, - 184, - 181, - 205, - 86, - 212, - 153, - 66, - 146, - 179, - 120, - 206, - 95, - 76, - 11, - 0, - 184, - 137, - 173 + 104, + 130, + 55, + 201, + 32, + 57, + 167, + 215, + 228 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap index 5a4d1cb394..33551b3453 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap @@ -1,38 +1,39 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs +assertion_line: 157 expression: app.app_hash.as_bytes() --- [ - 111, - 25, - 76, - 238, - 112, - 77, - 102, - 234, - 8, - 97, - 24, - 100, - 73, - 128, - 228, - 106, - 82, - 255, - 119, - 93, - 248, + 7, 224, - 51, - 239, 115, - 58, - 9, - 149, - 86, - 23, + 113, + 195, + 128, + 219, 248, - 114 + 198, + 108, + 251, + 204, + 202, + 182, + 150, + 203, + 69, + 213, + 169, + 101, + 228, + 90, + 61, + 94, + 59, + 180, + 251, + 59, + 119, + 37, + 42, + 216 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap index 6df05ffada..f5f80e8998 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_genesis_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 230, - 203, - 68, - 175, - 166, - 230, - 219, - 247, - 217, - 191, - 121, - 236, - 155, - 53, - 148, - 22, - 12, - 121, - 60, - 22, - 214, - 93, + 7, + 34, + 84, + 104, + 180, + 74, + 207, + 31, + 198, + 255, + 107, + 249, + 25, + 37, + 103, + 202, + 199, + 132, + 141, + 201, + 64, + 172, 26, - 177, - 216, - 135, - 217, - 212, - 93, - 40, - 173, - 94 + 24, + 80, + 182, + 114, + 182, + 189, + 220, + 109, + 211 ] diff --git a/crates/astria-sequencer/src/app/tests_app/mod.rs b/crates/astria-sequencer/src/app/tests_app/mod.rs index 1c0253308e..9b41f44732 100644 --- a/crates/astria-sequencer/src/app/tests_app/mod.rs +++ b/crates/astria-sequencer/src/app/tests_app/mod.rs @@ -6,6 +6,7 @@ use astria_core::{ primitive::v1::{ asset::TracePrefixed, RollupId, + TransactionId, }, protocol::{ genesis::v1alpha1::Account, @@ -54,10 +55,7 @@ use crate::{ StateWriteExt as _, ValidatorSet, }, - bridge::{ - StateReadExt as _, - StateWriteExt as _, - }, + bridge::StateWriteExt as _, proposal::commitment::generate_rollup_datas_commitment, state_ext::StateReadExt as _, test_utils::{ @@ -304,6 +302,23 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { state_tx .put_bridge_account_ibc_asset(bridge_address, nria()) .unwrap(); + // Put a deposit from a previous block to ensure it is not mixed in with deposits for this + // block (it has a different amount and tx ID to the later deposit). + let old_deposit = Deposit { + bridge_address, + rollup_id, + amount: 99, + asset: nria().into(), + destination_chain_address: "nootwashere".to_string(), + source_transaction_id: TransactionId::new([99; 32]), + source_action_index: starting_index_of_action, + }; + state_tx + .put_deposits( + &[32u8; 32], + HashMap::from_iter([(rollup_id, vec![old_deposit])]), + ) + .unwrap(); app.apply(state_tx); app.prepare_commit(storage.clone()).await.unwrap(); app.commit(storage.clone()).await; @@ -361,10 +376,6 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { .unwrap(); app.commit(storage).await; - // ensure deposits are cleared at the end of the block - let deposit_events = app.state.get_deposit_events(&rollup_id).await.unwrap(); - assert_eq!(deposit_events.len(), 0); - let block = app.state.get_sequencer_block_by_height(1).await.unwrap(); let mut deposits = vec![]; for (_, rollup_data) in block.rollup_transactions() { diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 23075d0019..30a6290ecf 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -773,7 +773,8 @@ async fn app_execute_transaction_bridge_lock_action_ok() { bridge_before_balance + amount ); - let deposits = app.state.get_deposit_events(&rollup_id).await.unwrap(); + let all_deposits = app.state.get_cached_block_deposits(); + let deposits = all_deposits.get(&rollup_id).unwrap(); assert_eq!(deposits.len(), 1); assert_eq!(deposits[0], expected_deposit); } @@ -1116,7 +1117,8 @@ async fn app_execute_transaction_action_index_correctly_increments() { app.execute_transaction(signed_tx.clone()).await.unwrap(); assert_eq!(app.state.get_account_nonce(alice_address).await.unwrap(), 1); - let deposits = app.state.get_deposit_events(&rollup_id).await.unwrap(); + let all_deposits = app.state.get_cached_block_deposits(); + let deposits = all_deposits.get(&rollup_id).unwrap(); assert_eq!(deposits.len(), 2); assert_eq!(deposits[0].source_action_index, starting_index_of_action); assert_eq!( diff --git a/crates/astria-sequencer/src/assets/state_ext.rs b/crates/astria-sequencer/src/assets/state_ext.rs index 0b281544e5..11075bb0d9 100644 --- a/crates/astria-sequencer/src/assets/state_ext.rs +++ b/crates/astria-sequencer/src/assets/state_ext.rs @@ -30,7 +30,7 @@ struct DenominationTrace(String); const BLOCK_FEES_PREFIX: &str = "block_fees/"; const FEE_ASSET_PREFIX: &str = "fee_asset/"; -const NATIVE_ASSET_KEY: &[u8] = b"nativeasset"; +const NATIVE_ASSET_KEY: &str = "nativeasset"; fn asset_storage_key>(asset: TAsset) -> String { format!("asset/{}", crate::storage_keys::hunks::Asset::from(asset)) @@ -71,7 +71,7 @@ pub(crate) trait StateReadExt: StateRead { #[instrument(skip_all)] async fn get_native_asset(&self) -> Result { let Some(bytes) = self - .nonverifiable_get_raw(NATIVE_ASSET_KEY) + .get_raw(NATIVE_ASSET_KEY) .await .map_err(anyhow_to_eyre) .wrap_err("failed to read raw native asset from state")? @@ -193,7 +193,7 @@ impl StateReadExt for T {} pub(crate) trait StateWriteExt: StateWrite { #[instrument(skip_all)] fn put_native_asset(&mut self, asset: &asset::TracePrefixed) { - self.nonverifiable_put_raw(NATIVE_ASSET_KEY.to_vec(), asset.to_string().into_bytes()); + self.put_raw(NATIVE_ASSET_KEY.to_string(), asset.to_string().into_bytes()); } #[instrument(skip_all)] diff --git a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs index 670f051848..6e9279f49a 100644 --- a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs @@ -142,10 +142,7 @@ impl ActionHandler for BridgeLockAction { .wrap_err("failed to deduct fee from account balance")?; state.record(deposit_abci_event); - state - .put_deposit_event(deposit) - .await - .wrap_err("failed to put deposit event into state")?; + state.cache_deposit_event(deposit); Ok(()) } } diff --git a/crates/astria-sequencer/src/bridge/state_ext.rs b/crates/astria-sequencer/src/bridge/state_ext.rs index 1a3b15c763..8c80d63a45 100644 --- a/crates/astria-sequencer/src/bridge/state_ext.rs +++ b/crates/astria-sequencer/src/bridge/state_ext.rs @@ -1,7 +1,4 @@ -use std::collections::{ - HashMap, - HashSet, -}; +use std::collections::HashMap; use astria_core::{ generated::sequencerblock::v1alpha1::Deposit as RawDeposit, @@ -33,8 +30,6 @@ use cnidarium::{ StateRead, StateWrite, }; -use futures::StreamExt as _; -use hex::ToHex as _; use prost::Message as _; use tracing::{ debug, @@ -62,10 +57,22 @@ struct AssetId([u8; 32]); #[derive(BorshSerialize, BorshDeserialize, Debug)] struct Fee(u128); +/// A wrapper to support storing a `Vec`. +/// +/// We don't currently have Borsh-encoding for `Deposit` and we also don't have a standalone +/// protobuf type representing a collection of `Deposit`s. +/// +/// This will be replaced (very soon hopefully) by a proper storage type able to be wholly Borsh- +/// encoded. Until then, we'll protobuf-encode the individual deposits and this is a collection of +/// those encoded values. +#[derive(BorshSerialize, BorshDeserialize, Debug)] +struct Deposits(Vec>); + const BRIDGE_ACCOUNT_PREFIX: &str = "bridgeacc"; const BRIDGE_ACCOUNT_SUDO_PREFIX: &str = "bsudo"; const BRIDGE_ACCOUNT_WITHDRAWER_PREFIX: &str = "bwithdrawer"; -const DEPOSIT_PREFIX: &str = "deposit"; +const DEPOSITS_EPHEMERAL_KEY: &str = "deposits"; +const DEPOSIT_PREFIX: &[u8] = b"deposit/"; const INIT_BRIDGE_ACCOUNT_BASE_FEE_STORAGE_KEY: &str = "initbridgeaccfee"; const BRIDGE_LOCK_BYTE_COST_MULTIPLIER_STORAGE_KEY: &str = "bridgelockmultiplier"; const BRIDGE_SUDO_CHANGE_FEE_STORAGE_KEY: &str = "bridgesudofee"; @@ -109,16 +116,8 @@ fn asset_id_storage_key(address: &T) -> String { ) } -fn deposit_storage_key_prefix(rollup_id: &RollupId) -> String { - format!("{DEPOSIT_PREFIX}/{}", rollup_id.encode_hex::()) -} - -fn deposit_storage_key(rollup_id: &RollupId, nonce: u32) -> Vec { - format!("{}/{}", deposit_storage_key_prefix(rollup_id), nonce).into() -} - -fn deposit_nonce_storage_key(rollup_id: &RollupId) -> Vec { - format!("depositnonce/{}", rollup_id.encode_hex::()).into() +fn deposit_storage_key(block_hash: &[u8; 32], rollup_id: &RollupId) -> Vec { + [DEPOSIT_PREFIX, block_hash, rollup_id.as_ref()].concat() } fn bridge_account_sudo_address_storage_key(address: &T) -> String { @@ -266,76 +265,37 @@ pub(crate) trait StateReadExt: StateRead + address::StateReadExt { } #[instrument(skip_all)] - async fn get_deposit_nonce(&self, rollup_id: &RollupId) -> Result { - let bytes = self - .nonverifiable_get_raw(&deposit_nonce_storage_key(rollup_id)) + fn get_cached_block_deposits(&self) -> HashMap> { + self.object_get(DEPOSITS_EPHEMERAL_KEY).unwrap_or_default() + } + + #[instrument(skip_all)] + async fn get_deposits( + &self, + block_hash: &[u8; 32], + rollup_id: &RollupId, + ) -> Result> { + let Some(bytes) = self + .nonverifiable_get_raw(&deposit_storage_key(block_hash, rollup_id)) .await .map_err(anyhow_to_eyre) - .wrap_err("failed reading raw deposit nonce from state")?; - let Some(bytes) = bytes else { - // no deposits for this rollup id yet; return 0 - return Ok(0); + .wrap_err("failed reading raw deposits from state")? + else { + return Ok(vec![]); }; - let Nonce(nonce) = - Nonce(u32::from_be_bytes(bytes.try_into().expect( - "all deposit nonces stored should be 4 bytes; this is a bug", - ))); - Ok(nonce) - } - - #[instrument(skip_all)] - async fn get_deposit_rollup_ids(&self) -> Result> { - let mut stream = std::pin::pin!(self.nonverifiable_prefix_raw(DEPOSIT_PREFIX.as_bytes())); - let mut rollup_ids = HashSet::new(); - while let Some(Ok((key, _))) = stream.next().await { - // the deposit key is of the form "deposit/{rollup_id}/{nonce}" - let key_str = - String::from_utf8(key).wrap_err("failed to convert deposit key to string")?; - let key_parts = key_str.split('/').collect::>(); - if key_parts.len() != 3 { - continue; - } - let rollup_id_bytes = - hex::decode(key_parts[1]).wrap_err("invalid rollup ID hex string")?; - let rollup_id = - RollupId::try_from_slice(&rollup_id_bytes).wrap_err("invalid rollup ID bytes")?; - rollup_ids.insert(rollup_id); - } - Ok(rollup_ids) - } + let pb_deposits = borsh::from_slice::(&bytes) + .wrap_err("failed to reconstruct protobuf deposits from storage")?; - #[instrument(skip_all)] - async fn get_deposit_events(&self, rollup_id: &RollupId) -> Result> { - let mut stream = std::pin::pin!( - self.nonverifiable_prefix_raw(deposit_storage_key_prefix(rollup_id).as_bytes()) - ); - let mut deposits = Vec::new(); - while let Some(Ok((_, value))) = stream.next().await { - let raw = RawDeposit::decode(value.as_ref()).wrap_err("invalid deposit bytes")?; + let mut deposits = Vec::with_capacity(pb_deposits.0.len()); + for pb_deposit in pb_deposits.0 { + let raw = RawDeposit::decode(pb_deposit.as_ref()).wrap_err("invalid deposit bytes")?; let deposit = Deposit::try_from_raw(raw).wrap_err("invalid deposit raw proto")?; deposits.push(deposit); } Ok(deposits) } - #[instrument(skip_all)] - async fn get_block_deposits(&self) -> Result>> { - let deposit_rollup_ids = self - .get_deposit_rollup_ids() - .await - .wrap_err("failed to get deposit rollup IDs")?; - let mut deposit_events = HashMap::new(); - for rollup_id in deposit_rollup_ids { - let rollup_deposit_events = self - .get_deposit_events(&rollup_id) - .await - .wrap_err("failed to get deposit events")?; - deposit_events.insert(rollup_id, rollup_deposit_events); - } - Ok(deposit_events) - } - #[instrument(skip_all)] async fn get_init_bridge_account_base_fee(&self) -> Result { let bytes = self @@ -483,52 +443,33 @@ pub(crate) trait StateWriteExt: StateWrite { Ok(()) } - // the deposit "nonce" for a given rollup ID during a given block. - // this is only used to generate storage keys for each of the deposits within a block, - // and is reset to 0 at the beginning of each block. + /// Push the deposit onto the end of a Vec of deposits for this rollup ID. These are held in + /// state's ephemeral store, pending being written to permanent storage during `finalize_block`. #[instrument(skip_all)] - fn put_deposit_nonce(&mut self, rollup_id: &RollupId, nonce: u32) { - self.nonverifiable_put_raw( - deposit_nonce_storage_key(rollup_id), - nonce.to_be_bytes().to_vec(), - ); - } - - // allow: false positive due to proc macro; fixed with rust/clippy 1.81 - #[allow(clippy::blocks_in_conditions)] - #[instrument(skip_all, err)] - async fn put_deposit_event(&mut self, deposit: Deposit) -> Result<()> { - let nonce = self.get_deposit_nonce(&deposit.rollup_id).await?; - self.put_deposit_nonce( - &deposit.rollup_id, - nonce.checked_add(1).ok_or_eyre("nonce overflowed")?, - ); - - let key = deposit_storage_key(&deposit.rollup_id, nonce); - self.nonverifiable_put_raw(key, deposit.into_raw().encode_to_vec()); - Ok(()) - } - - // clears the deposit nonce and all deposits for for a given rollup ID. - #[instrument(skip_all)] - async fn clear_deposit_info(&mut self, rollup_id: &RollupId) { - self.nonverifiable_delete(deposit_nonce_storage_key(rollup_id)); - let mut stream = std::pin::pin!( - self.nonverifiable_prefix_raw(deposit_storage_key_prefix(rollup_id).as_bytes()) - ); - while let Some(Ok((key, _))) = stream.next().await { - self.nonverifiable_delete(key); - } + fn cache_deposit_event(&mut self, deposit: Deposit) { + let mut cached_deposits = self.get_cached_block_deposits(); + cached_deposits + .entry(deposit.rollup_id) + .or_default() + .push(deposit); + self.object_put(DEPOSITS_EPHEMERAL_KEY, cached_deposits); } #[instrument(skip_all)] - async fn clear_block_deposits(&mut self) -> Result<()> { - let deposit_rollup_ids = self - .get_deposit_rollup_ids() - .await - .wrap_err("failed to get deposit rollup ids")?; - for rollup_id in deposit_rollup_ids { - self.clear_deposit_info(&rollup_id).await; + fn put_deposits( + &mut self, + block_hash: &[u8; 32], + all_deposits: HashMap>, + ) -> Result<()> { + for (rollup_id, deposits) in all_deposits { + let key = deposit_storage_key(block_hash, &rollup_id); + let serialized_deposits = deposits + .into_iter() + .map(|deposit| deposit.into_raw().encode_to_vec()) + .collect(); + let value = borsh::to_vec(&Deposits(serialized_deposits)) + .wrap_err("failed to serialize deposits")?; + self.nonverifiable_put_raw(key, value); } Ok(()) } @@ -586,14 +527,7 @@ mod test { use cnidarium::StateDelta; use insta::assert_snapshot; - use super::{ - asset_id_storage_key, - bridge_account_sudo_address_storage_key, - bridge_account_withdrawer_address_storage_key, - rollup_id_storage_key, - StateReadExt as _, - StateWriteExt as _, - }; + use super::*; use crate::test_utils::astria_address; fn asset_0() -> asset::Denom { @@ -759,90 +693,18 @@ mod test { } #[tokio::test] - async fn get_deposit_nonce_uninitialized_ok() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([2u8; 32]); - - // uninitialized ok - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("call to get deposit nonce should not fail on uninitialized rollup ids"), - 0u32, - "uninitialized rollup id nonce should be zero" - ); - } - - #[tokio::test] - async fn put_deposit_nonce() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([2u8; 32]); - let mut nonce = 1u32; - - // can write - state.put_deposit_nonce(&rollup_id, nonce); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("a rollup id nonce was written and must exist inside the database"), - nonce, - "stored nonce did not match expected" - ); - - // can update - nonce = 2u32; - state.put_deposit_nonce(&rollup_id, nonce); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("a rollup id nonce was written and must exist inside the database"), - nonce, - "stored nonce did not match expected" - ); - - // writing to different account is ok - let rollup_id_1 = RollupId::new([3u8; 32]); - let nonce_1 = 3u32; - state.put_deposit_nonce(&rollup_id_1, nonce_1); - assert_eq!( - state - .get_deposit_nonce(&rollup_id_1) - .await - .expect("a rollup id nonce was written and must exist inside the database"), - nonce_1, - "additional stored nonce did not match expected" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("a rollup id nonce was written and must exist inside the database"), - nonce, - "original stored nonce did not match expected" - ); - } - - #[tokio::test] - async fn get_deposit_events_empty_ok() { + async fn get_deposits_empty_ok() { let storage = cnidarium::TempStorage::new().await.unwrap(); let snapshot = storage.latest_snapshot(); let state = StateDelta::new(snapshot); + let block_hash = [32; 32]; let rollup_id = RollupId::new([2u8; 32]); // no events ok assert_eq!( state - .get_deposit_events(&rollup_id) + .get_deposits(&block_hash, &rollup_id) .await .expect("call for rollup id with no deposit events should not fail"), vec![], @@ -852,12 +714,13 @@ mod test { #[tokio::test] #[allow(clippy::too_many_lines)] // allow: it's a test - async fn get_deposit_events() { + async fn get_deposits() { let storage = cnidarium::TempStorage::new().await.unwrap(); let snapshot = storage.latest_snapshot(); let mut state = StateDelta::new(snapshot); - let rollup_id = RollupId::new([1u8; 32]); + let block_hash = [32; 32]; + let rollup_id_1 = RollupId::new([1u8; 32]); let bridge_address = astria_address(&[42u8; 20]); let amount = 10u128; let asset = asset_0(); @@ -865,7 +728,7 @@ mod test { let mut deposit = Deposit { bridge_address, - rollup_id, + rollup_id: rollup_id_1, amount, asset: asset.clone(), destination_chain_address: destination_chain_address.to_string(), @@ -873,30 +736,22 @@ mod test { source_action_index: 0, }; - let mut deposits = vec![deposit.clone()]; + let mut all_deposits = HashMap::new(); + let mut rollup_1_deposits = vec![deposit.clone()]; + all_deposits.insert(rollup_id_1, rollup_1_deposits.clone()); // can write state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events should be ok"); + .put_deposits(&block_hash, all_deposits.clone()) + .unwrap(); assert_eq!( state - .get_deposit_events(&rollup_id) + .get_deposits(&block_hash, &rollup_id_1) .await .expect("deposit info was written to the database and must exist"), - deposits, + rollup_1_deposits, "stored deposits do not match what was expected" ); - // nonce is correct - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("calls to get nonce should not fail"), - 1u32, - "nonce was consumed and should've been incremented" - ); // can write additional deposit = Deposit { @@ -904,366 +759,47 @@ mod test { source_action_index: 1, ..deposit }; - deposits.append(&mut vec![deposit.clone()]); + rollup_1_deposits.push(deposit.clone()); + all_deposits.insert(rollup_id_1, rollup_1_deposits.clone()); state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events should be ok"); - let mut returned_deposits = state - .get_deposit_events(&rollup_id) - .await - .expect("deposit info was written to the database and must exist"); - returned_deposits.sort_by_key(|d| d.amount); - deposits.sort_by_key(|d| d.amount); - assert_eq!( - returned_deposits, deposits, - "stored deposits do not match what was expected" - ); - // nonce is correct + .put_deposits(&block_hash, all_deposits.clone()) + .unwrap(); assert_eq!( state - .get_deposit_nonce(&rollup_id) + .get_deposits(&block_hash, &rollup_id_1) .await - .expect("calls to get nonce should not fail"), - 2u32, - "nonce was consumed and should've been incremented" + .expect("deposit info was written to the database and must exist"), + rollup_1_deposits, + "stored deposits do not match what was expected" ); // can write different rollup id and both ok - let rollup_id_1 = RollupId::new([2u8; 32]); + let rollup_id_2 = RollupId::new([2u8; 32]); deposit = Deposit { - rollup_id: rollup_id_1, + rollup_id: rollup_id_2, source_action_index: 2, ..deposit }; - let deposits_1 = vec![deposit.clone()]; - state - .put_deposit_event(deposit) - .await - .expect("writing deposit events should be ok"); + let rollup_2_deposits = vec![deposit.clone()]; + all_deposits.insert(rollup_id_2, rollup_2_deposits.clone()); + state.put_deposits(&block_hash, all_deposits).unwrap(); assert_eq!( state - .get_deposit_events(&rollup_id_1) + .get_deposits(&block_hash, &rollup_id_2) .await .expect("deposit info was written to the database and must exist"), - deposits_1, + rollup_2_deposits, "stored deposits do not match what was expected" ); // verify original still ok - returned_deposits = state - .get_deposit_events(&rollup_id) - .await - .expect("deposit info was written to the database and must exist"); - returned_deposits.sort_by_key(|d| d.amount); - assert_eq!( - returned_deposits, deposits, - "stored deposits do not match what was expected" - ); - } - - #[tokio::test] - async fn get_deposit_rollup_ids() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id_0 = RollupId::new([1u8; 32]); - let bridge_address = astria_address(&[42u8; 20]); - let amount = 10u128; - let asset = asset_0(); - let destination_chain_address = "0xdeadbeef"; - - let mut deposit = Deposit { - bridge_address, - rollup_id: rollup_id_0, - amount, - asset: asset.clone(), - destination_chain_address: destination_chain_address.to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - }; - - // write same rollup id twice - state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events should be ok"); - - // writing to same rollup id does not create duplicates - state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events should be ok"); - - // writing additional different rollup id - let rollup_id_1 = RollupId::new([2u8; 32]); - deposit = Deposit { - rollup_id: rollup_id_1, - source_action_index: 1, - ..deposit - }; - state - .put_deposit_event(deposit) - .await - .expect("writing deposit events should be ok"); - // ensure only two rollup ids are in system - let rollups = state - .get_deposit_rollup_ids() - .await - .expect("deposit info was written rollup ids should still be in database"); - assert_eq!(rollups.len(), 2, "only two rollup ids should exits"); - assert!( - rollups.contains(&rollup_id_0), - "deposit data was written for rollup and it should exist" - ); - assert!( - rollups.contains(&rollup_id_1), - "deposit data was written for rollup and it should exist" - ); - } - - #[tokio::test] - async fn clear_deposit_info_uninitialized_ok() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([1u8; 32]); - // uninitialized delete ok - state.clear_deposit_info(&rollup_id).await; - } - - #[tokio::test] - async fn clear_deposit_info() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([1u8; 32]); - let bridge_address = astria_address(&[42u8; 20]); - let amount = 10u128; - let asset = asset_0(); - let destination_chain_address = "0xdeadbeef"; - - let deposit = Deposit { - bridge_address, - rollup_id, - amount, - asset: asset.clone(), - destination_chain_address: destination_chain_address.to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - }; - - let deposits = vec![deposit.clone()]; - - // can write - state - .put_deposit_event(deposit) - .await - .expect("writing deposit events should be ok"); assert_eq!( state - .get_deposit_events(&rollup_id) + .get_deposits(&block_hash, &rollup_id_1) .await .expect("deposit info was written to the database and must exist"), - deposits, + rollup_1_deposits, "stored deposits do not match what was expected" ); - - // can delete - state.clear_deposit_info(&rollup_id).await; - assert_eq!( - state - .get_deposit_events(&rollup_id) - .await - .expect("deposit should return empty when none exists"), - vec![], - "deposits were cleared and should return empty vector" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("calls to get nonce should not fail"), - 0u32, - "nonce should have been deleted also" - ); - } - - #[tokio::test] - async fn clear_deposit_info_multiple_accounts() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([1u8; 32]); - let bridge_address = astria_address(&[42u8; 20]); - let amount = 10u128; - let asset = asset_0(); - let destination_chain_address = "0xdeadbeef"; - let mut deposit = Deposit { - bridge_address, - rollup_id, - amount, - asset: asset.clone(), - destination_chain_address: destination_chain_address.to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - }; - - // write to first - state - .put_deposit_event(deposit) - .await - .expect("writing deposit events should be ok"); - - // write to second - let rollup_id_1 = RollupId::new([2u8; 32]); - deposit = Deposit { - bridge_address, - rollup_id: rollup_id_1, - amount, - asset: asset.clone(), - destination_chain_address: destination_chain_address.to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 1, - }; - let deposits_1 = vec![deposit.clone()]; - - state - .put_deposit_event(deposit) - .await - .expect("writing deposit events for rollup 2 should be ok"); - - // delete first rollup's info - state.clear_deposit_info(&rollup_id).await; - assert_eq!( - state - .get_deposit_events(&rollup_id) - .await - .expect("deposit should return empty when none exists"), - vec![], - "deposits were cleared and should return empty vector" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("calls to get nonce should not fail"), - 0u32, - "nonce should have been deleted also" - ); - - // second rollup's info should be intact - assert_eq!( - state - .get_deposit_events(&rollup_id_1) - .await - .expect("deposit should return empty when none exists"), - deposits_1, - "deposits were written to the database and should exist" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id_1) - .await - .expect("calls to get nonce should not fail"), - 1u32, - "nonce was written to the database and should exist" - ); - } - - #[tokio::test] - async fn clear_block_info_uninitialized_ok() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - // uninitialized delete ok - state - .clear_block_deposits() - .await - .expect("calls to clear block deposit should succeed"); - } - - #[tokio::test] - async fn clear_block_deposits() { - let storage = cnidarium::TempStorage::new().await.unwrap(); - let snapshot = storage.latest_snapshot(); - let mut state = StateDelta::new(snapshot); - - let rollup_id = RollupId::new([1u8; 32]); - let bridge_address = astria_address(&[42u8; 20]); - let amount = 10u128; - let asset = asset_0(); - let destination_chain_address = "0xdeadbeef"; - let mut deposit = Deposit { - bridge_address, - rollup_id, - amount, - asset: asset.clone(), - destination_chain_address: destination_chain_address.to_string(), - source_transaction_id: TransactionId::new([0; 32]), - source_action_index: 0, - }; - - // write to first - state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events should be ok"); - - // write to second - let rollup_id_1 = RollupId::new([2u8; 32]); - deposit = Deposit { - rollup_id: rollup_id_1, - source_action_index: 1, - ..deposit - }; - state - .put_deposit_event(deposit.clone()) - .await - .expect("writing deposit events for rollup 2 should be ok"); - - // delete all info - state - .clear_block_deposits() - .await - .expect("clearing deposits call should not fail"); - assert_eq!( - state - .get_deposit_events(&rollup_id) - .await - .expect("deposit should return empty when none exists"), - vec![], - "deposits were cleared and should return empty vector" - ); - // check that all info was deleted - assert_eq!( - state - .get_deposit_events(&rollup_id_1) - .await - .expect("deposit should return empty when none exists"), - vec![], - "deposits were cleared and should return empty vector" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id) - .await - .expect("deposit should return empty when none exists"), - 0u32, - "nonce should have been deleted also" - ); - assert_eq!( - state - .get_deposit_nonce(&rollup_id_1) - .await - .expect("deposit should return empty when none exists"), - 0u32, - "nonce should have been deleted also" - ); } #[test] diff --git a/crates/astria-sequencer/src/ibc/ics20_transfer.rs b/crates/astria-sequencer/src/ibc/ics20_transfer.rs index 65201c5caa..67126d817e 100644 --- a/crates/astria-sequencer/src/ibc/ics20_transfer.rs +++ b/crates/astria-sequencer/src/ibc/ics20_transfer.rs @@ -739,10 +739,7 @@ async fn emit_deposit( }; let deposit_abci_event = create_deposit_event(&deposit); state.record(deposit_abci_event); - state - .put_deposit_event(deposit) - .await - .wrap_err("failed to put deposit event into state")?; + state.cache_deposit_event(deposit); Ok(()) } @@ -968,10 +965,7 @@ mod tests { ); assert_eq!(balance, 100); - let deposits = state_tx - .get_block_deposits() - .await - .expect("a deposit should exist as a result of the transfer to a bridge account"); + let deposits = state_tx.get_cached_block_deposits(); assert_eq!(deposits.len(), 1); let expected_deposit = Deposit { @@ -1049,10 +1043,7 @@ mod tests { .expect("receipt of funds to a rollup should have updated funds in the bridge account"); assert_eq!(balance, amount); - let deposits = state_tx - .get_block_deposits() - .await - .expect("a deposit should exist as a result of the transfer to a bridge account"); + let deposits = state_tx.get_cached_block_deposits(); assert_eq!(deposits.len(), 1); let expected_deposit = Deposit { @@ -1290,10 +1281,7 @@ mod tests { .expect("rollup withdrawal refund should have updated funds in the bridge address"); assert_eq!(balance, amount); - let deposit = state_tx - .get_block_deposits() - .await - .expect("a deposit should exist as a result of the rollup withdrawal refund"); + let deposit = state_tx.get_cached_block_deposits(); let expected_deposit = Deposit { bridge_address, @@ -1366,10 +1354,7 @@ mod tests { .expect("refunds of rollup withdrawals should be credited to the bridge account"); assert_eq!(balance, amount); - let deposits = state_tx - .get_block_deposits() - .await - .expect("a deposit should exist as a result of the rollup withdrawal refund"); + let deposits = state_tx.get_cached_block_deposits(); let deposit = deposits .get(&rollup_id) @@ -1450,10 +1435,7 @@ mod tests { .expect("refunding a rollup should add the tokens to its bridge address"); assert_eq!(balance, amount); - let deposits = state_tx - .get_block_deposits() - .await - .expect("a deposit should exist as a result of the rollup withdrawal refund"); + let deposits = state_tx.get_cached_block_deposits(); assert_eq!(deposits.len(), 1); let deposit = deposits.get(&rollup_id).unwrap().first().unwrap(); diff --git a/crates/astria-sequencer/src/utils.rs b/crates/astria-sequencer/src/utils.rs index 3851f6732c..4f96fddcad 100644 --- a/crates/astria-sequencer/src/utils.rs +++ b/crates/astria-sequencer/src/utils.rs @@ -13,17 +13,6 @@ use tendermint::abci::{ EventAttributeIndexExt as _, }; -pub(crate) struct Hex<'a>(pub(crate) &'a [u8]); - -impl<'a> std::fmt::Display for Hex<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for byte in self.0 { - f.write_fmt(format_args!("{byte:02x}"))?; - } - Ok(()) - } -} - pub(crate) fn cometbft_to_sequencer_validator( value: tendermint::validator::Update, ) -> Result {