Skip to content

Commit

Permalink
Implementing Otterscan support (#5414)
Browse files Browse the repository at this point in the history
* Implementing Otterscan support

Adds anvil support for Otterscan's custom RPC endpoints.

This is still a work in progress, as I have two endpoints to implement
still, but they should be **easy** (famous last words) compared to a lot
of the others, so I'm opening this ahead of time to gather feedback.

This was a bit tricky for a couple of reasons:
* Otterscan's endpoints are inherently hard to compute with existing
  data (otherwise they wouldn't be needed in the first place). They
  solve things that the original RPC spec didn't account for (most
  notably, listing historical transactions by address). For anvil, this
  is mostly a non-issue, as we can choose to traverse all the blocks &
  traces in-memory. Would be interesting to test this on an node with
  a heavy data-set though;
* After having gone through the spec, it seems to not be as well
  though-out as it could, and in some cases outdated compared to
  their code. Some of the design decisions behind it are a
  bit awkward to implement (see otterscan/otterscan#1081).

* comments

* code review

* code review

* code review

* code review

* code review

* Some fixes

* code review

* Update anvil/src/eth/otterscan.rs

Co-authored-by: evalir <[email protected]>

* code review

* code review

* ots_getBlockDetailsByHash

* search endpoints tests

* tests for ots_getBlockTransactions

* tests for ots_getBlockDetails

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* code review

* Update Cargo.toml

* code review

* clippy

* code review

* code review

---------

Co-authored-by: evalir <[email protected]>
  • Loading branch information
naps62 and Evalir authored Aug 8, 2023
1 parent 1a110a5 commit e4ac420
Show file tree
Hide file tree
Showing 11 changed files with 1,207 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions anvil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ serde = { version = "1", features = ["derive"] }
thiserror = "1"
yansi = "0.5"
tempfile = "3"
itertools = "0.10"

# cli
clap = { version = "4", features = ["derive", "env", "wrap_help"], optional = true }
Expand Down
87 changes: 87 additions & 0 deletions anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,93 @@ pub enum EthRequest {
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content)
#[cfg_attr(feature = "serde", serde(rename = "txpool_content", with = "empty_params"))]
TxPoolContent(()),

/// Otterscan's `ots_getApiLevel` endpoint
/// Otterscan currently requires this endpoint, even though it's not part of the ots_*
/// https://github.com/otterscan/otterscan/blob/071d8c55202badf01804f6f8d53ef9311d4a9e47/src/useProvider.ts#L71
/// Related upstream issue: https://github.com/otterscan/otterscan/issues/1081
#[cfg_attr(feature = "serde", serde(rename = "erigon_getHeaderByNumber"))]
ErigonGetHeaderByNumber(
#[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number_seq"))]
BlockNumber,
),

/// Otterscan's `ots_getApiLevel` endpoint
/// Used as a simple API versioning scheme for the ots_* namespace
#[cfg_attr(feature = "serde", serde(rename = "ots_getApiLevel", with = "empty_params"))]
OtsGetApiLevel(()),

/// Otterscan's `ots_getInternalOperations` endpoint
/// Traces internal ETH transfers, contracts creation (CREATE/CREATE2) and self-destructs for a
/// certain transaction.
#[cfg_attr(feature = "serde", serde(rename = "ots_getInternalOperations", with = "sequence"))]
OtsGetInternalOperations(H256),

/// Otterscan's `ots_hasCode` endpoint
/// Check if an ETH address contains code at a certain block number.
#[cfg_attr(feature = "serde", serde(rename = "ots_hasCode"))]
OtsHasCode(
Address,
#[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number", default))]
BlockNumber,
),

/// Otterscan's `ots_traceTransaction` endpoint
/// Trace a transaction and generate a trace call tree.
#[cfg_attr(feature = "serde", serde(rename = "ots_traceTransaction", with = "sequence"))]
OtsTraceTransaction(H256),

/// Otterscan's `ots_getTransactionError` endpoint
/// Given a transaction hash, returns its raw revert reason.
#[cfg_attr(feature = "serde", serde(rename = "ots_getTransactionError", with = "sequence"))]
OtsGetTransactionError(H256),

/// Otterscan's `ots_getBlockDetails` endpoint
/// Given a block number, return its data. Similar to the standard eth_getBlockByNumber/Hash
/// method, but can be optimized by excluding unnecessary data such as transactions and
/// logBloom
#[cfg_attr(feature = "serde", serde(rename = "ots_getBlockDetails"))]
OtsGetBlockDetails(
#[cfg_attr(feature = "serde", serde(deserialize_with = "lenient_block_number_seq"))]
BlockNumber,
),

/// Otterscan's `ots_getBlockDetails` endpoint
/// Same as `ots_getBlockDetails`, but receiving a block hash instead of number
#[cfg_attr(feature = "serde", serde(rename = "ots_getBlockDetailsByHash", with = "sequence"))]
OtsGetBlockDetailsByHash(H256),

/// Otterscan's `ots_getBlockTransactions` endpoint
/// Gets paginated transaction data for a certain block. Return data is similar to
/// eth_getBlockBy* + eth_getTransactionReceipt.
#[cfg_attr(feature = "serde", serde(rename = "ots_getBlockTransactions"))]
OtsGetBlockTransactions(u64, usize, usize),

/// Otterscan's `ots_searchTransactionsBefore` endpoint
/// Address history navigation. searches backwards from certain point in time.
#[cfg_attr(feature = "serde", serde(rename = "ots_searchTransactionsBefore"))]
OtsSearchTransactionsBefore(Address, u64, usize),

/// Otterscan's `ots_searchTransactionsAfter` endpoint
/// Address history navigation. searches forward from certain point in time.
#[cfg_attr(feature = "serde", serde(rename = "ots_searchTransactionsAfter"))]
OtsSearchTransactionsAfter(Address, u64, usize),

/// Otterscan's `ots_getTransactionBySenderAndNonce` endpoint
/// Given a sender address and a nonce, returns the tx hash or null if not found. It returns
/// only the tx hash on success, you can use the standard eth_getTransactionByHash after that
/// to get the full transaction data.
#[cfg_attr(feature = "serde", serde(rename = "ots_getTransactionBySenderAndNonce",))]
OtsGetTransactionBySenderAndNonce(
Address,
#[cfg_attr(feature = "serde", serde(deserialize_with = "deserialize_number"))] U256,
),

/// Otterscan's `ots_getTransactionBySenderAndNonce` endpoint
/// Given an ETH contract address, returns the tx hash and the direct address who created the
/// contract.
#[cfg_attr(feature = "serde", serde(rename = "ots_getContractCreator", with = "sequence"))]
OtsGetContractCreator(Address),
}

/// Represents ethereum JSON-RPC API
Expand Down
37 changes: 36 additions & 1 deletion anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub struct EthApi {
pool: Arc<Pool>,
/// Holds all blockchain related data
/// In-Memory only for now
backend: Arc<backend::mem::Backend>,
pub(super) backend: Arc<backend::mem::Backend>,
/// Whether this node is mining
is_mining: bool,
/// available signers
Expand Down Expand Up @@ -356,6 +356,41 @@ impl EthApi {
EthRequest::TxPoolStatus(_) => self.txpool_status().await.to_rpc_result(),
EthRequest::TxPoolInspect(_) => self.txpool_inspect().await.to_rpc_result(),
EthRequest::TxPoolContent(_) => self.txpool_content().await.to_rpc_result(),
EthRequest::ErigonGetHeaderByNumber(num) => {
self.erigon_get_header_by_number(num).await.to_rpc_result()
}
EthRequest::OtsGetApiLevel(_) => self.ots_get_api_level().await.to_rpc_result(),
EthRequest::OtsGetInternalOperations(hash) => {
self.ots_get_internal_operations(hash).await.to_rpc_result()
}
EthRequest::OtsHasCode(addr, num) => self.ots_has_code(addr, num).await.to_rpc_result(),
EthRequest::OtsTraceTransaction(hash) => {
self.ots_trace_transaction(hash).await.to_rpc_result()
}
EthRequest::OtsGetTransactionError(hash) => {
self.ots_get_transaction_error(hash).await.to_rpc_result()
}
EthRequest::OtsGetBlockDetails(num) => {
self.ots_get_block_details(num).await.to_rpc_result()
}
EthRequest::OtsGetBlockDetailsByHash(hash) => {
self.ots_get_block_details_by_hash(hash).await.to_rpc_result()
}
EthRequest::OtsGetBlockTransactions(num, page, page_size) => {
self.ots_get_block_transactions(num, page, page_size).await.to_rpc_result()
}
EthRequest::OtsSearchTransactionsBefore(address, num, page_size) => {
self.ots_search_transactions_before(address, num, page_size).await.to_rpc_result()
}
EthRequest::OtsSearchTransactionsAfter(address, num, page_size) => {
self.ots_search_transactions_after(address, num, page_size).await.to_rpc_result()
}
EthRequest::OtsGetTransactionBySenderAndNonce(address, nonce) => {
self.ots_get_transaction_by_sender_and_nonce(address, nonce).await.to_rpc_result()
}
EthRequest::OtsGetContractCreator(address) => {
self.ots_get_contract_creator(address).await.to_rpc_result()
}
}
}

Expand Down
20 changes: 15 additions & 5 deletions anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,8 +1355,18 @@ impl Backend {
Some(self.convert_block(block))
}

pub(crate) async fn mined_transactions_by_block_number(
&self,
number: BlockNumber,
) -> Option<Vec<Transaction>> {
if let Some(block) = self.get_block(number) {
return self.mined_transactions_in_block(&block)
}
None
}

/// Returns all transactions given a block
fn mined_transactions_in_block(&self, block: &Block) -> Option<Vec<Transaction>> {
pub(crate) fn mined_transactions_in_block(&self, block: &Block) -> Option<Vec<Transaction>> {
let mut transactions = Vec::with_capacity(block.transactions.len());
let base_fee = block.header.base_fee_per_gas;
let storage = self.blockchain.storage.read();
Expand Down Expand Up @@ -1443,7 +1453,7 @@ impl Backend {
self.blockchain.get_block_by_hash(&hash)
}

fn mined_block_by_number(&self, number: BlockNumber) -> Option<EthersBlock<TxHash>> {
pub fn mined_block_by_number(&self, number: BlockNumber) -> Option<EthersBlock<TxHash>> {
Some(self.convert_block(self.get_block(number)?))
}

Expand Down Expand Up @@ -1752,12 +1762,12 @@ impl Backend {
}

/// Returns the traces for the given transaction
fn mined_parity_trace_transaction(&self, hash: H256) -> Option<Vec<Trace>> {
pub(crate) fn mined_parity_trace_transaction(&self, hash: H256) -> Option<Vec<Trace>> {
self.blockchain.storage.read().transactions.get(&hash).map(|tx| tx.parity_traces())
}

/// Returns the traces for the given transaction
fn mined_parity_trace_block(&self, block: u64) -> Option<Vec<Trace>> {
/// Returns the traces for the given block
pub(crate) fn mined_parity_trace_block(&self, block: u64) -> Option<Vec<Trace>> {
let block = self.get_block(block)?;
let mut traces = vec![];
let storage = self.blockchain.storage.read();
Expand Down
1 change: 1 addition & 0 deletions anvil/src/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod api;
pub mod otterscan;
pub use api::EthApi;

pub mod backend;
Expand Down
Loading

0 comments on commit e4ac420

Please sign in to comment.