Merge pull request #3 from sprites0/feat/read-precompiles

Read precompiles and some refactors
This commit is contained in:
sprites0
2025-04-28 00:25:48 -04:00
committed by GitHub
25 changed files with 418 additions and 329 deletions

17
Cargo.lock generated
View File

@ -6686,6 +6686,7 @@ dependencies = [
"reth-execution-types",
"reth-exex",
"reth-fs-util",
"reth-hyperliquid-types",
"reth-network",
"reth-network-api",
"reth-network-p2p",
@ -7394,6 +7395,7 @@ dependencies = [
"reth-chain-state",
"reth-errors",
"reth-execution-types",
"reth-hyperliquid-types",
"reth-payload-builder-primitives",
"reth-payload-primitives",
"reth-primitives",
@ -7803,15 +7805,20 @@ dependencies = [
"alloy-genesis",
"alloy-primitives",
"alloy-sol-types",
"lz4_flex",
"parking_lot",
"reth-chainspec",
"reth-ethereum-forks",
"reth-evm",
"reth-execution-types",
"reth-hyperliquid-types",
"reth-primitives",
"reth-primitives-traits",
"reth-revm",
"reth-testing-utils",
"rmp-serde",
"secp256k1 0.30.0",
"serde",
"serde_json",
"sha2 0.10.8",
]
@ -7954,6 +7961,16 @@ dependencies = [
"thiserror 2.0.11",
]
[[package]]
name = "reth-hyperliquid-types"
version = "1.2.0"
dependencies = [
"alloy-primitives",
"clap",
"reth-cli-commands",
"serde",
]
[[package]]
name = "reth-invalid-block-hooks"
version = "1.2.0"

View File

@ -129,6 +129,7 @@ members = [
"crates/trie/parallel/",
"crates/trie/sparse",
"crates/trie/trie",
"crates/hyperliquid-types",
"examples/beacon-api-sidecar-fetcher/",
"examples/beacon-api-sse/",
"examples/bsc-p2p",
@ -622,6 +623,8 @@ snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
# See: https://github.com/eira-fransham/crunchy/issues/13
crunchy = "=0.2.2"
# hyperliquid
reth-hyperliquid-types = { path = "crates/hyperliquid-types" }
lz4_flex = "0.11.3"
rmp-serde = "1.3.0"

View File

@ -2,7 +2,13 @@
Hyperliquid archive node based on [reth](https://github.com/paradigmxyz/reth).
## How to run
## ⚠️ IMPORTANT: System Transactions Appear as Pseudo Transactions
Deposit transactions from `0x222..22` to user addresses are intentionally recorded as pseudo transactions.
This change simplifies block explorers, making it easier to track deposit timestamps.
Ensure careful handling when indexing.
## How to run (mainnet)
```sh
# Fetch EVM blocks
@ -11,11 +17,27 @@ $ goofys --region=ap-northeast-1 --requester-pays hl-mainnet-evm-blocks evm-bloc
# Run node
$ make install
$ reth node --http --http.addr 0.0.0.0 --http.api eth,ots,net,web3 --ws --ws.addr 0.0.0.0 --ws.origins '*' --ws.api eth,ots,net,web3 --ingest-dir ~/evm-blocks --ws.port 8545
$ reth node --http --http.addr 0.0.0.0 --http.api eth,ots,net,web3 \
--ws --ws.addr 0.0.0.0 --ws.origins '*' --ws.api eth,ots,net,web3 --ingest-dir ~/evm-blocks --ws.port 8545
```
## System Transactions Appear as Pseudo Transactions
## How to run (testnet)
Deposit transactions from `0x222..22` to user addresses are intentionally recorded as pseudo transactions.
This change simplifies block explorers, making it easier to track deposit timestamps.
Ensure careful handling when indexing.
Testnet is supported since block 21043587.
```sh
# Get testnet genesis at block 21043587
$ cd ~
$ git clone https://github.com/sprites0/hl-testnet-genesis
$ zstd --rm -d ~/hl-testnet-genesis/*.zst
# Init node
$ make install
$ reth init-state --without-evm --chain testnet --header ~/hl-testnet-genesis/21043587.rlp \
--header-hash 0x2404e9a38b87e9028295df91e922c2e804c3ca75b550289cf5b353a9c61c34ea \
~/hl-testnet-genesis/21043587.jsonl --total-difficulty 0
# Run node
$ reth node --http --http.addr 0.0.0.0 --http.api eth,ots,net,web3 \
--ws --ws.addr 0.0.0.0 --ws.origins '*' --ws.api eth,ots,net,web3 --ingest-dir ~/evm-blocks --ws.port 8546
```

View File

@ -64,6 +64,7 @@ reth-node-events.workspace = true
reth-node-metrics.workspace = true
reth-consensus.workspace = true
reth-prune.workspace = true
reth-hyperliquid-types.workspace = true
# crypto
alloy-eips = { workspace = true, features = ["kzg"] }

View File

@ -15,59 +15,18 @@ use reth_node_builder::EngineTypes;
use reth_node_builder::NodeTypesWithEngine;
use reth_node_builder::{rpc::RethRpcAddOns, FullNode};
use reth_payload_builder::{EthBuiltPayload, EthPayloadBuilderAttributes, PayloadId};
use reth_primitives::TransactionSigned;
use reth_primitives::{Transaction as TypedTransaction, TransactionSigned};
use reth_provider::{BlockHashReader, StageCheckpointReader};
use reth_rpc_api::EngineApiClient;
use reth_rpc_layer::AuthClientService;
use reth_stages::StageId;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use crate::serialized::TypedTransaction;
use crate::serialized::{self, BlockInner};
use crate::serialized::{BlockAndReceipts, EvmBlock};
use crate::spot_meta::erc20_contract_to_spot_token;
pub(crate) struct BlockIngest(pub PathBuf);
#[derive(Debug, Clone, Serialize, Deserialize)]
struct EvmContract {
pub address: Address,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SpotToken {
pub index: u64,
#[serde(rename = "evmContract")]
pub evm_contract: Option<EvmContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SpotMeta {
tokens: Vec<SpotToken>,
}
async fn fetch_spot_meta(is_testnet: bool) -> Result<SpotMeta, Box<dyn std::error::Error>> {
let url = if is_testnet {
"https://api.hyperliquid-testnet.xyz"
} else {
"https://api.hyperliquid.xyz"
};
let url = format!("{}/info", url);
// post body: {"type": "spotMeta"}
let client = reqwest::Client::new();
let response = client.post(url).json(&serde_json::json!({"type": "spotMeta"})).send().await?;
Ok(response.json().await?)
}
fn to_evm_map(meta: &SpotMeta) -> std::collections::HashMap<Address, u64> {
let mut map = std::collections::HashMap::new();
for token in &meta.tokens {
if let Some(evm_contract) = &token.evm_contract {
map.insert(evm_contract.address, token.index);
}
}
map
}
async fn submit_payload<Engine: PayloadTypes + EngineTypes>(
engine_api_client: &HttpClient<AuthClientService<HttpBackend>>,
payload: EthBuiltPayload,
@ -95,7 +54,7 @@ async fn submit_payload<Engine: PayloadTypes + EngineTypes>(
}
impl BlockIngest {
pub(crate) fn collect_block(&self, height: u64) -> Option<super::serialized::Block> {
pub(crate) fn collect_block(&self, height: u64) -> Option<BlockAndReceipts> {
let f = ((height - 1) / 1_000_000) * 1_000_000;
let s = ((height - 1) / 1_000) * 1_000;
let path = format!("{}/{f}/{s}/{height}.rmp.lz4", self.0.to_string_lossy());
@ -103,7 +62,7 @@ impl BlockIngest {
let file = std::fs::File::open(path).unwrap();
let file = std::io::BufReader::new(file);
let mut decoder = lz4_flex::frame::FrameDecoder::new(file);
let blocks: Vec<serialized::Block> = rmp_serde::from_read(&mut decoder).unwrap();
let blocks: Vec<BlockAndReceipts> = rmp_serde::from_read(&mut decoder).unwrap();
Some(blocks[0].clone())
} else {
None
@ -135,14 +94,14 @@ impl BlockIngest {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis();
let engine_api = node.auth_server_handle().http_client();
let mut evm_map = to_evm_map(&fetch_spot_meta(node.chain_spec().chain_id() == 998).await?);
let mut evm_map = erc20_contract_to_spot_token(node.chain_spec().chain_id()).await?;
loop {
let Some(original_block) = self.collect_block(height) else {
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
continue;
};
let BlockInner::Reth115(mut block) = original_block.block;
let EvmBlock::Reth115(mut block) = original_block.block;
{
debug!(target: "reth::cli", ?block, "Built new payload");
let timestamp = block.header().timestamp();
@ -153,38 +112,28 @@ impl BlockIngest {
std::mem::take(block.body_mut());
let mut system_txs = vec![];
for transaction in original_block.system_txs {
let s = match &transaction.tx {
TypedTransaction::Legacy(tx) => match tx.input().len() {
0 => U256::from(0x1),
_ => {
let TypedTransaction::Legacy(tx) = &transaction.tx else {
panic!("Unexpected transaction type");
};
let TxKind::Call(to) = tx.to else {
panic!("Unexpected contract creation");
};
let s = if tx.input().is_empty() {
U256::from(0x1)
} else {
loop {
match evm_map.get(&to).cloned() {
Some(s) => {
break {
let mut addr = [0u8; 32];
addr[12] = 0x20;
addr[24..32].copy_from_slice(s.to_be_bytes().as_ref());
U256::from_be_bytes(addr)
if let Some(spot) = evm_map.get(&to) {
break spot.to_s();
}
}
None => {
info!("Contract not found: {:?}, fetching again...", to);
evm_map = to_evm_map(
&fetch_spot_meta(
node.chain_spec().chain_id() == 998,
)
.await?,
info!(
"Contract not found: {:?} from spot mapping, fetching again...",
to
);
continue;
evm_map =
erc20_contract_to_spot_token(node.chain_spec().chain_id())
.await?;
}
}
}
}
},
_ => unreachable!(),
};
let signature = PrimitiveSignature::new(
// from anvil
@ -192,7 +141,7 @@ impl BlockIngest {
s,
true,
);
let typed_transaction = transaction.tx.to_reth();
let typed_transaction = transaction.tx;
let tx = TransactionSigned::new(
typed_transaction,
signature,

View File

@ -6,8 +6,7 @@ static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::ne
mod block_ingest;
mod forwarder;
mod serialized;
use std::path::PathBuf;
mod spot_meta;
use block_ingest::BlockIngest;
use clap::{Args, Parser};
@ -19,10 +18,6 @@ use tracing::info;
#[derive(Args, Debug, Clone)]
struct HyperliquidExtArgs {
/// EVM blocks base directory
#[arg(long, default_value = "/tmp/evm-blocks")]
pub ingest_dir: PathBuf,
/// Upstream RPC URL to forward incoming transactions.
#[arg(long, default_value = "https://rpc.hyperliquid.xyz/evm")]
pub upstream_rpc_url: String,
@ -37,12 +32,13 @@ fn main() {
}
if let Err(err) = Cli::<EthereumChainSpecParser, HyperliquidExtArgs>::parse().run(
|builder, ingest_args| async move {
|builder, ext_args| async move {
let ingest_dir = builder.config().ingest_dir.clone().expect("ingest dir not set");
info!(target: "reth::cli", "Launching node");
let handle = builder
.node(EthereumNode::default())
.extend_rpc_modules(move |ctx| {
let upstream_rpc_url = ingest_args.upstream_rpc_url.clone();
let upstream_rpc_url = ext_args.upstream_rpc_url.clone();
let rpc = forwarder::EthForwarderExt::new(upstream_rpc_url).into_rpc();
for method_name in rpc.method_names() {
ctx.modules.remove_method_from_configured(method_name);
@ -55,7 +51,6 @@ fn main() {
.launch()
.await?;
let ingest_dir = ingest_args.ingest_dir;
let ingest = BlockIngest(ingest_dir);
ingest.run(handle.node).await.unwrap();
handle.node_exit_future.await

View File

@ -1,105 +1,43 @@
use alloy_consensus::{TxEip1559, TxEip2930, TxLegacy};
use reth_primitives::{Log, SealedBlock, Transaction};
use alloy_primitives::{Address, Log};
use reth_hyperliquid_types::{ReadPrecompileInput, ReadPrecompileResult};
use reth_primitives::{SealedBlock, Transaction};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct SerializedTransaction {
pub transaction: TypedTransaction,
pub signature: SerializedSignature,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BlockAndReceipts {
pub block: EvmBlock,
pub receipts: Vec<LegacyReceipt>,
#[serde(default)]
pub system_txs: Vec<SystemTx>,
#[serde(default)]
pub read_precompile_calls: Vec<(Address, Vec<(ReadPrecompileInput, ReadPrecompileResult)>)>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct SerializedSignature {
pub r: [u8; 32],
pub s: [u8; 32],
pub v: [u8; 8],
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) enum BlockInner {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum EvmBlock {
Reth115(SealedBlock),
}
/// A raw transaction.
///
/// Transaction types were introduced in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) enum TypedTransaction {
/// Legacy transaction (type `0x0`).
///
/// Traditional Ethereum transactions, containing parameters `nonce`, `gasPrice`, `gasLimit`,
/// `to`, `value`, `data`, `v`, `r`, and `s`.
///
/// These transactions do not utilize access lists nor do they incorporate EIP-1559 fee market
/// changes.
Legacy(TxLegacy),
/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)), type `0x1`.
///
/// The `accessList` specifies an array of addresses and storage keys that the transaction
/// plans to access, enabling gas savings on cross-contract calls by pre-declaring the accessed
/// contract and storage slots.
Eip2930(TxEip2930),
/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)), type `0x2`.
///
/// Unlike traditional transactions, EIP-1559 transactions use an in-protocol, dynamically
/// changing base fee per gas, adjusted at each block to manage network congestion.
///
/// - `maxPriorityFeePerGas`, specifying the maximum fee above the base fee the sender is
/// willing to pay
/// - `maxFeePerGas`, setting the maximum total fee the sender is willing to pay.
///
/// The base fee is burned, while the priority fee is paid to the miner who includes the
/// transaction, incentivizing miners to include transactions with higher priority fees per
/// gas.
Eip1559(TxEip1559),
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LegacyReceipt {
tx_type: LegacyTxType,
success: bool,
cumulative_gas_used: u64,
logs: Vec<Log>,
}
impl TypedTransaction {
pub(crate) fn to_reth(self) -> Transaction {
match self {
Self::Legacy(tx) => Transaction::Legacy(tx),
Self::Eip2930(tx) => Transaction::Eip2930(tx),
Self::Eip1559(tx) => Transaction::Eip1559(tx),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum LegacyTxType {
Legacy = 0,
Eip2930 = 1,
Eip1559 = 2,
Eip4844 = 3,
Eip7702 = 4,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) enum TxType {
/// Legacy transaction type.
Legacy,
/// EIP-2930 transaction type.
Eip2930,
/// EIP-1559 transaction type.
Eip1559,
/// EIP-4844 transaction type.
Eip4844,
/// EIP-7702 transaction type.
Eip7702,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Receipt {
/// Receipt type.
pub tx_type: TxType,
/// If transaction is executed successfully.
///
/// This is the `statusCode`
pub success: bool,
/// Gas used
pub cumulative_gas_used: u64,
/// Log send from contracts.
pub logs: Vec<Log>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct SystemTransaction {
pub receipt: Receipt,
pub tx: TypedTransaction,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Block {
pub block: BlockInner,
pub system_txs: Vec<SystemTransaction>,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SystemTx {
pub tx: Transaction,
pub receipt: Option<LegacyReceipt>,
}

61
bin/reth/src/spot_meta.rs Normal file
View File

@ -0,0 +1,61 @@
use alloy_primitives::{Address, U256};
use eyre::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub(crate) const MAINNET_CHAIN_ID: u64 = 999;
pub(crate) const TESTNET_CHAIN_ID: u64 = 998;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct EvmContract {
address: Address,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SpotToken {
index: u64,
#[serde(rename = "evmContract")]
evm_contract: Option<EvmContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SpotMeta {
tokens: Vec<SpotToken>,
}
pub(crate) struct SpotId {
pub index: u64,
}
impl SpotId {
pub(crate) fn to_s(&self) -> U256 {
let mut addr = [0u8; 32];
addr[12] = 0x20;
addr[24..32].copy_from_slice(self.index.to_be_bytes().as_ref());
U256::from_be_bytes(addr)
}
}
async fn fetch_spot_meta(chain_id: u64) -> Result<SpotMeta> {
let url = match chain_id {
MAINNET_CHAIN_ID => "https://api.hyperliquid.xyz/info",
TESTNET_CHAIN_ID => "https://api.hyperliquid-testnet.xyz/info",
_ => return Err(Error::msg("unknown chain id")),
};
let client = reqwest::Client::new();
let response = client.post(url).json(&serde_json::json!({"type": "spotMeta"})).send().await?;
Ok(response.json().await?)
}
pub(crate) async fn erc20_contract_to_spot_token(
chain_id: u64,
) -> Result<BTreeMap<Address, SpotId>> {
let meta = fetch_spot_meta(chain_id).await?;
let mut map = BTreeMap::new();
for token in &meta.tokens {
if let Some(evm_contract) = &token.evm_contract {
map.insert(evm_contract.address, SpotId { index: token.index });
}
}
Ok(map)
}

View File

@ -114,6 +114,10 @@ pub struct NodeCommand<
/// Additional cli arguments
#[command(flatten, next_help_heading = "Extension")]
pub ext: Ext,
/// EVM blocks base directory
#[arg(long, default_value = "/tmp/evm-blocks")]
pub ingest_dir: PathBuf,
}
impl<C: ChainSpecParser> NodeCommand<C> {
@ -165,6 +169,7 @@ impl<
pruning,
ext,
engine,
ingest_dir,
} = self;
// set up node config
@ -183,6 +188,7 @@ impl<
dev,
pruning,
engine,
ingest_dir: Some(ingest_dir),
};
let data_dir = node_config.datadir();

View File

@ -31,6 +31,9 @@ alloy-eips.workspace = true
tokio = { workspace = true, features = ["sync"] }
futures.workspace = true
# hyperevm
reth-hyperliquid-types.workspace = true
# misc
auto_impl.workspace = true
serde.workspace = true

View File

@ -10,7 +10,7 @@ use std::fs::File;
use std::io::{Read, Write};
pub(crate) fn load_hl_testnet() -> ChainSpec {
const TESTNET_GENESIS_URL: &str = "https://raw.githubusercontent.com/sprites0/hl-testnet-genesis/main/19386700.rlp";
const TESTNET_GENESIS_URL: &str = "https://raw.githubusercontent.com/sprites0/hl-testnet-genesis/main/21304281.rlp";
fn download_testnet_genesis() -> Result<&'static str, Box<dyn std::error::Error>> {
let path = "/tmp/hl_testnet.rmp.lz4";

View File

@ -31,6 +31,12 @@ alloy-consensus.workspace = true
sha2.workspace = true
serde_json.workspace = true
serde = { workspace = true, features = ["derive"] }
rmp-serde.workspace = true
lz4_flex.workspace = true
reth-hyperliquid-types.workspace = true
parking_lot.workspace = true
[dev-dependencies]
reth-testing-utils.workspace = true

View File

@ -23,31 +23,33 @@ use alloy_evm::eth::EthEvmContext;
pub use alloy_evm::EthEvm;
use alloy_primitives::bytes::BufMut;
use alloy_primitives::hex::{FromHex, ToHexExt};
use alloy_primitives::{Address, Bytes, U256};
use alloy_primitives::{Address, B256};
use alloy_primitives::{Bytes, U256};
use core::{convert::Infallible, fmt::Debug};
use parking_lot::RwLock;
use reth_chainspec::{ChainSpec, EthChainSpec, MAINNET};
use reth_evm::Database;
use reth_evm::{ConfigureEvm, ConfigureEvmEnv, EvmEnv, EvmFactory, NextBlockEnvAttributes};
use reth_hyperliquid_types::{ReadPrecompileInput, ReadPrecompileResult};
use reth_primitives::TransactionSigned;
use reth_primitives::{SealedBlock, Transaction};
use reth_revm::context::result::{EVMError, HaltReason};
use reth_revm::context::{Block, Cfg, ContextTr};
use reth_revm::handler::{EthPrecompiles, PrecompileProvider};
use reth_revm::context::Cfg;
use reth_revm::handler::EthPrecompiles;
use reth_revm::inspector::NoOpInspector;
use reth_revm::interpreter::interpreter::EthInterpreter;
use reth_revm::interpreter::{Gas, InstructionResult, InterpreterResult};
use reth_revm::precompile::{
PrecompileError, PrecompileErrors, PrecompileFn, PrecompileOutput, PrecompileResult,
Precompiles,
};
use reth_revm::precompile::{PrecompileError, PrecompileErrors, Precompiles};
use reth_revm::MainBuilder;
use reth_revm::{
context::{BlockEnv, CfgEnv, TxEnv},
context_interface::block::BlobExcessGasAndPrice,
specification::hardfork::SpecId,
};
use reth_revm::{revm, Context, Inspector, MainBuilder, MainContext};
use sha2::Digest;
use reth_revm::{Context, Inspector, MainContext};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Write;
use std::sync::OnceLock;
use std::path::PathBuf;
mod config;
mod fix;
@ -65,15 +67,23 @@ pub mod eip6110;
/// Ethereum-related EVM configuration.
#[derive(Debug, Clone)]
pub struct EthEvmConfig {
chain_spec: Arc<ChainSpec>,
evm_factory: HyperliquidEvmFactory,
ingest_dir: Option<PathBuf>,
}
impl EthEvmConfig {
/// Creates a new Ethereum EVM configuration with the given chain spec.
pub fn new(chain_spec: Arc<ChainSpec>) -> Self {
Self { chain_spec, evm_factory: Default::default() }
Self { chain_spec, ingest_dir: None, evm_factory: Default::default() }
}
pub fn with_ingest_dir(mut self, ingest_dir: PathBuf) -> Self {
self.ingest_dir = Some(ingest_dir.clone());
self.evm_factory.ingest_dir = Some(ingest_dir);
self
}
/// Creates a new Ethereum EVM configuration for the ethereum mainnet.
@ -182,137 +192,65 @@ impl ConfigureEvmEnv for EthEvmConfig {
}
}
/// A custom precompile that contains static precompiles.
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct L1ReadPrecompiles<CTX> {
precompiles: EthPrecompiles<CTX>,
warm_addresses: Vec<Address>,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BlockAndReceipts {
#[serde(default)]
pub read_precompile_calls: Vec<(Address, Vec<(ReadPrecompileInput, ReadPrecompileResult)>)>,
}
impl<CTX: ContextTr> L1ReadPrecompiles<CTX> {
fn new() -> Self {
let mut this = Self { precompiles: EthPrecompiles::default(), warm_addresses: vec![] };
this.update_warm_addresses(false);
this
}
fn update_warm_addresses(&mut self, precompile_enabled: bool) {
self.warm_addresses = if !precompile_enabled {
self.precompiles.warm_addresses().collect()
} else {
self.precompiles
.warm_addresses()
.chain((0..=9).into_iter().map(|x| {
let mut addr = [0u8; 20];
addr[18] = 0x8;
addr[19] = x;
Address::from_slice(&addr)
}))
.collect()
}
}
}
impl<CTX: ContextTr> PrecompileProvider for L1ReadPrecompiles<CTX> {
type Context = CTX;
type Output = InterpreterResult;
fn set_spec(&mut self, spec: <<Self::Context as ContextTr>::Cfg as Cfg>::Spec) {
self.precompiles.set_spec(spec);
// TODO: How to pass block number and chain id?
self.update_warm_addresses(false);
}
fn run(
&mut self,
context: &mut Self::Context,
address: &Address,
bytes: &Bytes,
gas_limit: u64,
) -> Result<Option<Self::Output>, revm::precompile::PrecompileErrors> {
if address[..18] == [0u8; 18] {
let maybe_precompile_index = u16::from_be_bytes([address[18], address[19]]);
let precompile_base =
std::env::var("PRECOMPILE_BASE").unwrap_or("/tmp/precompiles".to_string());
if 0x800 <= maybe_precompile_index && maybe_precompile_index <= 0x809 {
let block_number = context.block().number();
let input = vec![];
let mut writer = input.writer();
writer.write(&address.as_slice()).unwrap();
writer.write(bytes).unwrap();
writer.flush().unwrap();
let hash = sha2::Sha256::digest(writer.get_ref());
let file =
format!("{}/{}/{}.json", precompile_base, block_number, hash.encode_hex());
let (output, gas) = match load_result(file) {
Ok(Some(value)) => value,
Ok(None) => {
return Ok(Some(InterpreterResult {
result: InstructionResult::Return,
gas: Gas::new(gas_limit),
output: Bytes::new(),
}))
}
Err(value) => return Err(value),
};
return Ok(Some(InterpreterResult {
result: InstructionResult::Return,
gas: Gas::new(gas_limit - gas),
output,
}));
}
}
self.precompiles.run(context, address, bytes, gas_limit)
}
fn contains(&self, address: &Address) -> bool {
self.precompiles.contains(address)
}
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address> + '_> {
Box::new(self.warm_addresses.iter().cloned())
}
}
fn load_result(file: String) -> Result<Option<(Bytes, u64)>, PrecompileErrors> {
let Ok(file) = std::fs::File::open(file) else {
return Ok(None);
};
let reader = std::io::BufReader::new(file);
let json: serde_json::Value = serde_json::from_reader(reader).unwrap();
let object = json.as_object().unwrap().clone();
let success = object.get("success").unwrap().as_bool().unwrap();
if !success {
return Err(PrecompileErrors::Error(PrecompileError::other("Invalid input")));
}
let output =
Bytes::from_hex(object.get("output").unwrap().as_str().unwrap().to_owned()).unwrap();
let gas = object.get("gas").unwrap_or(&serde_json::json!(0)).as_u64().unwrap_or_default();
println!("output: {}, gas: {}", output.encode_hex(), gas);
Ok(Some((output, gas)))
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum EvmBlock {
Reth115(SealedBlock),
}
/// Custom EVM configuration.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct HyperliquidEvmFactory;
pub struct HyperliquidEvmFactory {
ingest_dir: Option<PathBuf>,
}
pub(crate) fn collect_block(ingest_path: PathBuf, height: u64) -> Option<BlockAndReceipts> {
let f = ((height - 1) / 1_000_000) * 1_000_000;
let s = ((height - 1) / 1_000) * 1_000;
let path = format!("{}/{f}/{s}/{height}.rmp.lz4", ingest_path.to_string_lossy());
if std::path::Path::new(&path).exists() {
let file = std::fs::File::open(path).unwrap();
let file = std::io::BufReader::new(file);
let mut decoder = lz4_flex::frame::FrameDecoder::new(file);
let blocks: Vec<BlockAndReceipts> = rmp_serde::from_read(&mut decoder).unwrap();
Some(blocks[0].clone())
} else {
None
}
}
impl EvmFactory<EvmEnv> for HyperliquidEvmFactory {
type Evm<DB: Database, I: Inspector<EthEvmContext<DB>, EthInterpreter>> =
EthEvm<DB, I, L1ReadPrecompiles<EthEvmContext<DB>>>;
EthEvm<DB, I, ReplayPrecompile<EthEvmContext<DB>>>;
type Tx = TxEnv;
type Error<DBError: core::error::Error + Send + Sync + 'static> = EVMError<DBError>;
type HaltReason = HaltReason;
type Context<DB: Database> = EthEvmContext<DB>;
fn create_evm<DB: Database>(&self, db: DB, input: EvmEnv) -> Self::Evm<DB, NoOpInspector> {
let cache = collect_block(self.ingest_dir.clone().unwrap(), input.block_env.number)
.unwrap()
.read_precompile_calls;
let evm = Context::mainnet()
.with_db(db)
.with_cfg(input.cfg_env)
.with_block(input.block_env)
.build_mainnet_with_inspector(NoOpInspector {})
.with_precompiles(L1ReadPrecompiles::new());
.with_precompiles(ReplayPrecompile::new(
EthPrecompiles::default(),
Arc::new(RwLock::new(
cache
.into_iter()
.map(|(address, calls)| (address, HashMap::from_iter(calls.into_iter())))
.collect(),
)),
));
EthEvm::new(evm, false)
}
@ -509,3 +447,7 @@ mod tests {
assert_eq!(evm.tx, Default::default());
}
}
mod precompile_replay;
pub use precompile_replay::ReplayPrecompile;

View File

@ -0,0 +1,100 @@
use alloy_primitives::{Address, Bytes};
use parking_lot::RwLock;
use reth_hyperliquid_types::{ReadPrecompileInput, ReadPrecompileResult};
use reth_revm::{
context::{Cfg, ContextTr},
handler::{EthPrecompiles, PrecompileProvider},
interpreter::{Gas, InstructionResult, InterpreterResult},
precompile::{PrecompileError, PrecompileErrors},
};
use std::{collections::HashMap, sync::Arc};
/// Precompile that replays cached results.
#[derive(Clone)]
pub struct ReplayPrecompile<CTX: ContextTr> {
precompiles: EthPrecompiles<CTX>,
cache: Arc<RwLock<HashMap<Address, HashMap<ReadPrecompileInput, ReadPrecompileResult>>>>,
}
impl<CTX: ContextTr> std::fmt::Debug for ReplayPrecompile<CTX> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReplayPrecompile").finish()
}
}
impl<CTX: ContextTr> ReplayPrecompile<CTX> {
/// Creates a new replay precompile with the given precompiles and cache.
pub fn new(
precompiles: EthPrecompiles<CTX>,
cache: Arc<RwLock<HashMap<Address, HashMap<ReadPrecompileInput, ReadPrecompileResult>>>>,
) -> Self {
Self { precompiles, cache }
}
}
impl<CTX: ContextTr> PrecompileProvider for ReplayPrecompile<CTX> {
type Context = CTX;
type Output = InterpreterResult;
fn set_spec(&mut self, spec: <<Self::Context as ContextTr>::Cfg as Cfg>::Spec) {
self.precompiles.set_spec(spec);
}
fn run(
&mut self,
context: &mut Self::Context,
address: &Address,
bytes: &Bytes,
gas_limit: u64,
) -> Result<Option<Self::Output>, PrecompileErrors> {
let cache = self.cache.read();
if let Some(precompile_calls) = cache.get(address) {
let input = ReadPrecompileInput { input: bytes.clone(), gas_limit };
let mut result = InterpreterResult {
result: InstructionResult::Return,
gas: Gas::new(gas_limit),
output: Bytes::new(),
};
let Some(get) = precompile_calls.get(&input) else {
result.gas.spend_all();
result.result = InstructionResult::PrecompileError;
return Ok(Some(result))
};
return match *get {
ReadPrecompileResult::Ok { gas_used, ref bytes } => {
let underflow = result.gas.record_cost(gas_used);
assert!(underflow, "Gas underflow is not possible");
result.output = bytes.clone();
Ok(Some(result))
}
ReadPrecompileResult::OutOfGas => {
// Use all the gas passed to this precompile
result.gas.spend_all();
result.result = InstructionResult::OutOfGas;
Ok(Some(result))
}
ReadPrecompileResult::Error => {
result.gas.spend_all();
result.result = InstructionResult::PrecompileError;
Ok(Some(result))
}
ReadPrecompileResult::UnexpectedError => panic!("unexpected precompile error"),
};
}
// If no cached result, fall back to normal precompile execution
self.precompiles.run(context, address, bytes, gas_limit)
}
fn contains(&self, address: &Address) -> bool {
self.precompiles.contains(address) || self.cache.read().get(address).is_some()
}
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address> + '_> {
let addresses: Vec<Address> =
self.precompiles.warm_addresses().chain(self.cache.read().keys().cloned()).collect();
Box::new(addresses.into_iter())
}
}

View File

@ -248,7 +248,7 @@ where
ctx: &BuilderContext<Node>,
) -> eyre::Result<(Self::EVM, Self::Executor)> {
let chain_spec = ctx.chain_spec();
let evm_config = EthEvmConfig::new(ctx.chain_spec());
let evm_config = EthEvmConfig::new(ctx.chain_spec()).with_ingest_dir(ctx.ingest_dir());
let strategy_factory = EthExecutionStrategyFactory::new(chain_spec, evm_config.clone());
let executor = BasicBlockExecutorProvider::new(strategy_factory);

View File

@ -74,6 +74,6 @@ where
ctx: &BuilderContext<Node>,
pool: Pool,
) -> eyre::Result<Self::PayloadBuilder> {
self.build(EthEvmConfig::new(ctx.chain_spec()), ctx, pool)
self.build(EthEvmConfig::new(ctx.chain_spec()).with_ingest_dir(ctx.ingest_dir()), ctx, pool)
}
}

View File

@ -332,9 +332,9 @@ impl Hash for TransactionSigned {
impl PartialEq for TransactionSigned {
fn eq(&self, other: &Self) -> bool {
self.signature == other.signature
&& self.transaction == other.transaction
&& self.tx_hash() == other.tx_hash()
self.signature == other.signature &&
self.transaction == other.transaction &&
self.tx_hash() == other.tx_hash()
}
}
@ -582,13 +582,13 @@ impl<'a> arbitrary::Arbitrary<'a> for TransactionSigned {
)
.unwrap();
Ok(Self { transaction, signature, ..Default::default() })
Ok(Self { transaction, signature, hash: Default::default() })
}
}
impl InMemorySize for TransactionSigned {
fn size(&self) -> usize {
let Self { hash: _, signature, transaction, .. } = self;
let Self { hash: _, signature, transaction } = self;
self.tx_hash().size() + signature.size() + transaction.size()
}
}
@ -617,26 +617,26 @@ impl Decodable2718 for TransactionSigned {
TxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
TxType::Eip2930 => {
let (tx, signature) = TxEip2930::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Eip2930(tx), signature, ..Default::default() })
Ok(Self { transaction: Transaction::Eip2930(tx), signature, hash: Default::default() })
}
TxType::Eip1559 => {
let (tx, signature) = TxEip1559::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Eip1559(tx), signature, ..Default::default() })
Ok(Self { transaction: Transaction::Eip1559(tx), signature, hash: Default::default() })
}
TxType::Eip4844 => {
let (tx, signature) = TxEip4844::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Eip4844(tx), signature, ..Default::default() })
Ok(Self { transaction: Transaction::Eip4844(tx), signature, hash: Default::default() })
}
TxType::Eip7702 => {
let (tx, signature) = TxEip7702::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Eip7702(tx), signature, ..Default::default() })
Ok(Self { transaction: Transaction::Eip7702(tx), signature, hash: Default::default() })
}
}
}
fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
let (tx, signature) = TxLegacy::rlp_decode_with_signature(buf)?;
Ok(Self { transaction: Transaction::Legacy(tx), signature, ..Default::default() })
Ok(Self { transaction: Transaction::Legacy(tx), signature, hash: Default::default() })
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "reth-hyperliquid-types"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
alloy-primitives.workspace = true
serde.workspace = true
[dev-dependencies]
clap.workspace = true
reth-cli-commands.workspace = true

View File

@ -0,0 +1,16 @@
use alloy_primitives::Bytes;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
pub struct ReadPrecompileInput {
pub input: Bytes,
pub gas_limit: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReadPrecompileResult {
Ok { gas_used: u64, bytes: Bytes },
OutOfGas,
Error,
UnexpectedError,
}

View File

@ -37,7 +37,7 @@ use reth_provider::{
use reth_tasks::TaskExecutor;
use reth_transaction_pool::{PoolConfig, PoolTransaction, TransactionPool};
use secp256k1::SecretKey;
use std::sync::Arc;
use std::{path::PathBuf, sync::Arc};
use tracing::{info, trace, warn};
pub mod add_ons;
@ -750,6 +750,10 @@ impl<Node: FullNodeTypes> BuilderContext<Node> {
{
network_builder.build(self.provider.clone())
}
pub fn ingest_dir(&self) -> PathBuf {
self.config().ingest_dir.clone().expect("ingest dir not set")
}
}
impl<Node: FullNodeTypes<Types: NodeTypes<ChainSpec: Hardforks>>> BuilderContext<Node> {

View File

@ -145,6 +145,9 @@ pub struct NodeConfig<ChainSpec> {
/// All engine related arguments
pub engine: EngineArgs,
/// The ingest directory for the node.
pub ingest_dir: Option<PathBuf>,
}
impl NodeConfig<ChainSpec> {
@ -174,6 +177,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
pruning: PruningArgs::default(),
datadir: DatadirArgs::default(),
engine: EngineArgs::default(),
ingest_dir: None,
}
}
@ -465,6 +469,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
dev: self.dev,
pruning: self.pruning,
engine: self.engine,
ingest_dir: self.ingest_dir,
}
}
}
@ -492,6 +497,7 @@ impl<ChainSpec> Clone for NodeConfig<ChainSpec> {
pruning: self.pruning.clone(),
datadir: self.datadir.clone(),
engine: self.engine.clone(),
ingest_dir: self.ingest_dir.clone(),
}
}
}

View File

@ -20,14 +20,14 @@ pub trait FullSignedTx: SignedTransaction + MaybeCompact + MaybeSerdeBincodeComp
impl<T> FullSignedTx for T where T: SignedTransaction + MaybeCompact + MaybeSerdeBincodeCompat {}
/// Hyperliquid system transaction from address.
pub const HL_SYSTEM_TX_FROM_ADDR: Address = address!("2222222222222222222222222222222222222222");
pub const NATIVE_TOKEN_SYSTEM_ADDRESS: Address = address!("2222222222222222222222222222222222222222");
/// Check if the transaction is impersonated.
/// Signature part is introduced in block_ingest, while the gas_price is trait of hyperliquid system transactions.
pub fn is_impersonated_tx(signature: &Signature, gas_price: Option<u128>) -> Option<Address> {
if signature.r() == U256::from(1) && signature.v() == true && gas_price == Some(0u128) {
if signature.s() == U256::from(1) {
Some(HL_SYSTEM_TX_FROM_ADDR)
Some(NATIVE_TOKEN_SYSTEM_ADDRESS)
} else {
let s = signature.s().reduce_mod(U256::from(U160::MAX).add(U256::from(1)));
let s = U160::from(s);

View File

@ -159,7 +159,8 @@ impl<DB: EvmStateProvider> DatabaseRef for StateProviderDatabase<DB> {
///
/// Returns `Ok` with the block hash if found, or the default hash otherwise.
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
if number >= 270000 {
const NON_PLACEHOLDER_BLOCK_HASH_HEIGHT: u64 = 243_538;
if number >= NON_PLACEHOLDER_BLOCK_HASH_HEIGHT {
// Get the block hash or default hash with an attempt to convert U256 block number to u64
Ok(self.0.block_hash(number)?.unwrap_or_default())
} else {

View File

@ -72,7 +72,7 @@ where
request: TransactionRequest,
) -> Result<TransactionSigned, Self::Error> {
let Ok(tx) = request.build_typed_tx() else {
return Err(EthApiError::TransactionConversionError);
return Err(EthApiError::TransactionConversionError)
};
// Create an empty signature for the transaction.

View File

@ -3067,7 +3067,7 @@ impl<TX: DbTxMut + DbTx + 'static, N: NodeTypesForProvider + 'static> BlockWrite
return Ok(())
}
let first_number: u64 = blocks.first().unwrap().number();
let first_number = blocks.first().unwrap().number();
let last = blocks.last().unwrap();
let last_block_number = last.number();