diff --git a/Cargo.lock b/Cargo.lock index 3da4aa30..1cdcf7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2166,7 +2166,7 @@ dependencies = [ [[package]] name = "starknet_api" version = "0.12.0-dev.1" -source = "git+https://github.com/starkware-libs/starknet-api.git?rev=6b14aaf#6b14aaf081e7ca19f66fe8d861d99c5672e9082c" +source = "git+https://github.com/starkware-libs/starknet-api.git?rev=016ff6c#016ff6c26b75b2a2b8a46d6e7100776d5b65a59f" dependencies = [ "cairo-lang-starknet-classes", "derive_more", @@ -2204,6 +2204,16 @@ dependencies = [ "validator", ] +[[package]] +name = "starknet_mempool" +version = "0.0.0" +dependencies = [ + "assert_matches", + "derive_more", + "starknet_api", + "tokio", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index e46f3652..b541ef14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/gateway", "crates/mempool_node"] +members = ["crates/gateway", "crates/mempool", "crates/mempool_node"] [workspace.package] version = "0.0.0" @@ -21,15 +21,16 @@ as_conversions = "deny" [workspace.dependencies] assert_matches = "1.5.0" axum = "0.6.12" -clap = { version = "4.3.10" } +clap = "4.3.10" +derive_more = "0.99" hyper = "1.2.0" +papyrus_config = "0.3.0" pretty_assertions = "1.4.0" rstest = "0.17.0" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0" # TODO(Arni, 1/5/2024): Use a fixed version once the StarkNet API is stable. -starknet_api = { git = "https://github.com/starkware-libs/starknet-api.git", rev = "6b14aaf" } -papyrus_config = "0.3.0" +starknet_api = { git = "https://github.com/starkware-libs/starknet-api.git", rev = "016ff6c" } thiserror = "1.0" tokio = { version = "1", features = ["full"] } tower = "0.4.13" diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml new file mode 100644 index 00000000..4beff2d2 --- /dev/null +++ b/crates/mempool/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "starknet_mempool" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +derive_more.workspace = true +starknet_api.workspace = true + +[dev-dependencies] +assert_matches.workspace = true +tokio.workspace = true diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs new file mode 100644 index 00000000..b25f4f10 --- /dev/null +++ b/crates/mempool/src/lib.rs @@ -0,0 +1,2 @@ +// TODO: change to pub(crate) once this is used by the (not yet implemented) mempool struct. +pub mod priority_queue; diff --git a/crates/mempool/src/priority_queue.rs b/crates/mempool/src/priority_queue.rs new file mode 100644 index 00000000..bc02d687 --- /dev/null +++ b/crates/mempool/src/priority_queue.rs @@ -0,0 +1,75 @@ +#[cfg(test)] +#[path = "priority_queue_test.rs"] +pub mod priority_queue_test; + +use std::{cmp::Ordering, collections::BTreeSet}; + +use starknet_api::{ + internal_transaction::InternalTransaction, + transaction::{DeclareTransaction, DeployAccountTransaction, InvokeTransaction, Tip}, +}; + +// Assumption: for the MVP only one transaction from the same contract class can be in the mempool +// at a time. When this changes, saving the transactions themselves on the queu might no longer be +// appropriate, because we'll also need to stores transactions without indexing them. For example, +// transactions with future nonces will need to be stored, and potentially indexed on block commits. +#[derive(Clone, Debug, Default, derive_more::Deref, derive_more::DerefMut)] +pub struct PriorityQueue(BTreeSet); + +impl PriorityQueue { + pub fn push(&mut self, tx: InternalTransaction) { + let mempool_tx = PQTransaction(tx); + self.insert(mempool_tx); + } + + // Removes and returns the transaction with the highest tip. + pub fn pop(&mut self) -> Option { + self.pop_last().map(|tx| tx.0) + } +} + +#[derive(Clone, Debug, derive_more::Deref)] +pub struct PQTransaction(pub InternalTransaction); + +impl PQTransaction { + fn tip(&self) -> Tip { + match &self.0 { + InternalTransaction::Declare(declare_tx) => match &declare_tx.tx { + DeclareTransaction::V3(tx_v3) => tx_v3.tip, + _ => unimplemented!(), + }, + InternalTransaction::DeployAccount(deploy_account_tx) => match &deploy_account_tx.tx { + DeployAccountTransaction::V3(tx_v3) => tx_v3.tip, + _ => unimplemented!(), + }, + InternalTransaction::Invoke(invoke_tx) => match &invoke_tx.tx { + InvokeTransaction::V3(tx_v3) => tx_v3.tip, + _ => unimplemented!(), + }, + } + } +} + +// Compare transactions based on their tip only, which implies `Eq`, because `tip` is uint. +impl PartialEq for PQTransaction { + fn eq(&self, other: &PQTransaction) -> bool { + self.tip() == other.tip() + } +} + +/// Marks PQTransaction as capable of strict equality comparisons, signaling to the compiler it +/// adheres to equality semantics. +// Note: this depends on the implementation of `PartialEq`, see its docstring. +impl Eq for PQTransaction {} + +impl Ord for PQTransaction { + fn cmp(&self, other: &Self) -> Ordering { + self.tip().cmp(&other.tip()) + } +} + +impl PartialOrd for PQTransaction { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/crates/mempool/src/priority_queue_test.rs b/crates/mempool/src/priority_queue_test.rs new file mode 100644 index 00000000..105d827f --- /dev/null +++ b/crates/mempool/src/priority_queue_test.rs @@ -0,0 +1,62 @@ +use starknet_api::hash::StarkFelt; +use starknet_api::{ + data_availability::DataAvailabilityMode, + internal_transaction::{InternalInvokeTransaction, InternalTransaction}, + transaction::{ + InvokeTransaction, InvokeTransactionV3, ResourceBounds, ResourceBoundsMapping, Tip, + TransactionHash, + }, +}; + +use crate::priority_queue::PriorityQueue; + +pub fn create_tx_for_testing(tip: Tip, tx_hash: TransactionHash) -> InternalTransaction { + let tx = InvokeTransactionV3 { + resource_bounds: ResourceBoundsMapping::try_from(vec![ + ( + starknet_api::transaction::Resource::L1Gas, + ResourceBounds::default(), + ), + ( + starknet_api::transaction::Resource::L2Gas, + ResourceBounds::default(), + ), + ]) + .expect("Resource bounds mapping has unexpected structure."), + signature: Default::default(), + nonce: Default::default(), + sender_address: Default::default(), + calldata: Default::default(), + nonce_data_availability_mode: DataAvailabilityMode::L1, + fee_data_availability_mode: DataAvailabilityMode::L1, + paymaster_data: Default::default(), + account_deployment_data: Default::default(), + tip, + }; + + InternalTransaction::Invoke(InternalInvokeTransaction { + tx: InvokeTransaction::V3(tx), + tx_hash, + only_query: false, + }) +} + +#[tokio::test] +async fn test_priority_queue() { + let tx_hash_50 = TransactionHash(StarkFelt::ONE); + let tx_hash_100 = TransactionHash(StarkFelt::TWO); + let tx_hash_10 = TransactionHash(StarkFelt::THREE); + + let tx_tip_50 = create_tx_for_testing(Tip(50), tx_hash_50); + let tx_tip_100 = create_tx_for_testing(Tip(100), tx_hash_100); + let tx_tip_10 = create_tx_for_testing(Tip(10), tx_hash_10); + + let mut pq = PriorityQueue::default(); + pq.push(tx_tip_50.clone()); + pq.push(tx_tip_100.clone()); + pq.push(tx_tip_10.clone()); + + assert_eq!(pq.pop().unwrap(), tx_tip_100); + assert_eq!(pq.pop().unwrap(), tx_tip_50); + assert_eq!(pq.pop().unwrap(), tx_tip_10); +}