diff --git a/crates/bridge-tx-builder/src/withdrawal.rs b/crates/bridge-tx-builder/src/withdrawal.rs index 2e243b60e..c524c71dd 100644 --- a/crates/bridge-tx-builder/src/withdrawal.rs +++ b/crates/bridge-tx-builder/src/withdrawal.rs @@ -1,7 +1,7 @@ //! Provides types/traits associated with the withdrawal process. use alpen_express_primitives::{ - bridge::{OperatorIdx, TxSigningData}, + bridge::{BitcoinBlockHeight, OperatorIdx, TxSigningData}, l1::{BitcoinPsbt, TaprootSpendPath, XOnlyPk}, }; use bitcoin::{Amount, FeeRate, OutPoint, Psbt, Transaction, TxOut}; @@ -21,7 +21,7 @@ use crate::{ /// /// It has all the information required to create a transaction for fulfilling a user's withdrawal /// request and pay operator fees. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CooperativeWithdrawalInfo { /// The [`OutPoint`] of the UTXO in the Bridge Address that is to be used to service the /// withdrawal request. @@ -33,6 +33,12 @@ pub struct CooperativeWithdrawalInfo { /// The index of the operator that is assigned the withdrawal. assigned_operator_idx: OperatorIdx, + + /// The bitcoin block height before which the withdrawal has to be processed. + /// + /// Any withdrawal request whose `exec_deadline` is before the current bitcoin block height is + /// considered stale and must be ignored. + exec_deadline: BitcoinBlockHeight, } impl TxKind for CooperativeWithdrawalInfo { @@ -65,14 +71,21 @@ impl CooperativeWithdrawalInfo { deposit_outpoint: OutPoint, user_pk: XOnlyPk, assigned_operator_idx: OperatorIdx, + exec_deadline: BitcoinBlockHeight, ) -> Self { Self { deposit_outpoint, user_pk, assigned_operator_idx, + exec_deadline, } } + /// Check if the passed bitcoin block height is greater than the deadline for the withdrawal. + pub fn is_expired_at(&self, block_height: BitcoinBlockHeight) -> bool { + self.exec_deadline < block_height + } + fn create_prevout(&self, build_context: &T) -> BridgeTxBuilderResult { // We are not committing to any script path as the internal key should already be // randomized due to MuSig2 aggregation. See: @@ -195,7 +208,7 @@ mod tests { let assigned_operator_idx = assigned_operator_idx as OperatorIdx; let withdrawal_info = - CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx); + CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx, 0); let build_context = TxBuildContext::new( Network::Regtest, @@ -255,6 +268,7 @@ mod tests { deposit_outpoint, invalid_user_pk, assigned_operator_idx, + 0, ); let build_context = @@ -293,7 +307,7 @@ mod tests { let assigned_operator_idx = assigned_operator_idx as OperatorIdx; let withdrawal_info = - CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx); + CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx, 0); let build_context = TxBuildContext::new(Network::Regtest, pubkey_table, assigned_operator_idx); @@ -334,7 +348,7 @@ mod tests { let assigned_operator_idx = assigned_operator_idx as OperatorIdx; let withdrawal_info = - CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx); + CooperativeWithdrawalInfo::new(deposit_outpoint, user_pk, assigned_operator_idx, 0); let build_context = TxBuildContext::new(Network::Regtest, pubkey_table, assigned_operator_idx); diff --git a/crates/chaintsn/src/transition.rs b/crates/chaintsn/src/transition.rs index 1b445a4ec..d53c22940 100644 --- a/crates/chaintsn/src/transition.rs +++ b/crates/chaintsn/src/transition.rs @@ -351,7 +351,7 @@ fn next_rand_op_pos(rng: &mut SlotRng, num: u32) -> u32 { #[cfg(test)] mod tests { - use alpen_express_primitives::{buf::Buf32, params::OperatorConfig}; + use alpen_express_primitives::{buf::Buf32, l1::BitcoinAmount, params::OperatorConfig}; use alpen_express_state::{ block::{ExecSegment, L1Segment, L2BlockBody}, bridge_state::OperatorTable, @@ -381,7 +381,7 @@ mod tests { let maturation_queue = header_record.maturation_queue(); let mut state_cache = StateCache::new(chs); - let amt = 100_000_000_000; + let amt: BitcoinAmount = ArbitraryGenerator::new().generate(); let new_payloads_with_deposit_update_tx: Vec = (1..=params.rollup().l1_reorg_safe_depth + 1) diff --git a/crates/primitives/src/bridge.rs b/crates/primitives/src/bridge.rs index 2c74dd5de..b495b17ac 100644 --- a/crates/primitives/src/bridge.rs +++ b/crates/primitives/src/bridge.rs @@ -28,6 +28,9 @@ use crate::{ /// mathematical operations on it while managing the operator table. pub type OperatorIdx = u32; +/// The bitcoin block height that a withdrawal command references. +pub type BitcoinBlockHeight = u64; + /// A table that maps [`OperatorIdx`] to the corresponding [`PublicKey`]. /// /// We use a [`PublicKey`] instead of an [`bitcoin::secp256k1::XOnlyPublicKey`] for convenience diff --git a/crates/primitives/src/l1.rs b/crates/primitives/src/l1.rs index 2f72b28f1..18065afba 100644 --- a/crates/primitives/src/l1.rs +++ b/crates/primitives/src/l1.rs @@ -38,6 +38,8 @@ use crate::{buf::Buf32, constants::HASH_SIZE, errors::ParseError}; Arbitrary, BorshDeserialize, BorshSerialize, + Serialize, + Deserialize, )] pub struct L1TxRef(u64, u32); diff --git a/crates/rpc/api/src/lib.rs b/crates/rpc/api/src/lib.rs index 554b6a96e..c94245eb3 100644 --- a/crates/rpc/api/src/lib.rs +++ b/crates/rpc/api/src/lib.rs @@ -2,10 +2,10 @@ use alpen_express_db::types::L1TxStatus; use alpen_express_primitives::bridge::{OperatorIdx, PublickeyTable}; use alpen_express_rpc_types::{ - types::{BlockHeader, ClientStatus, DepositEntry, ExecUpdate, L1Status}, + types::{BlockHeader, ClientStatus, ExecUpdate, L1Status}, BridgeDuties, HexBytes, HexBytes32, NodeSyncStatus, RawBlockWitness, RpcCheckpointInfo, }; -use alpen_express_state::id::L2BlockId; +use alpen_express_state::{bridge_state::DepositEntry, id::L2BlockId}; use bitcoin::Txid; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; diff --git a/crates/rpc/types/src/types.rs b/crates/rpc/types/src/types.rs index 71c403fa0..5f1bb0627 100644 --- a/crates/rpc/types/src/types.rs +++ b/crates/rpc/types/src/types.rs @@ -178,27 +178,6 @@ pub struct ExecUpdate { pub da_blobs: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum DepositState { - Created, - Accepted, - Dispatched, - Executed, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DepositEntry { - /// The index of the deposit, used to identify or track the deposit within the system. - pub deposit_idx: u32, - - /// The amount of currency deposited. - pub amt: u64, - - /// Deposit state. - pub state: DepositState, -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct NodeSyncStatus { /// Current head L2 slot known to this node diff --git a/crates/state/src/bridge_ops.rs b/crates/state/src/bridge_ops.rs index 1024c589e..537cb4563 100644 --- a/crates/state/src/bridge_ops.rs +++ b/crates/state/src/bridge_ops.rs @@ -74,9 +74,9 @@ pub struct DepositIntent { } impl DepositIntent { - pub fn new(amt: u64, dest_ident: &[u8]) -> Self { + pub fn new(amt: BitcoinAmount, dest_ident: &[u8]) -> Self { Self { - amt: BitcoinAmount::from_sat(amt), + amt, dest_ident: dest_ident.to_vec(), } } diff --git a/crates/state/src/bridge_state.rs b/crates/state/src/bridge_state.rs index de7e3354f..302599fe7 100644 --- a/crates/state/src/bridge_state.rs +++ b/crates/state/src/bridge_state.rs @@ -4,7 +4,7 @@ //! extended to a more sophisticated design when we have that specced out. use alpen_express_primitives::{ - bridge::OperatorIdx, + bridge::{BitcoinBlockHeight, OperatorIdx}, buf::Buf32, l1::{self, BitcoinAmount, OutputRef, XOnlyPk}, operator::{OperatorKeyProvider, OperatorPubkeys}, @@ -12,9 +12,6 @@ use alpen_express_primitives::{ use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -/// The bitcoin block height that a withdrawal command references. -pub type BitcoinBlockHeight = u64; - /// Entry for an operator. /// /// Each operator has: @@ -260,7 +257,7 @@ impl DepositsTable { self.deposits.get(pos as usize) } - pub fn add_deposits(&mut self, tx_ref: &OutputRef, operators: &[u32], amt: u64) { + pub fn add_deposits(&mut self, tx_ref: &OutputRef, operators: &[u32], amt: BitcoinAmount) { // TODO: work out what we want to do with pending update transaction let deposit_entry = DepositEntry::new(self.next_idx(), tx_ref, operators, amt, Vec::new()); @@ -271,10 +268,14 @@ impl DepositsTable { pub fn next_idx(&self) -> u32 { self.next_idx } + + pub fn deposits(&self) -> impl Iterator { + self.deposits.iter() + } } /// Container for the state machine of a deposit factory. -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct DepositEntry { deposit_idx: u32, @@ -285,8 +286,8 @@ pub struct DepositEntry { // TODO convert this to a windowed bitmap or something notary_operators: Vec, - /// Deposit amount, in the native asset. For Bitcoin this is sats. - amt: u64, + /// Deposit amount, in the native asset. + amt: BitcoinAmount, /// Refs to txs in the maturation queue that will update the deposit entry /// when they mature. This is here so that we don't have to scan a @@ -309,7 +310,7 @@ impl DepositEntry { idx: u32, output: &OutputRef, operators: &[OperatorIdx], - amt: u64, + amt: BitcoinAmount, pending_update_txs: Vec, ) -> Self { Self { @@ -346,16 +347,21 @@ impl DepositEntry { &mut self.state } - pub fn amt(&self) -> u64 { + pub fn amt(&self) -> BitcoinAmount { self.amt } + pub fn output(&self) -> &OutputRef { + &self.output + } + pub fn set_state(&mut self, new_state: DepositState) { self.state = new_state; } } -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum DepositState { /// Deposit utxo has been recognized. Created(CreatedState), @@ -376,7 +382,7 @@ pub struct CreatedState { dest_ident: Vec, } -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct DispatchedState { /// Configuration for outputs to be written to. cmd: DispatchCommand, @@ -431,7 +437,13 @@ impl DispatchedState { /// outputs we're trying to withdraw to. /// /// May also include future information to deal with fee accounting. -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize)] +/// +/// # Note +/// +/// This is mostly here in order to support withdrawal batching (i.e., sub-denomination withdrawal +/// amounts that can be batched and then serviced together). At the moment, the underlying `Vec` of +/// [`WithdrawOutput`] always has a single element. +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub struct DispatchCommand { /// The table of withdrawal outputs. withdraw_outputs: Vec, @@ -449,6 +461,7 @@ impl DispatchCommand { /// An output constructed from [`crate::bridge_ops::WithdrawalIntent`]. #[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub struct WithdrawOutput { /// Taproot Schnorr XOnlyPubkey with the merkle root information. dest_addr: XOnlyPk, @@ -461,4 +474,8 @@ impl WithdrawOutput { pub fn new(dest_addr: XOnlyPk, amt: BitcoinAmount) -> Self { Self { dest_addr, amt } } + + pub fn dest_addr(&self) -> &XOnlyPk { + &self.dest_addr + } } diff --git a/crates/state/src/state_op.rs b/crates/state/src/state_op.rs index 9dd7109e2..113235067 100644 --- a/crates/state/src/state_op.rs +++ b/crates/state/src/state_op.rs @@ -3,13 +3,16 @@ //! decide to expand the chain state in the future such that we can't keep it //! entire in memory. -use alpen_express_primitives::{bridge::OperatorIdx, buf::Buf32}; +use alpen_express_primitives::{ + bridge::{BitcoinBlockHeight, OperatorIdx}, + buf::Buf32, +}; use borsh::{BorshDeserialize, BorshSerialize}; use tracing::*; use crate::{ bridge_ops::DepositIntent, - bridge_state::{BitcoinBlockHeight, DepositState, DispatchCommand, DispatchedState}, + bridge_state::{DepositState, DispatchCommand, DispatchedState}, chain_state::ChainState, header::L2Header, id::L2BlockId, @@ -138,7 +141,7 @@ fn apply_op_to_chainstate(op: &StateOp, state: &mut ChainState) { let (_, deposit_txs, _) = matured_block.into_parts(); for tx in deposit_txs { if let Deposit(deposit_info) = tx.tx().protocol_operation() { - println!("we got some deposit_txs"); + trace!("we got some deposit_txs"); let amt = deposit_info.amt; let deposit_intent = DepositIntent::new(amt, &deposit_info.address); deposits.push_back(deposit_intent); diff --git a/crates/state/src/tx.rs b/crates/state/src/tx.rs index c8ec7e1a9..ef17879de 100644 --- a/crates/state/src/tx.rs +++ b/crates/state/src/tx.rs @@ -1,4 +1,4 @@ -use alpen_express_primitives::l1::OutputRef; +use alpen_express_primitives::l1::{BitcoinAmount, OutputRef}; use arbitrary::Arbitrary; use borsh::{BorshDeserialize, BorshSerialize}; @@ -16,8 +16,8 @@ pub enum ProtocolOperation { #[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, Arbitrary)] pub struct DepositInfo { - /// amount in satoshis - pub amt: u64, + /// Bitcoin amount + pub amt: BitcoinAmount, /// outpoint pub outpoint: OutputRef, diff --git a/crates/tx-parser/src/deposit/deposit_tx.rs b/crates/tx-parser/src/deposit/deposit_tx.rs index ba8234579..a917829e9 100644 --- a/crates/tx-parser/src/deposit/deposit_tx.rs +++ b/crates/tx-parser/src/deposit/deposit_tx.rs @@ -26,7 +26,7 @@ pub fn extract_deposit_info(tx: &Transaction, config: &DepositTxConfig) -> Optio // Construct and return the DepositInfo Some(DepositInfo { - amt: output_0.value.to_sat(), + amt: output_0.value.into(), address: ee_address.to_vec(), outpoint: OutputRef::from(prev_out.previous_output), }) @@ -103,7 +103,7 @@ mod tests { assert!(out.is_some()); let out = out.unwrap(); - assert_eq!(out.amt, amt.to_sat()); + assert_eq!(out.amt, amt.into()); assert_eq!(out.address, ee_addr); } } diff --git a/crates/tx-parser/src/filter.rs b/crates/tx-parser/src/filter.rs index 2d93c1f55..ffc41b843 100644 --- a/crates/tx-parser/src/filter.rs +++ b/crates/tx-parser/src/filter.rs @@ -79,6 +79,7 @@ mod test { use alpen_express_btcio::test_utils::{ build_reveal_transaction_test, generate_inscription_script_test, }; + use alpen_express_primitives::l1::BitcoinAmount; use alpen_express_state::{ batch::SignedBatchCheckpoint, tx::{InscriptionData, ProtocolOperation}, @@ -266,7 +267,8 @@ mod test { if let ProtocolOperation::Deposit(deposit_info) = &result[0].proto_op() { assert_eq!(deposit_info.address, ee_addr, "EE address should match"); assert_eq!( - deposit_info.amt, config.deposit_quantity, + deposit_info.amt, + BitcoinAmount::from_sat(config.deposit_quantity), "Deposit amount should match" ); } else { @@ -389,7 +391,8 @@ mod test { i ); assert_eq!( - deposit_info.amt, config.deposit_quantity, + deposit_info.amt, + BitcoinAmount::from_sat(config.deposit_quantity), "Deposit amount should match for transaction {}", i ); diff --git a/sequencer/src/extractor.rs b/sequencer/src/extractor.rs index 75b58ed1e..6930b1a89 100644 --- a/sequencer/src/extractor.rs +++ b/sequencer/src/extractor.rs @@ -12,12 +12,14 @@ use std::sync::Arc; use alpen_express_db::traits::L1DataProvider; use alpen_express_primitives::l1::BitcoinAddress; use alpen_express_rpc_types::RpcServerError; -use alpen_express_state::tx::ProtocolOperation; +use alpen_express_state::{ + bridge_state::DepositState, chain_state::ChainState, tx::ProtocolOperation, +}; use bitcoin::{ consensus::Decodable, hashes::Hash, params::Params, Address, Amount, Network, OutPoint, TapNodeHash, Transaction, }; -use express_bridge_tx_builder::prelude::DepositInfo; +use express_bridge_tx_builder::prelude::{CooperativeWithdrawalInfo, DepositInfo}; use jsonrpsee::core::RpcResult; use tracing::{debug, error}; @@ -134,6 +136,47 @@ pub(super) async fn extract_deposit_requests( Ok((deposit_info_iter, latest_idx)) } +/// Extract the withdrawal duties from the chain state. +/// +/// This can be expensive if the chain state has a lot of deposits. +/// +/// As this is an internal API, it does need an +/// [`OperatorIdx`](alpen_express_primitives::bridge::OperatorIdx) to be passed in as a withdrawal +/// duty is relevant for all operators for now. +pub(super) fn extract_withdrawal_infos( + chain_state: &Arc, +) -> impl Iterator + '_ { + let deposits_table = chain_state.deposits_table(); + let deposits = deposits_table.deposits(); + + let withdrawal_infos = deposits.filter_map(|deposit| { + if let DepositState::Dispatched(dispatched_state) = deposit.deposit_state() { + let deposit_outpoint = deposit.output().outpoint(); + let user_pk = dispatched_state + .cmd() + .withdraw_outputs() + .first() + .expect("there should be a withdraw output in a dispatched deposit") + .dest_addr(); + let assigned_operator_idx = dispatched_state.assignee(); + let exec_deadline = dispatched_state.exec_deadline(); + + let withdrawal_info = CooperativeWithdrawalInfo::new( + *deposit_outpoint, + *user_pk, + assigned_operator_idx, + exec_deadline, + ); + + return Some(withdrawal_info); + } + + None + }); + + withdrawal_infos +} + #[cfg(test)] mod tests { use std::ops::Not; @@ -141,9 +184,24 @@ mod tests { use alpen_express_common::logging; use alpen_express_db::traits::L1DataStore; use alpen_express_mmr::CompactMmr; - use alpen_express_primitives::l1::{BitcoinAmount, L1BlockManifest, L1TxProof, OutputRef}; + use alpen_express_primitives::{ + bridge::OperatorIdx, + buf::Buf32, + l1::{BitcoinAmount, L1BlockManifest, L1TxProof, OutputRef, XOnlyPk}, + }; use alpen_express_rocksdb::{test_utils::get_rocksdb_tmp_instance, L1Db}; - use alpen_express_state::{l1::L1Tx, tx::DepositRequestInfo}; + use alpen_express_state::{ + bridge_state::{ + DepositEntry, DepositsTable, DispatchCommand, DispatchedState, OperatorTable, + WithdrawOutput, + }, + exec_env::ExecEnvState, + exec_update::UpdateInput, + genesis::GenesisStateData, + id::L2BlockId, + l1::{L1BlockId, L1HeaderRecord, L1Tx, L1ViewState}, + tx::DepositRequestInfo, + }; use alpen_test_utils::{bridge::generate_mock_unsigned_tx, ArbitraryGenerator}; use bitcoin::{ absolute::LockTime, @@ -223,6 +281,49 @@ mod tests { ); } + #[test] + fn test_extract_withdrawal_infos() { + let num_deposits = 10; + let (chain_state, num_withdrawals, needle) = + generate_empty_chain_state_with_deposits(num_deposits); + let chain_state = Arc::new(chain_state); + + let withdrawal_infos = + extract_withdrawal_infos(&chain_state).collect::>(); + + assert_eq!( + withdrawal_infos.len(), + num_withdrawals, + "number of withdrawals generated and extracted must be the same" + ); + + let deposit_state = needle.deposit_state(); + if let DepositState::Dispatched(dispatched_state) = deposit_state { + let withdraw_output = dispatched_state + .cmd() + .withdraw_outputs() + .first() + .expect("should have at least one `WithdrawOutput"); + let user_pk = withdraw_output.dest_addr(); + + let expected_info = CooperativeWithdrawalInfo::new( + *needle.output().outpoint(), + *user_pk, + dispatched_state.assignee(), + dispatched_state.exec_deadline(), + ); + + assert!( + withdrawal_infos + .into_iter() + .any(|info| info == expected_info), + "should be able to find the expected withdrawal info in the list of withdrawal infos" + ); + } else { + unreachable!("needle must be in dispatched state"); + } + } + /// Populates the db with block data. /// /// This data includes the `needle` at some random block within the provided range. @@ -434,4 +535,105 @@ mod tests { (raw_tx, deposit_request) } + + /// Generate a random chain state with some dispatched deposits. + /// + /// # Returns + /// + /// A tuple containing: + /// + /// * a random empty chain state with some deposits. + /// * the number of deposits currently dispatched (which is nearly half of all the deposits). + /// * a random [`DepositEntry`] that has been dispatched. + fn generate_empty_chain_state_with_deposits( + num_deposits: usize, + ) -> (ChainState, usize, DepositEntry) { + let l1_block_id = L1BlockId::from(Buf32::zero()); + let safe_block = L1HeaderRecord::new(l1_block_id, vec![], Buf32::zero()); + let l1_state = L1ViewState::new_at_horizon(0, safe_block); + + let operator_table = OperatorTable::new_empty(); + + let base_input = UpdateInput::new(0, vec![], Buf32::zero(), vec![]); + let exec_state = ExecEnvState::from_base_input(base_input, Buf32::zero()); + + let l2_block_id = L2BlockId::from(Buf32::zero()); + let gdata = GenesisStateData::new(l2_block_id, l1_state, operator_table, exec_state); + + let mut empty_chain_state = ChainState::from_genesis(&gdata); + + let empty_deposits = empty_chain_state.deposits_table_mut(); + let mut deposits_table = DepositsTable::new_empty(); + + let arb = ArbitraryGenerator::new(); + + let mut operators: Vec = arb.generate(); + loop { + if operators.is_empty() { + operators = arb.generate(); + continue; + } + + break; + } + + let mut rng = rand::thread_rng(); + + let random_assignee = rng.gen_range(0..operators.len()); + let random_assignee = operators[random_assignee]; + + let mut dispatched_deposits = vec![]; + let mut num_dispatched = 0; + + for _ in 0..num_deposits { + let tx_ref: OutputRef = arb.generate(); + let amt: BitcoinAmount = arb.generate(); + + deposits_table.add_deposits(&tx_ref, &operators, amt); + + // dispatch about half of the deposits + let should_dispatch = rng.gen_bool(0.5); + if should_dispatch.not() { + continue; + } + + num_dispatched += 1; + + let random_buf: Buf32 = arb.generate(); + let dest_addr = XOnlyPk::new(random_buf); + + let dispatched_state = DepositState::Dispatched(DispatchedState::new( + DispatchCommand::new(vec![WithdrawOutput::new(dest_addr, amt)]), + random_assignee, + 0, + )); + + let cur_idx = deposits_table.next_idx() - 1; + let entry = deposits_table.get_deposit_mut(cur_idx).unwrap(); + + entry.set_state(dispatched_state); + + dispatched_deposits.push(entry.idx()); + } + + assert!( + dispatched_deposits.is_empty().not(), + "some deposits should have been randomly dispatched" + ); + + let needle_index = rand::thread_rng().gen_range(0..dispatched_deposits.len()); + + let needle = dispatched_deposits + .get(needle_index) + .expect("at least one dispatched duty must be present"); + + let needle = deposits_table + .get_deposit(*needle) + .expect("deposit entry must exist at index") + .clone(); + + *empty_deposits = deposits_table; + + (empty_chain_state, num_dispatched, needle) + } } diff --git a/sequencer/src/rpc_server.rs b/sequencer/src/rpc_server.rs index e148f808b..a3077e8d9 100644 --- a/sequencer/src/rpc_server.rs +++ b/sequencer/src/rpc_server.rs @@ -16,15 +16,15 @@ use alpen_express_primitives::{ }; use alpen_express_rpc_api::{AlpenAdminApiServer, AlpenApiServer, AlpenSequencerApiServer}; use alpen_express_rpc_types::{ - errors::RpcServerError as Error, BlockHeader, BridgeDuties, ClientStatus, DaBlob, DepositEntry, - DepositState, ExecUpdate, HexBytes, HexBytes32, L1Status, NodeSyncStatus, RawBlockWitness, - RpcCheckpointInfo, + errors::RpcServerError as Error, BlockHeader, BridgeDuties, ClientStatus, DaBlob, ExecUpdate, + HexBytes, HexBytes32, L1Status, NodeSyncStatus, RawBlockWitness, RpcCheckpointInfo, }; use alpen_express_state::{ batch::BatchCheckpoint, block::L2BlockBundle, bridge_duties::BridgeDuty, bridge_ops::WithdrawalIntent, + bridge_state::DepositEntry, chain_state::ChainState, client_state::ClientState, da_blob::{BlobDest, BlobIntent}, @@ -49,7 +49,7 @@ use jsonrpsee::core::RpcResult; use tokio::sync::{oneshot, Mutex}; use tracing::*; -use crate::extractor::extract_deposit_requests; +use crate::extractor::{extract_deposit_requests, extract_withdrawal_infos}; fn fetch_l2blk( l2_prov: &Arc<::L2DataProv>, @@ -403,20 +403,7 @@ impl AlpenApiServer for AlpenRpcImpl { .get_deposit(deposit_id) .ok_or(Error::UnknownIdx(deposit_id))?; - let state = match deposit_entry.deposit_state() { - alpen_express_state::bridge_state::DepositState::Created(_) => DepositState::Created, - alpen_express_state::bridge_state::DepositState::Accepted => DepositState::Accepted, - alpen_express_state::bridge_state::DepositState::Dispatched(_) => { - DepositState::Dispatched - } - alpen_express_state::bridge_state::DepositState::Executed => DepositState::Executed, - }; - - Ok(DepositEntry { - deposit_idx: deposit_id, - amt: deposit_entry.amt(), - state, - }) + Ok(deposit_entry.clone()) } async fn sync_status(&self) -> RpcResult { @@ -512,8 +499,12 @@ impl AlpenApiServer for AlpenRpcImpl { ) -> RpcResult { info!(%operator_idx, %start_index, "received request for bridge duties"); - let l1_db_provider = self.database.l1_provider(); + // OPTIMIZE: the extraction of deposit and withdrawal duties can happen in parallel as they + // depend on independent sources of information. This optimization can be done if this RPC + // call takes a lot of time (for example, when there are hundreds of thousands of + // deposits/withdrawals). + let l1_db_provider = self.database.l1_provider(); let network = self.status_rx.l1.borrow().network; let (deposit_duties, latest_index) = @@ -521,8 +512,10 @@ impl AlpenApiServer for AlpenRpcImpl { let deposit_duties = deposit_duties.map(BridgeDuty::from); - // TODO: Extract withdrawal duties as well. - let withdrawal_duties = vec![]; + let (_, current_states) = self.get_cur_states().await?; + let chain_state = current_states.ok_or(Error::BeforeGenesis)?; + + let withdrawal_duties = extract_withdrawal_infos(&chain_state).map(BridgeDuty::from); let mut duties = vec![]; duties.extend(deposit_duties); diff --git a/tests/cooperative-bridge-flow.rs b/tests/cooperative-bridge-flow.rs index 2f056d0c7..694c9f45a 100644 --- a/tests/cooperative-bridge-flow.rs +++ b/tests/cooperative-bridge-flow.rs @@ -86,7 +86,7 @@ async fn full_flow() { event!(Level::INFO, event = "assigning withdrawal", operator_idx = %assigned_operator_idx); let withdrawal_info = - CooperativeWithdrawalInfo::new(outpoint, user_x_only_pk, assigned_operator_idx); + CooperativeWithdrawalInfo::new(outpoint, user_x_only_pk, assigned_operator_idx, 0); event!(Level::DEBUG, action = "creating withdrawal duty", withdrawal_info = ?withdrawal_info); let duty = BridgeDuty::Withdrawal(withdrawal_info); diff --git a/tests/cooperative-bridge-out-flow.rs b/tests/cooperative-bridge-out-flow.rs index 762ba70b5..47a56fac3 100644 --- a/tests/cooperative-bridge-out-flow.rs +++ b/tests/cooperative-bridge-out-flow.rs @@ -51,7 +51,7 @@ async fn withdrawal_flow() { event!(Level::INFO, event = "assigning withdrawal", operator_idx = %assigned_operator_idx); let withdrawal_info = - CooperativeWithdrawalInfo::new(outpoint, user_x_only_pk, assigned_operator_idx); + CooperativeWithdrawalInfo::new(outpoint, user_x_only_pk, assigned_operator_idx, 0); event!(Level::DEBUG, action = "creating withdrawal duty", withdrawal_info = ?withdrawal_info); let duty = BridgeDuty::Withdrawal(withdrawal_info);