Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ The middle row is an escape: a real sender of `0x00...01` would otherwise encode

The RPC returns this recovered address as each transaction's `from`; together with the zero `gasPrice` and the table above, that lets you recognize system transactions. To confirm them authoritatively, use hl-node's [`eth_getSystemTxsByBlockHash` / `eth_getSystemTxsByBlockNumber`](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/hyperevm/json-rpc) methods, which return the system transactions originating from HyperCore, and source blocks only from a node you trust.

### System transactions without a receipt

hl-node occasionally lists a system transaction that has no receipt (`eth_getTransactionReceipt` returns `null`), such as the `gas: 0` callback at mainnet block 49,461,291. These transactions are not executed, so nanoreth leaves them out of the block: they are missing from block transaction lists and from `eth_getEvmSystemTxsByBlock*`, while hl-node's `eth_getSystemTxsByBlock*` still returns them.

### Per-request compliance multiplexing

If you need a single endpoint to serve both filtered and unfiltered responses, use `--hl-node-compliant-multiplexed`. This adds a middleware layer that reads the `?hl=` query parameter from each request:
Expand Down
31 changes: 30 additions & 1 deletion src/node/types/reth_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ impl SealedBlock {
receipts: Vec<LegacyReceipt>,
chain_id: u64,
) -> HlBlock {
// NOTE: These types of transactions are tracked at #97.
// System txs without a receipt are not executed by hl-node (#97), so they are not part of
// the block.
system_txs.retain(|tx| tx.receipt.is_some());

let mut merged_txs = vec![];
Expand Down Expand Up @@ -402,6 +403,34 @@ mod tests {
assert_eq!(round_trip_signer(tx), one);
}

#[test]
fn receiptless_system_tx_is_dropped() {
let mut kept = system_tx(HOLDER_A, TOKEN, 0);
kept.from = Some(HOLDER_A);
let mut dropped = system_tx(HOLDER_B, TOKEN, 1);
dropped.from = Some(HOLDER_B);
dropped.receipt = None;

let sealed = SealedBlock {
header: SealedHeader {
hash: Default::default(),
header: Header { number: 56_000_000, ..Default::default() },
},
body: BlockBody { transactions: vec![], ommers: vec![], withdrawals: None },
};
let block = sealed.to_reth_block(
Default::default(),
None,
vec![kept, dropped],
vec![],
TESTNET_CHAIN_ID,
);

assert_eq!(block.body.inner.transactions.len(), 1);
assert_eq!(block.header.extras.system_tx_count, 1);
assert_eq!(block.body.inner.transactions[0].recover_signer().unwrap(), HOLDER_A);
}

/// Full sync round trip (to_reth_block -> from_db -> to_reth_block); returns the re-served
/// system tx's recovered signer, which must equal the original's.
fn round_trip_signer(tx: SystemTx) -> Address {
Expand Down
Loading