Skip to content

Commit 153cafd

Browse files
authored
refactor(rpc): pool abstraction (#348)
1 parent 1a84fa2 commit 153cafd

File tree

17 files changed

+394
-213
lines changed

17 files changed

+394
-213
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/executor/tests/fixtures/transaction.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use katana_primitives::transaction::ExecutableTxWithHash;
1010
use katana_primitives::utils::transaction::compute_invoke_v3_tx_hash;
1111
use katana_primitives::Felt;
1212
use katana_rpc_types::broadcasted::BroadcastedInvokeTx;
13+
use katana_rpc_types::{BroadcastedTx, BroadcastedTxWithChainId};
1314
use starknet::accounts::{Account, ExecutionEncoder, ExecutionEncoding, SingleOwnerAccount};
1415
use starknet::core::types::{BlockId, BlockTag, Call};
1516
use starknet::macros::{felt, selector};
@@ -119,7 +120,7 @@ pub fn invoke_executable_tx(
119120
broadcasted_tx.signature = vec![]
120121
}
121122

122-
ExecutableTxWithHash::new(broadcasted_tx.into_inner(chain_id).into())
123+
BroadcastedTxWithChainId { tx: BroadcastedTx::Invoke(broadcasted_tx), chain: chain_id }.into()
123124
}
124125

125126
#[rstest::fixture]

crates/gateway/gateway-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ katana-executor.workspace = true
1111
katana-core.workspace = true
1212
katana-primitives.workspace = true
1313
katana-metrics.workspace = true
14+
katana-pool.workspace = true
1415
katana-rpc.workspace = true
1516
katana-rpc-api.workspace = true
1617
katana-provider-api.workspace = true

crates/gateway/gateway-server/src/handlers.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use katana_gateway_types::{
77
Block, ConfirmedReceipt, ConfirmedTransaction, ContractClass, ErrorCode, GatewayError,
88
ReceiptBody, StateUpdate, StateUpdateWithBlock,
99
};
10+
use katana_pool::TxPool;
1011
use katana_primitives::block::{BlockHash, BlockIdOrTag, BlockNumber};
1112
use katana_primitives::class::{ClassHash, CompiledClass, ContractClassCompilationError};
1213
use katana_provider_api::block::{BlockIdReader, BlockProvider, BlockStatusProvider};
@@ -20,7 +21,7 @@ use starknet::core::types::ResourcePrice;
2021
/// Shared application state containing the backend
2122
#[derive(Clone)]
2223
pub struct AppState {
23-
pub api: StarknetApi<BlockifierFactory, BlockProducer<BlockifierFactory>>,
24+
pub api: StarknetApi<BlockifierFactory, TxPool, BlockProducer<BlockifierFactory>>,
2425
}
2526

2627
impl AppState {

crates/gateway/gateway-server/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,18 @@ pub struct GatewayServer {
7575
health_check: bool,
7676
metered: bool,
7777

78-
starknet_api: StarknetApi<BlockifierFactory, BlockProducer<BlockifierFactory>>,
78+
starknet_api:
79+
StarknetApi<BlockifierFactory, katana_pool::TxPool, BlockProducer<BlockifierFactory>>,
7980
}
8081

8182
impl GatewayServer {
8283
/// Create a new feeder gateway server.
8384
pub fn new(
84-
starknet_api: StarknetApi<BlockifierFactory, BlockProducer<BlockifierFactory>>,
85+
starknet_api: StarknetApi<
86+
BlockifierFactory,
87+
katana_pool::TxPool,
88+
BlockProducer<BlockifierFactory>,
89+
>,
8590
) -> Self {
8691
Self {
8792
timeout: DEFAULT_GATEWAY_TIMEOUT,

crates/pool/pool-api/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub enum PoolError {
3030
pub type PoolResult<T> = Result<T, PoolError>;
3131

3232
/// Represents a complete transaction pool.
33-
pub trait TransactionPool {
33+
pub trait TransactionPool: Send + Sync {
3434
/// The pool's transaction type.
3535
type Transaction: PoolTransaction;
3636

@@ -67,6 +67,13 @@ pub trait TransactionPool {
6767

6868
/// Get a reference to the pool's validator.
6969
fn validator(&self) -> &Self::Validator;
70+
71+
/// Get the next expected nonce for an account based on pending transactions in the pool.
72+
///
73+
/// Returns `Some(nonce)` if there are pending transactions for this account,
74+
/// where nonce is the next expected nonce (highest pending nonce + 1).
75+
/// Returns `None` if no pending transactions exist for this account.
76+
fn get_nonce(&self, address: ContractAddress) -> Option<Nonce>;
7077
}
7178

7279
// the transaction type is recommended to implement a cheap clone (eg ref-counting) so that it

crates/pool/pool/src/pool.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use katana_pool_api::{
99
PendingTransactions, PendingTx, PoolError, PoolOrd, PoolResult, PoolTransaction, Subscription,
1010
TransactionPool, TxId,
1111
};
12+
use katana_primitives::contract::Nonce;
1213
use katana_primitives::transaction::TxHash;
14+
use katana_primitives::ContractAddress;
1315
use parking_lot::RwLock;
1416
use tokio::sync::mpsc;
1517
use tracing::{error, trace, warn, Instrument};
@@ -227,6 +229,17 @@ where
227229
fn validator(&self) -> &Self::Validator {
228230
&self.inner.validator
229231
}
232+
233+
fn get_nonce(&self, address: ContractAddress) -> Option<Nonce> {
234+
self.inner
235+
.transactions
236+
.read()
237+
.iter()
238+
.filter(|tx| tx.tx.sender() == address)
239+
.map(|tx| tx.tx.nonce())
240+
.max()
241+
.map(|max_nonce| max_nonce + 1)
242+
}
230243
}
231244

232245
impl<T, V, O> Clone for Pool<T, V, O>
@@ -494,4 +507,97 @@ mod tests {
494507
#[test]
495508
#[ignore = "Txs dependency management not fully implemented yet"]
496509
fn dependent_txs_random_insertion() {}
510+
511+
#[tokio::test]
512+
async fn get_nonce_returns_none_for_unknown_address() {
513+
let pool = TestPool::test();
514+
let unknown_address = ContractAddress::from(Felt::from_hex("0xdead").unwrap());
515+
assert_eq!(pool.get_nonce(unknown_address), None);
516+
}
517+
518+
#[tokio::test]
519+
async fn get_nonce_returns_next_nonce_for_single_pending_tx() {
520+
let pool = TestPool::test();
521+
let sender = ContractAddress::from(Felt::from_hex("0x1337").unwrap());
522+
let nonce = Nonce::from(5u128);
523+
524+
let tx = PoolTx::new().with_sender(sender).with_nonce(nonce);
525+
pool.add_transaction(tx).await.unwrap();
526+
527+
// Should return nonce + 1
528+
assert_eq!(pool.get_nonce(sender), Some(Nonce::from(6u128)));
529+
}
530+
531+
#[tokio::test]
532+
async fn get_nonce_returns_highest_nonce_plus_one_for_multiple_txs() {
533+
let pool = TestPool::test();
534+
let sender = ContractAddress::from(Felt::from_hex("0x1337").unwrap());
535+
536+
// Add transactions with nonces 1, 5, 3 (out of order)
537+
let tx1 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(1u128));
538+
let tx2 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(3u128));
539+
let tx3 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(2u128));
540+
541+
pool.add_transaction(tx1).await.unwrap();
542+
pool.add_transaction(tx2).await.unwrap();
543+
pool.add_transaction(tx3).await.unwrap();
544+
545+
// Should return max(1, 3, 2) + 1 = 4
546+
assert_eq!(pool.get_nonce(sender), Some(Nonce::from(4u128)));
547+
}
548+
549+
#[tokio::test]
550+
async fn get_nonce_isolated_per_address() {
551+
let pool = TestPool::test();
552+
553+
let sender1 = ContractAddress::from(Felt::from_hex("0x1").unwrap());
554+
let sender2 = ContractAddress::from(Felt::from_hex("0x2").unwrap());
555+
let sender3 = ContractAddress::from(Felt::from_hex("0x3").unwrap());
556+
557+
// Add transactions from different senders
558+
let tx1 = PoolTx::new().with_sender(sender1).with_nonce(Nonce::from(10u128));
559+
let tx2 = PoolTx::new().with_sender(sender2).with_nonce(Nonce::from(20u128));
560+
let tx3 = PoolTx::new().with_sender(sender1).with_nonce(Nonce::from(15u128));
561+
562+
pool.add_transaction(tx1).await.unwrap();
563+
pool.add_transaction(tx2).await.unwrap();
564+
pool.add_transaction(tx3).await.unwrap();
565+
566+
// Each sender should have their own max nonce
567+
assert_eq!(pool.get_nonce(sender1), Some(Nonce::from(16u128))); // max(10, 15) + 1
568+
assert_eq!(pool.get_nonce(sender2), Some(Nonce::from(21u128))); // 20 + 1
569+
assert_eq!(pool.get_nonce(sender3), None); // No txs from sender3
570+
}
571+
572+
#[tokio::test]
573+
async fn get_nonce_updates_after_transaction_removal() {
574+
let pool = TestPool::test();
575+
let sender = ContractAddress::from(Felt::from_hex("0x1337").unwrap());
576+
577+
let tx1 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(1u128));
578+
let tx2 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(2u128));
579+
let tx3 = PoolTx::new().with_sender(sender).with_nonce(Nonce::from(3u128));
580+
581+
let hash1 = tx1.hash();
582+
let hash3 = tx3.hash();
583+
584+
pool.add_transaction(tx1).await.unwrap();
585+
pool.add_transaction(tx2.clone()).await.unwrap();
586+
pool.add_transaction(tx3).await.unwrap();
587+
588+
// Should be 4 (max nonce 3 + 1)
589+
assert_eq!(pool.get_nonce(sender), Some(Nonce::from(4u128)));
590+
591+
// Remove transactions with nonce 1 and 3
592+
pool.remove_transactions(&[hash1, hash3]);
593+
594+
// Should now be 3 (only tx with nonce 2 remains)
595+
assert_eq!(pool.get_nonce(sender), Some(Nonce::from(3u128)));
596+
597+
// Remove last transaction
598+
pool.remove_transactions(&[tx2.hash()]);
599+
600+
// Should be None (no transactions left)
601+
assert_eq!(pool.get_nonce(sender), None);
602+
}
497603
}

crates/pool/pool/src/validation/stateful.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use katana_primitives::env::{BlockEnv, CfgEnv};
2727
use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash};
2828
use katana_primitives::Felt;
2929
use katana_provider::api::state::StateProvider;
30-
use katana_provider::api::ProviderError;
3130
use parking_lot::Mutex;
3231

3332
use super::ValidationResult;
@@ -72,19 +71,6 @@ impl TxValidator {
7271
this.block_env = block_env;
7372
this.state = Arc::new(new_state);
7473
}
75-
76-
// NOTE:
77-
// If you check the get_nonce method of StatefulValidator in blockifier, under the hood it
78-
// unwraps the Option to get the state of the TransactionExecutor struct. StatefulValidator
79-
// guaranteees that the state will always be present so it is safe to uwnrap. However, this
80-
// safety is not guaranteed by TransactionExecutor itself.
81-
pub fn pool_nonce(&self, address: ContractAddress) -> Result<Option<Nonce>, ProviderError> {
82-
let this = self.inner.lock();
83-
match this.pool_nonces.get(&address) {
84-
Some(nonce) => Ok(Some(*nonce)),
85-
None => Ok(this.state.nonce(address)?),
86-
}
87-
}
8874
}
8975

9076
impl Debug for Inner {

crates/primitives/src/class.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl ContractClass {
6262
.map(|f| f.value.clone().into())
6363
.collect::<Vec<Felt>>();
6464

65-
compute_sierra_class_hash(&abi_str, &class.entry_points_by_type, sierra_program)
65+
Ok(compute_sierra_class_hash(&abi_str, &class.entry_points_by_type, sierra_program))
6666
}
6767

6868
Self::Legacy(class) => compute_legacy_class_hash(class),
@@ -232,7 +232,7 @@ pub fn compute_sierra_class_hash(
232232
abi: &str,
233233
entry_points_by_type: &ContractEntryPoints,
234234
sierra_program: &[Felt],
235-
) -> Result<Felt, ComputeClassHashError> {
235+
) -> Felt {
236236
let mut hasher = starknet_crypto::PoseidonHasher::new();
237237
hasher.update(short_string!("CONTRACT_CLASS_V0.1.0"));
238238

@@ -245,7 +245,7 @@ pub fn compute_sierra_class_hash(
245245
// Hashes Sierra program
246246
hasher.update(Poseidon::hash_array(sierra_program));
247247

248-
Ok(normalize_address(hasher.finalize()))
248+
normalize_address(hasher.finalize())
249249
}
250250

251251
/// Computes the hash of a legacy contract class.

0 commit comments

Comments
 (0)