Skip to content

Commit 2bc92e6

Browse files
davidtaikochaclaudeDavid
authored
feat(rpc): engine capabilities routing, tx-list gas overflow, and L1-origin row decoding (#220)
* fix(rpc): serve engine_exchangeCapabilities with the real capability list The auth module previously did not route engine_exchangeCapabilities at all (method-not-found for the driver), and the inner EngineApi was configured with EngineCapabilities::default() -- reth's full ~23-method list -- while TAIKO_ENGINE_CAPABILITIES sat unused. Route the method on the Taiko engine trait and advertise exactly the three served methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rpc): reject u64 overflow in txPoolContent combined gas limit blockMaxGasLimit * maxTransactionsLists wrapped silently in release builds (panicked in debug) since both are caller-supplied u64 params. Use checked_mul and surface an invalid-params error instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(db): return DecompressError for truncated StoredL1Origin rows decompress() fed DB bytes into fixed-size cursor reads that panic on short input, so a corrupt row would crash the node instead of surfacing a database error. Guard the fixed 202-byte layout length up front. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: harden tx-list allocation and L1-origin decoding --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: David <cai@Davids-MacBook-Pro.local>
1 parent 84f4709 commit 2bc92e6

6 files changed

Lines changed: 155 additions & 9 deletions

File tree

crates/block/src/tx_selection/mod.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,9 @@ where
133133
let mut best_txs = pool
134134
.best_transactions_with_attributes(BestTransactionsAttributes::new(config.base_fee, None));
135135

136-
let mut lists = Vec::with_capacity(config.max_lists.max(1));
137-
lists.push(ExecutedTxList::default());
136+
let mut lists = vec![ExecutedTxList::default()];
138137
// Per-list state for adaptive DA size calibration.
139-
let mut da_guard_states = Vec::with_capacity(config.max_lists.max(1));
140-
da_guard_states.push(DaRatioState::default());
138+
let mut da_guard_states = vec![DaRatioState::default()];
141139

142140
while let Some(pool_tx) = best_txs.next() {
143141
// 1. Check cancellation
@@ -300,6 +298,44 @@ mod tests {
300298
const BENCH_LIMIT_CALLER: Address = Address::with_last_byte(0x32);
301299
const BENCH_LATE_CALLER: Address = Address::with_last_byte(0x33);
302300

301+
#[test]
302+
fn tx_selection_does_not_preallocate_the_caller_supplied_list_limit() {
303+
let chain_spec = Arc::new(unzen_chain_spec());
304+
let mut state =
305+
State::builder().with_database(db_with_contracts(&[])).with_bundle_update().build();
306+
let evm = TaikoEvmFactory.create_evm(&mut state, unzen_evm_env());
307+
let executor = TaikoBlockExecutor::new(
308+
evm,
309+
unzen_execution_ctx(),
310+
chain_spec,
311+
RethReceiptBuilder::default(),
312+
);
313+
let mut builder = ExecutorBackedBuilder { executor };
314+
let pool = testing_pool();
315+
316+
let outcome = select_and_execute_pool_transactions(
317+
&mut builder,
318+
&pool,
319+
&TxSelectionConfig {
320+
base_fee: 0,
321+
gas_limit_per_list: 1,
322+
max_da_bytes_per_list: 1,
323+
da_size_zlib_guard_bytes: 0,
324+
max_lists: usize::MAX,
325+
min_tip: 0,
326+
locals: vec![],
327+
},
328+
|| false,
329+
)
330+
.expect("an empty pool should complete without allocating the maximum list count");
331+
332+
let SelectionOutcome::Completed(lists) = outcome else {
333+
panic!("selection should not cancel")
334+
};
335+
assert_eq!(lists.len(), 1);
336+
assert!(lists[0].transactions.is_empty());
337+
}
338+
303339
#[test]
304340
fn tx_selection_breaks_on_zk_gas_error_but_keeps_skipping_invalid_txs() {
305341
let chain_spec = Arc::new(unzen_chain_spec());

crates/db/src/compress.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ use reth_codecs::{Compact, DecompressError};
44

55
use crate::model::StoredL1Origin;
66

7+
/// Fixed byte length of a `Compact`-encoded [`StoredL1Origin`] row: `block_id` (32) +
8+
/// `l2_block_hash` (32) + `l1_block_height` (32) + `l1_block_hash` (32) +
9+
/// `build_payload_args_id` (8) + `is_forced_inclusion` (1) + `signature` (65).
10+
const STORED_L1_ORIGIN_COMPACT_LEN: usize = 32 + 32 + 32 + 32 + 8 + 1 + 65;
11+
712
impl Compact for StoredL1Origin {
813
/// Takes a buffer which can be written to. *Ideally*, it returns the length written to.
914
fn to_compact<B>(&self, buf: &mut B) -> usize
@@ -83,6 +88,16 @@ impl reth_db_api::table::Compress for StoredL1Origin {
8388
impl reth_db_api::table::Decompress for StoredL1Origin {
8489
/// Decompresses owned data coming from the database.
8590
fn decompress(value: &[u8]) -> Result<Self, DecompressError> {
91+
// `from_compact` reads a fixed-size layout with panicking cursor reads and returns any
92+
// trailing bytes separately, so reject every non-exact row here instead of panicking or
93+
// silently ignoring corruption.
94+
if value.len() != STORED_L1_ORIGIN_COMPACT_LEN {
95+
return Err(DecompressError::new(std::io::Error::other(format!(
96+
"StoredL1Origin row has invalid length: got {} bytes, expected \
97+
{STORED_L1_ORIGIN_COMPACT_LEN}",
98+
value.len()
99+
))));
100+
}
86101
let (obj, _) = Compact::from_compact(value, value.len());
87102
Ok(obj)
88103
}
@@ -115,6 +130,49 @@ mod test {
115130
assert_eq!(stored, decompressed);
116131
}
117132

133+
#[test]
134+
fn test_decompress_truncated_row_returns_error() {
135+
let stored = StoredL1Origin {
136+
block_id: U256::random(),
137+
l2_block_hash: B256::random(),
138+
l1_block_height: U256::random(),
139+
l1_block_hash: B256::random(),
140+
build_payload_args_id: [1u8; 8],
141+
is_forced_inclusion: true,
142+
signature: [1u8; 65],
143+
};
144+
145+
let mut buf = Vec::new();
146+
stored.compress_to_buf(&mut buf);
147+
148+
// A corrupt/truncated row must surface as a `DecompressError`, not a panic.
149+
for len in [0, 1, 32, buf.len() - 1] {
150+
assert!(
151+
StoredL1Origin::decompress(&buf[..len]).is_err(),
152+
"decompress of {len}-byte row should fail"
153+
);
154+
}
155+
}
156+
157+
#[test]
158+
fn test_decompress_oversized_row_returns_error() {
159+
let stored = StoredL1Origin {
160+
block_id: U256::random(),
161+
l2_block_hash: B256::random(),
162+
l1_block_height: U256::random(),
163+
l1_block_hash: B256::random(),
164+
build_payload_args_id: [1u8; 8],
165+
is_forced_inclusion: true,
166+
signature: [1u8; 65],
167+
};
168+
169+
let mut buf = Vec::new();
170+
stored.compress_to_buf(&mut buf);
171+
buf.push(0xff);
172+
173+
assert!(StoredL1Origin::decompress(&buf).is_err());
174+
}
175+
118176
#[test]
119177
fn test_stored_l1_origin_compress_decompress() {
120178
let stored = StoredL1Origin {

crates/rpc/src/engine/api.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use reth_provider::{
2727
BlockReader, DBProvider, DatabaseProviderFactory, HeaderProvider, StateProviderFactory,
2828
};
2929
use reth_rpc::EngineApi;
30-
use reth_rpc_engine_api::EngineApiError;
30+
use reth_rpc_engine_api::{EngineApiError, EngineCapabilities};
3131

3232
use alethia_reth_chainspec::{hardfork::TaikoHardforks, spec::TaikoChainSpec};
3333
use alethia_reth_db::model::{
@@ -36,9 +36,16 @@ use alethia_reth_db::model::{
3636
};
3737

3838
/// The list of all supported Engine capabilities available over the engine endpoint.
39+
///
40+
/// Per the Engine API spec, `engine_exchangeCapabilities` itself is served but never listed.
3941
pub const TAIKO_ENGINE_CAPABILITIES: &[&str] =
4042
&["engine_forkchoiceUpdatedV2", "engine_getPayloadV2", "engine_newPayloadV2"];
4143

44+
/// Returns the Engine API capabilities advertised by the Taiko engine endpoint.
45+
pub fn taiko_engine_capabilities() -> EngineCapabilities {
46+
EngineCapabilities::new(TAIKO_ENGINE_CAPABILITIES.iter().copied())
47+
}
48+
4249
/// Extension trait that gives access to Taiko engine API RPC methods.
4350
///
4451
/// Note:
@@ -64,6 +71,10 @@ pub trait TaikoEngineApi<Engine: EngineTypes> {
6471
&self,
6572
payload_id: PayloadId,
6673
) -> RpcResult<Engine::ExecutionPayloadEnvelopeV2>;
74+
75+
/// Exchange the list of supported Engine API methods with the connected driver.
76+
#[method(name = "exchangeCapabilities")]
77+
async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>>;
6778
}
6879

6980
/// A concrete implementation of the `TaikoEngineApi` trait.
@@ -254,6 +265,13 @@ where
254265
self.wait_for_built_payload(payload_id).await.map_err(ErrorObjectOwned::from)?;
255266
Ok(self.convert_built_payload_to_execution_payload_envelope_v2(built_payload))
256267
}
268+
269+
/// Exchanges supported Engine API methods with the driver, returning this node's list.
270+
async fn exchange_capabilities(&self, capabilities: Vec<String>) -> RpcResult<Vec<String>> {
271+
let el_capabilities = self.inner.capabilities();
272+
el_capabilities.log_capability_mismatches(&capabilities);
273+
Ok(el_capabilities.list())
274+
}
257275
}
258276

259277
impl<Provider, EngineT, Pool, Validator, ChainSpec> IntoEngineApiRpcModule
@@ -303,6 +321,19 @@ mod tests {
303321
use reth_primitives_traits::Block as _;
304322
use std::sync::Arc;
305323

324+
#[test]
325+
fn engine_capabilities_advertise_exactly_the_served_methods() {
326+
let mut capabilities = taiko_engine_capabilities().list();
327+
capabilities.sort();
328+
329+
// Exactly the methods routed by `TaikoEngineApiServer::into_rpc`; per the Engine API
330+
// spec, `engine_exchangeCapabilities` itself must not be part of the list.
331+
assert_eq!(
332+
capabilities,
333+
vec!["engine_forkchoiceUpdatedV2", "engine_getPayloadV2", "engine_newPayloadV2"]
334+
);
335+
}
336+
306337
#[test]
307338
fn unzen_payload_overwrites_block_value_with_header_difficulty() {
308339
let chain_spec = unzen_chain_spec();

crates/rpc/src/engine/builder.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ use reth_node_api::{AddOnsContext, FullNodeComponents, NodeTypes};
66
use reth_node_builder::rpc::{EngineApiBuilder, PayloadValidatorBuilder};
77
use reth_node_core::version::{CLIENT_CODE, version_metadata};
88
use reth_rpc::EngineApi;
9-
use reth_rpc_engine_api::EngineCapabilities;
109

1110
use alethia_reth_chainspec::spec::TaikoChainSpec;
1211
use alethia_reth_primitives::engine::TaikoEngineTypes;
1312
use reth_ethereum::EthPrimitives;
1413

15-
use crate::engine::api::TaikoEngineApi;
14+
use crate::engine::api::{TaikoEngineApi, taiko_engine_capabilities};
1615

1716
/// Builder for basic [`EngineApi`] implementation.
1817
///
@@ -74,7 +73,7 @@ where
7473
ctx.node.pool().clone(),
7574
ctx.node.task_executor().clone(),
7675
client,
77-
EngineCapabilities::default(),
76+
taiko_engine_capabilities(),
7877
engine_validator,
7978
ctx.config.engine.accept_execution_requests_hash,
8079
ctx.node.network().clone(),

crates/rpc/src/eth/auth/mod.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ pub use types::{PreBuiltTxList, TxPoolContentParams, TxPoolContentWithMinTipPara
5454
#[cfg(test)]
5555
mod tests;
5656

57+
/// Computes the combined gas budget across all requested candidate tx lists, rejecting
58+
/// parameter combinations whose product does not fit in a `u64`.
59+
fn combined_tx_lists_gas_limit(
60+
block_max_gas_limit: u64,
61+
max_transactions_lists: u64,
62+
) -> Result<u64, EthApiError> {
63+
block_max_gas_limit.checked_mul(max_transactions_lists).ok_or_else(|| {
64+
EthApiError::InvalidParams(
65+
"`blockMaxGasLimit` * `maxTransactionsLists` overflows u64".to_string(),
66+
)
67+
})
68+
}
69+
5770
/// trait interface for a custom auth rpc namespace: `taikoAuth`
5871
///
5972
/// This defines the Taiko namespace where all methods are configured as trait functions.
@@ -298,6 +311,8 @@ where
298311
)
299312
.into());
300313
}
314+
let combined_gas_limit =
315+
combined_tx_lists_gas_limit(block_max_gas_limit, max_transactions_lists)?;
301316

302317
// Fetch the parent block and its state, for building the prebuilt transaction lists later.
303318
let parent_block = self
@@ -328,7 +343,7 @@ where
328343
timestamp: parent.timestamp(),
329344
suggested_fee_recipient: beneficiary,
330345
prev_randao: parent.mix_hash().unwrap_or_default(),
331-
gas_limit: block_max_gas_limit * max_transactions_lists,
346+
gas_limit: combined_gas_limit,
332347
extra_data: parent.extra_data().clone(),
333348
base_fee_per_gas: base_fee,
334349
},

crates/rpc/src/eth/auth/tests.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ fn tx_pool_content_params_conversion_defaults_min_tip_to_zero() {
201201
assert_eq!(with_tip.min_tip, 0);
202202
}
203203

204+
#[test]
205+
/// Ensures the combined tx-list gas limit multiplies normally and rejects u64 overflow.
206+
fn combined_tx_lists_gas_limit_rejects_u64_overflow() {
207+
assert_eq!(super::combined_tx_lists_gas_limit(30_000_000, 4).unwrap(), 120_000_000);
208+
assert!(super::combined_tx_lists_gas_limit(u64::MAX, 2).is_err());
209+
}
210+
204211
// ---------------------------------------------------------------------------
205212
// Proposal ID decoding tests
206213
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)