Skip to content

Commit

Permalink
feat: add anvil_{get,set}Automine/anvil_setIntervalMining (#446)
Browse files Browse the repository at this point in the history
* add `anvil_{get,set}Automine`/`anvil_setIntervalMining`

* add an e2e test
  • Loading branch information
itegulov authored Nov 28, 2024
1 parent bf55eb9 commit 5c0d204
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 55 deletions.
4 changes: 2 additions & 2 deletions e2e-tests-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ categories = ["cryptography"]
publish = false

[dependencies]
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218" }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types"] }
anyhow = "1.0"
fs2 = "0.4.3"
tokio = { version = "1", features = ["time", "rt", "process"] }

[dev-dependencies]
alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "e0e54a5ac9e24c1c32c7a8783bbf3e34dde2e218" }
alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types"] }

[workspace] # ignore higher-level workspace
31 changes: 31 additions & 0 deletions e2e-tests-rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
use alloy::providers::{Provider, ProviderCall};
use alloy::rpc::client::NoParams;
use alloy::transports::Transport;
use alloy_zksync::network::Zksync;

pub mod utils;

pub trait EraTestNodeApiProvider<T>: Provider<T, Zksync>
where
T: Transport + Clone,
{
fn get_auto_mine(&self) -> ProviderCall<T, NoParams, bool> {
self.client().request_noparams("anvil_getAutomine").into()
}

fn set_auto_mine(&self, enable: bool) -> ProviderCall<T, (bool,), ()> {
self.client().request("anvil_setAutomine", (enable,)).into()
}

fn set_interval_mining(&self, seconds: u64) -> ProviderCall<T, (u64,), ()> {
self.client()
.request("anvil_setIntervalMining", (seconds,))
.into()
}
}

impl<P, T> EraTestNodeApiProvider<T> for P
where
T: Transport + Clone,
P: Provider<T, Zksync>,
{
}
112 changes: 85 additions & 27 deletions e2e-tests-rust/tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use alloy_zksync::network::Zksync;
use alloy_zksync::node_bindings::EraTestNode;
use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt};
use era_test_node_e2e_tests::utils::LockedPort;
use era_test_node_e2e_tests::EraTestNodeApiProvider;
use std::time::Duration;

async fn init(
Expand All @@ -32,45 +33,28 @@ async fn init(
Ok(provider)
}

#[tokio::test]
async fn interval_sealing_finalization() -> anyhow::Result<()> {
// Test that we can submit a transaction and wait for it to finalize when era-test-node is
// operating in interval sealing mode.
let provider = init(|node| node.block_time(1)).await?;

let tx = TransactionRequest::default()
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
assert!(receipt.status());

Ok(())
}

#[tokio::test]
async fn interval_sealing_multiple_txs() -> anyhow::Result<()> {
// Test that we can submit two transactions and wait for them to finalize in the same block when
// era-test-node is operating in interval sealing mode. 3 seconds should be long enough for
// the entire flow to execute before the first block is produced.
let provider = init(|node| node.block_time(3)).await?;
const RICH_WALLET0: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
const RICH_WALLET1: Address = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
const TARGET: Address = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
const RICH_WALLET0: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
const RICH_WALLET1: Address = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");

async fn test_finalize_two_txs_in_the_same_block(
provider: impl Provider<Http<reqwest::Client>, Zksync> + WalletProvider<Zksync> + Clone + 'static,
target: Address,
) -> anyhow::Result<()> {
async fn submit_tx(
provider: impl Provider<Http<reqwest::Client>, Zksync> + WalletProvider<Zksync> + Clone,
rich_wallet: Address,
target: Address,
) -> Result<PendingTransaction, PendingTransactionError> {
let tx = TransactionRequest::default()
.with_from(rich_wallet)
.with_to(TARGET)
.with_to(target)
.with_value(U256::from(100));
provider.send_transaction(tx).await?.register().await
}

// Submit two txs at the same time
let handle0 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET0));
let handle1 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET1));
let handle0 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET0, target));
let handle1 = tokio::spawn(submit_tx(provider.clone(), RICH_WALLET1, target));

// Wait until both are finalized
let (pending_tx0, pending_tx1) = tokio::join!(handle0, handle1);
Expand Down Expand Up @@ -103,6 +87,37 @@ async fn interval_sealing_multiple_txs() -> anyhow::Result<()> {
Ok(())
}

#[tokio::test]
async fn interval_sealing_finalization() -> anyhow::Result<()> {
// Test that we can submit a transaction and wait for it to finalize when era-test-node is
// operating in interval sealing mode.
let provider = init(|node| node.block_time(1)).await?;

let tx = TransactionRequest::default()
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
assert!(receipt.status());

Ok(())
}

#[tokio::test]
async fn interval_sealing_multiple_txs() -> anyhow::Result<()> {
// Test that we can submit two transactions and wait for them to finalize in the same block when
// era-test-node is operating in interval sealing mode. 3 seconds should be long enough for
// the entire flow to execute before the first block is produced.
let provider = init(|node| node.block_time(3)).await?;

test_finalize_two_txs_in_the_same_block(
provider,
address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"),
)
.await?;

Ok(())
}

#[tokio::test]
async fn no_sealing_timeout() -> anyhow::Result<()> {
// Test that we can submit a transaction and timeout while waiting for it to finalize when
Expand All @@ -118,3 +133,46 @@ async fn no_sealing_timeout() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn dynamic_sealing_mode() -> anyhow::Result<()> {
// Test that we can successfully switch between different sealing modes
let provider = init(|node| node.no_mine()).await?;
assert_eq!(provider.get_auto_mine().await?, false);

// Enable immediate block sealing
provider.set_auto_mine(true).await?;
assert_eq!(provider.get_auto_mine().await?, true);

// Check that we can finalize transactions now
let tx = TransactionRequest::default()
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let receipt = provider.send_transaction(tx).await?.get_receipt().await?;
assert!(receipt.status());

// Enable interval block sealing
provider.set_interval_mining(3).await?;
assert_eq!(provider.get_auto_mine().await?, false);

// Check that we can finalize two txs in the same block now
test_finalize_two_txs_in_the_same_block(
provider.clone(),
address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"),
)
.await?;

// Disable block sealing entirely
provider.set_auto_mine(false).await?;
assert_eq!(provider.get_auto_mine().await?, false);

// Check that transactions do not get finalized now
let tx = TransactionRequest::default()
.with_to(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"))
.with_value(U256::from(100));
let pending_tx = provider.send_transaction(tx).await?.register().await?;
let finalization_result = tokio::time::timeout(Duration::from_secs(3), pending_tx).await;
assert!(finalization_result.is_err());

Ok(())
}
21 changes: 13 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ use crate::namespaces::{
EthTestNodeNamespaceT, EvmNamespaceT, HardhatNamespaceT, NetNamespaceT, Web3NamespaceT,
ZksNamespaceT,
};
use crate::node::{BlockProducer, BlockSealer, ImpersonationManager, TimestampManager, TxPool};
use crate::node::{
BlockProducer, BlockSealer, BlockSealerMode, ImpersonationManager, TimestampManager, TxPool,
};
use crate::system_contracts::SystemContracts;

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -260,13 +262,23 @@ async fn main() -> anyhow::Result<()> {
let time = TimestampManager::default();
let impersonation = ImpersonationManager::default();
let pool = TxPool::new(impersonation.clone());
let sealing_mode = if config.no_mining {
BlockSealerMode::noop()
} else if let Some(block_time) = config.block_time {
BlockSealerMode::fixed_time(config.max_transactions, block_time)
} else {
BlockSealerMode::immediate(config.max_transactions)
};
let block_sealer = BlockSealer::new(sealing_mode);

let node: InMemoryNode<HttpForkSource> = InMemoryNode::new(
fork_details,
Some(observability),
&config,
time.clone(),
impersonation,
pool.clone(),
block_sealer.clone(),
);

if let Some(ref bytecodes_dir) = config.override_bytecodes_dir {
Expand Down Expand Up @@ -307,13 +319,6 @@ async fn main() -> anyhow::Result<()> {
}))
.await;

let block_sealer = if config.no_mining {
BlockSealer::noop()
} else if let Some(block_time) = config.block_time {
BlockSealer::fixed_time(config.max_transactions, block_time)
} else {
BlockSealer::immediate(config.max_transactions)
};
let system_contracts =
SystemContracts::from_options(&config.system_contracts_options, config.use_evm_emulator);
let block_producer_handle = tokio::task::spawn(BlockProducer::new(
Expand Down
26 changes: 25 additions & 1 deletion src/namespaces/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,37 @@ use crate::utils::Numeric;

#[rpc]
pub trait AnvilNamespaceT {
/// Gets node's auto mining status.
///
/// # Returns
/// `true` if auto mining is enabled, `false` otherwise
#[rpc(name = "anvil_getAutomine")]
fn get_auto_mine(&self) -> RpcResult<bool>;

/// Enables or disables, based on the single boolean argument, the automatic mining of new
/// blocks with each new transaction submitted to the network.
///
/// # Arguments
///
/// * `enable` - if `true` automatic mining will be enabled, disabled otherwise
#[rpc(name = "anvil_setAutomine")]
fn set_auto_mine(&self, enable: bool) -> RpcResult<()>;

/// Sets the mining behavior to interval with the given interval (seconds).
///
/// # Arguments
///
/// * `seconds` - Frequency of automatic block production (in seconds)
#[rpc(name = "anvil_setIntervalMining")]
fn set_interval_mining(&self, seconds: u64) -> RpcResult<()>;

/// Sets the block timestamp interval. All future blocks' timestamps will
/// have the provided amount of seconds in-between of them. Does not affect
/// the block production interval.
///
/// # Arguments
///
/// * `seconds` - The minimum gas price to be set
/// * `seconds` - The interval between two consecutive blocks (in seconds)
#[rpc(name = "anvil_setBlockTimestampInterval")]
fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()>;

Expand Down
27 changes: 27 additions & 0 deletions src/node/anvil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ use crate::{
impl<S: ForkSource + std::fmt::Debug + Clone + Send + Sync + 'static> AnvilNamespaceT
for InMemoryNode<S>
{
fn get_auto_mine(&self) -> RpcResult<bool> {
self.get_immediate_sealing()
.map_err(|err| {
tracing::error!("failed getting immediate sealing: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn set_auto_mine(&self, enable: bool) -> RpcResult<()> {
self.set_immediate_sealing(enable)
.map_err(|err| {
tracing::error!("failed setting immediate sealing: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn set_interval_mining(&self, seconds: u64) -> RpcResult<()> {
self.set_interval_sealing(seconds)
.map_err(|err| {
tracing::error!("failed setting interval sealing: {:?}", err);
into_jsrpc_error(Web3Error::InternalError(err))
})
.into_boxed_future()
}

fn set_block_timestamp_interval(&self, seconds: u64) -> RpcResult<()> {
self.time.set_block_timestamp_interval(seconds);
Ok(()).into_boxed_future()
Expand Down
9 changes: 8 additions & 1 deletion src/node/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use zksync_web3_decl::error::Web3Error;

use crate::node::impersonate::{ImpersonationManager, ImpersonationState};
use crate::node::time::{AdvanceTime, ReadTime, TimestampManager};
use crate::node::TxPool;
use crate::node::{BlockSealer, TxPool};
use crate::{
bootloader_debug::{BootloaderDebug, BootloaderDebugTracer},
config::{
Expand Down Expand Up @@ -971,6 +971,7 @@ pub struct InMemoryNode<S: Clone> {
/// An optional handle to the observability stack
pub(crate) observability: Option<Observability>,
pub(crate) pool: TxPool,
pub(crate) sealer: BlockSealer,
}

fn contract_address_from_tx_result(execution_result: &VmExecutionResultAndLogs) -> Option<H160> {
Expand All @@ -992,6 +993,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone> Default for InMemoryNode<S> {
TimestampManager::default(),
impersonation.clone(),
TxPool::new(impersonation),
BlockSealer::default(),
)
}
}
Expand All @@ -1004,6 +1006,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone> InMemoryNode<S> {
time: TimestampManager,
impersonation: ImpersonationManager,
pool: TxPool,
sealer: BlockSealer,
) -> Self {
let system_contracts_options = config.system_contracts_options;
let inner = InMemoryNodeInner::new(fork, config, &time, impersonation.clone());
Expand All @@ -1015,6 +1018,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone> InMemoryNode<S> {
impersonation,
observability,
pool,
sealer,
}
}

Expand All @@ -1029,6 +1033,7 @@ impl<S: ForkSource + std::fmt::Debug + Clone> InMemoryNode<S> {
TimestampManager::default(),
impersonation.clone(),
TxPool::new(impersonation),
BlockSealer::default(),
)
}

Expand Down Expand Up @@ -2070,6 +2075,7 @@ mod tests {
TimestampManager::default(),
impersonation.clone(),
TxPool::new(impersonation),
BlockSealer::default(),
);

let tx = testing::TransactionBuilder::new().build();
Expand All @@ -2094,6 +2100,7 @@ mod tests {
TimestampManager::default(),
impersonation.clone(),
TxPool::new(impersonation),
BlockSealer::default(),
);

let private_key = K256PrivateKey::from_bytes(H256::repeat_byte(0xef)).unwrap();
Expand Down
Loading

0 comments on commit 5c0d204

Please sign in to comment.