Skip to content

Commit

Permalink
fix: make manual mining produce non-empty blocks (#459)
Browse files Browse the repository at this point in the history
* slightly reduce lock-related boilerplate

* make sure system cotnracts are immutable

* move `SystemContracts` to base node to minimize locking

* make manual mining take txs from mempool

* feat: add `anvil_mine_detailed` (#460)

* fix `v` calculation for legacy txs

* add `anvil_mine_detailed`
  • Loading branch information
itegulov authored Dec 3, 2024
1 parent 6ef2c9b commit 7587a7a
Show file tree
Hide file tree
Showing 17 changed files with 417 additions and 227 deletions.
1 change: 1 addition & 0 deletions SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The `status` options are:

| Namespace | API | <div style="width:130px">Status</div> | Description |
| --- | --- | --- | --- |
| `ANVIL` | `anvil_mine_detailed` | `SUPPORTED` | Mines a single block in the same way as `evm_mine` but returns extra fields |
| `ANVIL` | `anvil_setRpcUrl` | `SUPPORTED` | Sets the fork RPC url. Assumes the underlying chain is the same as before |
| `ANVIL` | `anvil_setNextBlockBaseFeePerGas` | `SUPPORTED` | Sets the base fee of the next block |
| `ANVIL` | `anvil_dropTransaction` | `SUPPORTED` | Removes a transaction from the pool |
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests-rust/Cargo.lock

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

2 changes: 1 addition & 1 deletion e2e-tests-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ categories = ["cryptography"]
publish = false

[dependencies]
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218" }
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "c43bba1a6c5e744afb975b261cba6e964d6a58c6" }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types"] }
anyhow = "1.0"
fs2 = "0.4.3"
Expand Down
25 changes: 25 additions & 0 deletions e2e-tests-rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use alloy::network::Network;
use alloy::primitives::{Address, TxHash};
use alloy::providers::{Provider, ProviderCall};
use alloy::rpc::client::NoParams;
use alloy::serde::WithOtherFields;
use alloy::transports::Transport;
use alloy_zksync::network::Zksync;

Expand Down Expand Up @@ -41,6 +43,29 @@ where
.request("anvil_removePoolTransactions", (address,))
.into()
}

fn mine(
&self,
num_blocks: Option<u64>,
interval: Option<u64>,
) -> ProviderCall<T, (Option<u64>, Option<u64>), ()> {
self.client()
.request("anvil_mine", (num_blocks, interval))
.into()
}

fn mine_detailed(
&self,
) -> ProviderCall<
T,
NoParams,
alloy::rpc::types::Block<
WithOtherFields<<Zksync as Network>::TransactionResponse>,
<Zksync as Network>::HeaderResponse,
>,
> {
self.client().request_noparams("anvil_mine_detailed").into()
}
}

impl<P, T> EraTestNodeApiProvider<T> for P
Expand Down
72 changes: 72 additions & 0 deletions e2e-tests-rust/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,15 @@ async fn no_sealing_timeout() -> anyhow::Result<()> {
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx = provider.send_transaction(tx).await?.register().await?;
let tx_hash = pending_tx.tx_hash().clone();
let finalization_result = tokio::time::timeout(Duration::from_secs(3), pending_tx).await;
assert!(finalization_result.is_err());

// Mine a block manually and assert that the transaction is finalized now
provider.mine(None, None).await?;
let receipt = provider.get_transaction_receipt(tx_hash).await?.unwrap();
assert!(receipt.status());

Ok(())
}

Expand Down Expand Up @@ -275,3 +281,69 @@ async fn remove_pool_transactions() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn manual_mining_two_txs_in_one_block() -> anyhow::Result<()> {
// Test that we can submit two transaction and then manually mine one block that contains both
// transactions in it.
let provider = init(|node| node.no_mine()).await?;

let tx0 = TransactionRequest::default()
.with_from(RICH_WALLET0)
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx0 = provider.send_transaction(tx0).await?.register().await?;
let tx1 = TransactionRequest::default()
.with_from(RICH_WALLET1)
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx1 = provider.send_transaction(tx1).await?.register().await?;

// Mine a block manually and assert that both transactions are finalized now
provider.mine(None, None).await?;
let receipt0 = provider
.get_transaction_receipt(pending_tx0.await?)
.await?
.unwrap();
assert!(receipt0.status());
let receipt1 = provider
.get_transaction_receipt(pending_tx1.await?)
.await?
.unwrap();
assert!(receipt1.status());

assert_eq!(receipt0.block_hash(), receipt1.block_hash());
assert_eq!(receipt0.block_number(), receipt1.block_number());

Ok(())
}

#[tokio::test]
async fn detailed_mining_success() -> anyhow::Result<()> {
// Test that we can detailed mining on a successful transaction and get output from it.
let provider = init(|node| node.no_mine()).await?;

let tx = TransactionRequest::default()
.with_from(RICH_WALLET0)
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
provider.send_transaction(tx).await?.register().await?;

// Mine a block manually and assert that it has our transaction with extra fields
let block = provider.mine_detailed().await?;
assert_eq!(block.transactions.len(), 1);
let actual_tx = block
.transactions
.clone()
.into_transactions()
.next()
.unwrap();

assert_eq!(
actual_tx.other.get("output").and_then(|x| x.as_str()),
Some("0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000")
);
assert!(actual_tx.other.get("revertReason").is_none());

Ok(())
}
6 changes: 2 additions & 4 deletions src/deps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ impl InMemoryStorage {
system_contracts_options: &crate::system_contracts::Options,
use_evm_emulator: bool,
) -> Self {
let contracts = crate::system_contracts::get_deployed_contracts(
system_contracts_options,
use_evm_emulator,
);
let contracts =
system_contracts::get_deployed_contracts(system_contracts_options, use_evm_emulator);

let system_context_init_log = get_system_context_init_logs(chain_id);

Expand Down
12 changes: 12 additions & 0 deletions src/deps/system_contracts.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::system_contracts::Options;
use once_cell::sync::Lazy;
use serde_json::Value;
use zksync_types::system_contracts::get_system_smart_contracts;
use zksync_types::{
block::DeployedContract, ACCOUNT_CODE_STORAGE_ADDRESS, BOOTLOADER_ADDRESS,
BOOTLOADER_UTILITIES_ADDRESS, CODE_ORACLE_ADDRESS, COMPRESSOR_ADDRESS,
Expand Down Expand Up @@ -190,3 +192,13 @@ pub static COMPILED_IN_SYSTEM_CONTRACTS: Lazy<Vec<DeployedContract>> = Lazy::new
deployed_system_contracts.extend(empty_system_contracts);
deployed_system_contracts
});

pub fn get_deployed_contracts(
options: &Options,
use_evm_emulator: bool,
) -> Vec<zksync_types::block::DeployedContract> {
match options {
Options::BuiltIn | Options::BuiltInWithoutSecurity => COMPILED_IN_SYSTEM_CONTRACTS.clone(),
Options::Local => get_system_smart_contracts(use_evm_emulator),
}
}
29 changes: 26 additions & 3 deletions src/namespaces/anvil.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use jsonrpc_derive::rpc;
use zksync_types::{Address, H256, U256, U64};

use super::{ResetRequest, RpcResult};
use crate::utils::Numeric;
use jsonrpc_derive::rpc;
use serde::{Deserialize, Serialize};
use zksync_types::api::{Block, Transaction};
use zksync_types::web3::Bytes;
use zksync_types::{Address, H256, U256, U64};

#[rpc]
pub trait AnvilNamespaceT {
/// Mines a single block in the same way as `evm_mine` but returns extra fields.
///
///
/// # Returns
/// Freshly mined block's representation along with extra fields.
#[rpc(name = "anvil_mine_detailed")]
fn mine_detailed(&self) -> RpcResult<Block<DetailedTransaction>>;

/// Sets the fork RPC url. Assumes the underlying chain is the same as before.
///
/// # Arguments
Expand Down Expand Up @@ -278,3 +288,16 @@ pub trait AnvilNamespaceT {
#[rpc(name = "anvil_setStorageAt")]
fn set_storage_at(&self, address: Address, slot: U256, value: U256) -> RpcResult<bool>;
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct DetailedTransaction {
#[serde(flatten)]
pub inner: Transaction,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub output: Option<Bytes>,
#[serde(rename = "revertReason")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub revert_reason: Option<String>,
}
2 changes: 1 addition & 1 deletion src/namespaces/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod net;
mod web3;
mod zks;

pub use anvil::AnvilNamespaceT;
pub use anvil::{AnvilNamespaceT, DetailedTransaction};
pub use config::ConfigurationApiNamespaceT;
pub use debug::DebugNamespaceT;
pub use eth::EthNamespaceT;
Expand Down
11 changes: 11 additions & 0 deletions src/node/anvil.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use zksync_types::api::Block;
use zksync_types::{Address, H256, U256, U64};
use zksync_web3_decl::error::Web3Error;

use crate::namespaces::DetailedTransaction;
use crate::utils::Numeric;
use crate::{
fork::ForkSource,
Expand All @@ -12,6 +14,15 @@ use crate::{
impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNamespaceT
for InMemoryNode<S>
{
fn mine_detailed(&self) -> RpcResult<Block<DetailedTransaction>> {
self.mine_detailed()
.map_err(|err| {
tracing::error!("failed mining with detailed view: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn set_rpc_url(&self, url: String) -> RpcResult<()> {
self.set_rpc_url(url)
.map_err(|err| {
Expand Down
2 changes: 1 addition & 1 deletion src/node/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> DebugNames
let only_top = options.is_some_and(|o| o.tracer_config.only_top_call);
let inner = self.get_inner().clone();
let time = self.time.clone();
let system_contracts = self.system_contracts.contracts_for_l2_call().clone();
Box::pin(async move {
if block.is_some() && !matches!(block, Some(BlockId::Number(BlockNumber::Latest))) {
return Err(jsonrpc_core::Error::invalid_params(
Expand All @@ -158,7 +159,6 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> DebugNames
)))
})?;

let system_contracts = inner.system_contracts.contracts_for_l2_call();
let allow_no_target = system_contracts.evm_emulator.is_some();
let mut l2_tx = L2Tx::from_request(request.into(), MAX_TX_SIZE, allow_no_target)
.map_err(|err| into_jsrpc_error(Web3Error::SerializationError(err)))?;
Expand Down
16 changes: 12 additions & 4 deletions src/node/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use anyhow::Context as _;
use colored::Colorize;
use futures::FutureExt;
use itertools::Itertools;
use zksync_multivm::interface::ExecutionResult;
use zksync_multivm::interface::{ExecutionResult, TxExecutionMode};
use zksync_multivm::vm_latest::constants::ETH_CALL_GAS_LIMIT;
use zksync_types::{
api::{Block, BlockIdVariant, BlockNumber, TransactionVariant},
Expand Down Expand Up @@ -41,7 +41,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
&self,
req: zksync_types::transaction_request::CallRequest,
) -> Result<Bytes, Web3Error> {
let system_contracts = self.system_contracts_for_l2_call()?;
let system_contracts = self.system_contracts.contracts_for_l2_call().clone();
let allow_no_target = system_contracts.evm_emulator.is_some();

let mut tx = L2Tx::from_request(req.into(), MAX_TX_SIZE, allow_no_target)?;
Expand Down Expand Up @@ -89,7 +89,11 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
.chain_id;

let (tx_req, hash) = TransactionRequest::from_bytes(&tx_bytes.0, chain_id)?;
let system_contracts = self.system_contracts_for_tx(tx_req.from.unwrap_or_default())?;
// Impersonation does not matter in this context so we assume the tx is not impersonated:
// system contracts here are fetched solely to check for EVM emulator.
let system_contracts = self
.system_contracts
.contracts(TxExecutionMode::VerifyExecute, false);
let allow_no_target = system_contracts.evm_emulator.is_some();
let mut l2_tx = L2Tx::from_request(tx_req, MAX_TX_SIZE, allow_no_target)?;

Expand Down Expand Up @@ -154,7 +158,11 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> InMemoryNo
27,
))?;

let system_contracts = self.system_contracts_for_tx(tx_req.from.unwrap_or_default())?;
// Impersonation does not matter in this context so we assume the tx is not impersonated:
// system contracts here are fetched solely to check for EVM emulator.
let system_contracts = self
.system_contracts
.contracts(TxExecutionMode::VerifyExecute, false);
let allow_no_target = system_contracts.evm_emulator.is_some();
let mut l2_tx: L2Tx = L2Tx::from_request(tx_req, MAX_TX_SIZE, allow_no_target)?;

Expand Down
1 change: 1 addition & 0 deletions src/node/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> EvmNamespa
tracing::error!("failed mining block: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.map(|_| "0x0".to_string())
.into_boxed_future()
}

Expand Down
Loading

0 comments on commit 7587a7a

Please sign in to comment.