Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ce7debb
chore(clippy): allow double_must_use from jsonrpsee rpc expansion
sprites0 Nov 10, 2026
a34e0d6
ci: run checks on pushes to node-builder
sprites0 Nov 10, 2026
3358788
Merge pull request #165 from hl-archive-node/ci/fix-clippy-double-mus…
sprites0 Nov 10, 2026
7b8aff9
feat(pseudo-peer): keep cache state consistent across refreshes
sprites0 Sep 1, 2026
d40787d
fix(reorg): preserve depth and serve consistent sync data
sprites0 Sep 1, 2026
c9b4abb
fix(pseudo-peer): resolve prefetched parent hashes
sprites0 Aug 19, 2026
b407560
fix(pseudo-peer): safely batch evicted block recovery
sprites0 Aug 27, 2026
ece69ec
fix(pseudo-peer): remove stale LRU keys
sprites0 Sep 3, 2026
6b77ae6
fix(pseudo-peer): bound header responses
sprites0 Sep 3, 2026
7a08107
test(reorg): round-trip HlHeader through the database codec
sprites0 Sep 3, 2026
50ba18c
fix(mainnet): gate source reorg handling to testnet
sprites0 Sep 4, 2026
d25ed1a
chore: satisfy clippy test module ordering
sprites0 Sep 4, 2026
fd65d4e
test(mainnet): guard testnet-only reorg handling
sprites0 Sep 4, 2026
2bfab61
fix(pseudo-peer): bound canonical hash map by the block cache
sprites0 Nov 10, 2026
322fb8c
fix: hardcode testnet gap blocks (69038325–69071492) in BlockStore
sprites0 Nov 10, 2026
a3fddd1
Merge pull request #166 from hl-archive-node/fix/testnet-gap-69038325
sprites0 Nov 10, 2026
7a03cea
Merge pull request #151 from hl-archive-node/fix/pseudo-peer-recover-…
sprites0 Nov 10, 2026
a8acb4c
feat: reprice precompiles at the MRB block-number hardfork
sprites0 Nov 10, 2026
6aa2085
fix(compliance): skip system txs in eth_get(Raw)TransactionByBlock{Ha…
icmono Oct 28, 2026
f0cad31
fix(compliance): look up by-number index txs by the resolved block hash
icmono Nov 2, 2026
6b332b7
Merge upstream release nb-20261110 into node-builder
dvir1994 Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Continuous Integration
on:
push:
branches:
- main
- node-builder
pull_request:
branches:
- "**"
Expand Down
3 changes: 3 additions & 0 deletions src/addons/call_forwarder.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// jsonrpsee's `#[rpc]` expansion adds `#[must_use]` to methods whose boxed future already is.
#![allow(clippy::double_must_use)]

use alloy_eips::BlockId;
use alloy_json_rpc::RpcObject;
use alloy_primitives::{Bytes, U256};
Expand Down
198 changes: 193 additions & 5 deletions src/addons/hl_node_compliance/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
//! For non-system transactions, we can just return the log as is, and the client will
//! adjust the transaction index accordingly.

// jsonrpsee's `#[rpc]` expansion adds `#[must_use]` to methods whose boxed future already is.
#![allow(clippy::double_must_use)]

use alloy_consensus::{
BlockHeader, TxReceipt,
transaction::{TransactionMeta, TxHashRef},
};
use alloy_eips::{BlockId, BlockNumberOrTag};
use alloy_json_rpc::RpcObject;
use alloy_primitives::{B256, U256};
use alloy_primitives::{B256, Bytes, U256};
use alloy_rpc_types::{
BlockTransactions, Filter, FilterChanges, FilterId, Log, TransactionInfo,
BlockTransactions, Filter, FilterChanges, FilterId, Index, Log, TransactionInfo,
pubsub::{Params, SubscriptionKind},
};
use jsonrpsee::{PendingSubscriptionSink, proc_macros::rpc};
Expand Down Expand Up @@ -450,7 +453,7 @@ impl<Eth: EthWrapper> HlNodeBlockFilterHttp<Eth> {
}

#[rpc(server, namespace = "eth")]
pub trait EthBlockApi<B: RpcObject, R: RpcObject> {
pub trait EthBlockApi<B: RpcObject, R: RpcObject, T: RpcObject> {
/// Returns information about a block by hash.
#[method(name = "getBlockByHash", with_extensions)]
async fn block_by_hash(&self, hash: B256, full: bool) -> RpcResult<Option<B>>;
Expand Down Expand Up @@ -481,6 +484,34 @@ pub trait EthBlockApi<B: RpcObject, R: RpcObject> {

#[method(name = "getTransactionReceipt", with_extensions)]
async fn transaction_receipt(&self, hash: B256) -> RpcResult<Option<R>>;

#[method(name = "getTransactionByBlockHashAndIndex", with_extensions)]
async fn transaction_by_block_hash_and_index(
&self,
hash: B256,
index: Index,
) -> RpcResult<Option<T>>;

#[method(name = "getTransactionByBlockNumberAndIndex", with_extensions)]
async fn transaction_by_block_number_and_index(
&self,
number: BlockNumberOrTag,
index: Index,
) -> RpcResult<Option<T>>;

#[method(name = "getRawTransactionByBlockHashAndIndex", with_extensions)]
async fn raw_transaction_by_block_hash_and_index(
&self,
hash: B256,
index: Index,
) -> RpcResult<Option<Bytes>>;

#[method(name = "getRawTransactionByBlockNumberAndIndex", with_extensions)]
async fn raw_transaction_by_block_number_and_index(
&self,
number: BlockNumberOrTag,
index: Index,
) -> RpcResult<Option<Bytes>>;
}

macro_rules! engine_span {
Expand Down Expand Up @@ -700,9 +731,43 @@ fn system_tx_count_for_block<Eth: EthWrapper>(
Ok(header.map(|header| header.extras.system_tx_count as usize))
}

/// Resolves `block_id` to the hash of the block plus its system tx count, so the caller can do
/// the lookup by hash: the count and the transaction then come from the same block, even if the
/// tag (`latest`, ...) or the block at that height changes in between.
/// Returns `None` if the header isn't available locally.
fn resolve_block_for_index<Eth: EthWrapper>(
eth_api: &Eth,
block_id: BlockId,
) -> Result<Option<(B256, usize)>, Eth::Error> {
let provider = eth_api.provider();
let header = provider.sealed_header_by_id(block_id).map_err(Eth::Error::from_eth_err)?;
Ok(header.map(|header| (header.hash(), header.extras.system_tx_count as usize)))
}

/// Maps a compliant (system txs hidden) index to the index in the full block, where the system
/// txs occupy the first `system_tx_count` positions.
fn full_block_index(index: Index, system_tx_count: usize) -> Index {
Index::from(usize::from(index).saturating_add(system_tx_count))
}

/// Shifts the `transactionIndex` of a tx looked up in the full block back to its compliant index.
fn adjust_transaction_index(
mut tx: alloy_rpc_types_eth::Transaction,
system_tx_count: usize,
) -> alloy_rpc_types_eth::Transaction {
if let Some(idx) = &mut tx.transaction_index {
*idx -= system_tx_count as u64;
}
tx
}

#[async_trait]
impl<Eth: EthWrapper> EthBlockApiServer<RpcBlock<Eth::NetworkTypes>, RpcReceipt<Eth::NetworkTypes>>
for HlNodeBlockFilterHttp<Eth>
impl<Eth: EthWrapper>
EthBlockApiServer<
RpcBlock<Eth::NetworkTypes>,
RpcReceipt<Eth::NetworkTypes>,
RpcTransaction<Eth::NetworkTypes>,
> for HlNodeBlockFilterHttp<Eth>
where
Eth: EthApiTypes + 'static,
ErrorObject<'static>: From<Eth::Error>,
Expand Down Expand Up @@ -842,6 +907,112 @@ where
.await?;
Ok(result)
}

/// Handler for: `eth_getTransactionByBlockHashAndIndex`
async fn transaction_by_block_hash_and_index(
&self,
ext: &Extensions,
hash: B256,
index: Index,
) -> RpcResult<Option<RpcTransaction<Eth::NetworkTypes>>> {
trace!(target: "rpc::eth", ?hash, ?index, "Serving eth_getTransactionByBlockHashAndIndex");
if !is_hl_compliant(ext, self.default_compliant) {
return self
.eth_api
.transaction_by_block_hash_and_index(hash, index)
.instrument(engine_span!())
.await;
}
let Some(system_tx_count) =
system_tx_count_for_block(&*self.eth_api, BlockId::Hash(hash.into()))?
else {
return Ok(None);
};
let tx = self
.eth_api
.transaction_by_block_hash_and_index(hash, full_block_index(index, system_tx_count))
.instrument(engine_span!())
.await?;
Ok(tx.map(|tx| adjust_transaction_index(tx, system_tx_count)))
}

/// Handler for: `eth_getTransactionByBlockNumberAndIndex`
async fn transaction_by_block_number_and_index(
&self,
ext: &Extensions,
number: BlockNumberOrTag,
index: Index,
) -> RpcResult<Option<RpcTransaction<Eth::NetworkTypes>>> {
trace!(target: "rpc::eth", ?number, ?index, "Serving eth_getTransactionByBlockNumberAndIndex");
if !is_hl_compliant(ext, self.default_compliant) {
return self
.eth_api
.transaction_by_block_number_and_index(number, index)
.instrument(engine_span!())
.await;
}
let Some((hash, system_tx_count)) =
resolve_block_for_index(&*self.eth_api, number.into())?
else {
return Ok(None);
};
let tx = self
.eth_api
.transaction_by_block_hash_and_index(hash, full_block_index(index, system_tx_count))
.instrument(engine_span!())
.await?;
Ok(tx.map(|tx| adjust_transaction_index(tx, system_tx_count)))
}

/// Handler for: `eth_getRawTransactionByBlockHashAndIndex`
async fn raw_transaction_by_block_hash_and_index(
&self,
ext: &Extensions,
hash: B256,
index: Index,
) -> RpcResult<Option<Bytes>> {
trace!(target: "rpc::eth", ?hash, ?index, "Serving eth_getRawTransactionByBlockHashAndIndex");
let index = if is_hl_compliant(ext, self.default_compliant) {
let Some(system_tx_count) =
system_tx_count_for_block(&*self.eth_api, BlockId::Hash(hash.into()))?
else {
return Ok(None);
};
full_block_index(index, system_tx_count)
} else {
index
};
self.eth_api
.raw_transaction_by_block_hash_and_index(hash, index)
.instrument(engine_span!())
.await
}

/// Handler for: `eth_getRawTransactionByBlockNumberAndIndex`
async fn raw_transaction_by_block_number_and_index(
&self,
ext: &Extensions,
number: BlockNumberOrTag,
index: Index,
) -> RpcResult<Option<Bytes>> {
trace!(target: "rpc::eth", ?number, ?index, "Serving eth_getRawTransactionByBlockNumberAndIndex");
if !is_hl_compliant(ext, self.default_compliant) {
return self
.eth_api
.raw_transaction_by_block_number_and_index(number, index)
.instrument(engine_span!())
.await;
}
let Some((hash, system_tx_count)) =
resolve_block_for_index(&*self.eth_api, number.into())?
else {
return Ok(None);
};
self.eth_api
.raw_transaction_by_block_hash_and_index(hash, full_block_index(index, system_tx_count))
.instrument(engine_span!())
.await
}
}

pub fn install<Node, EthApi>(
Expand Down Expand Up @@ -884,3 +1055,20 @@ where

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn full_block_index_skips_system_txs() {
assert_eq!(usize::from(full_block_index(Index::from(0), 0)), 0);
assert_eq!(usize::from(full_block_index(Index::from(1), 1)), 2);
assert_eq!(usize::from(full_block_index(Index::from(3), 2)), 5);
}

#[test]
fn full_block_index_saturates() {
assert_eq!(usize::from(full_block_index(Index::from(usize::MAX), 1)), usize::MAX);
}
}
Loading
Loading