Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement evm_increaseTime and evm_setNextBlockTimestamp #93

Merged
merged 14 commits into from
Sep 11, 2023
4 changes: 2 additions & 2 deletions SUPPORTED_APIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ The `status` options are:
| `ETH` | `eth_uninstallFilter` | `NOT IMPLEMENTED`<br />[GitHub Issue #38](https://github.com/matter-labs/era-test-node/issues/38) | Uninstalls a filter with given id |
| `ETH` | `eth_unsubscribe` | `NOT IMPLEMENTED` | Cancel a subscription to a particular event |
| `EVM` | `evm_addAccount` | `NOT IMPLEMENTED` | Adds any arbitrary account |
| `EVM` | `evm_increaseTime` | `NOT IMPLEMENTED`<br />[GitHub Issue #66](https://github.com/matter-labs/era-test-node/issues/66) | Jump forward in time by the given amount of time, in seconds |
| `EVM` | `evm_increaseTime` | `SUPPORTED` | Jump forward in time by the given amount of time, in seconds |
MexicanAce marked this conversation as resolved.
Show resolved Hide resolved
| `EVM` | `evm_mine` | `NOT IMPLEMENTED`<br />[GitHub Issue #67](https://github.com/matter-labs/era-test-node/issues/67) | Force a single block to be mined |
| `EVM` | `evm_removeAccount` | `NOT IMPLEMENTED` | Removes an account |
| `EVM` | `evm_revert` | `NOT IMPLEMENTED`<br />[GitHub Issue #70](https://github.com/matter-labs/era-test-node/issues/70) | Revert the state of the blockchain to a previous snapshot |
Expand All @@ -85,7 +85,7 @@ The `status` options are:
| `EVM` | `evm_setBlockGasLimit` | `NOT IMPLEMENTED` | Sets the Block Gas Limit of the network |
| `EVM` | `evm_setIntervalMining` | `NOT IMPLEMENTED` | Enables (with a numeric argument greater than 0) or disables (with a numeric argument equal to 0), the automatic mining of blocks at a regular interval of milliseconds, each of which will include all pending transactions |
| `EVM` | `evm_setNextBlockTimestamp` | `NOT IMPLEMENTED`<br />[GitHub Issue #68](https://github.com/matter-labs/era-test-node/issues/68) | Works like `evm_increaseTime`, but takes the exact timestamp that you want in the next block, and increases the time accordingly |
| `EVM` | `evm_setTime` | `NOT IMPLEMENTED` | Sets the internal clock time to the given timestamp |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's actually a need for both here. Ganache only supports evm_setTime, while Hardhat only support evm_setNextBlockTimestamp ... but I think they both do the same thing. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can either implement evm_setTime in this PR, or leave the line here in this table.

Also, it looks like this change leaves 2 rows/entries for evm_setNextBlockTimestamp

Copy link
Contributor Author

@nbaztec nbaztec Sep 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's actually a need for both here. Ganache only supports evm_setTime, while Hardhat only support evm_setNextBlockTimestamp ... but I think they both do the same thing. Thoughts?

Yeah they do exactly the same thing with the following differences:

  • setNextBlockTimestamp allows only setting a timestamp forward in future, while setTime supports setting it backwards.
  • setNextBlockTimestamp returns the newly set timestamp in response, whereas setTime returns the difference between the current and the new timestamp.

AFAIK setTime can be seen as a superset of setNextBlockTimestamp and from most examples out there no one seems to be using the return value to do anything meaningful.

I'm fine with supporting both, though it must be explicitly mentioned (to avoid any confusion for users) that these two achieve the same result and are mostly arising out of API differences between hardhat and ganache - though it does tend to make our API interface a bit messy.

| `EVM` | `evm_setTime` | `SUPPORTED` | Sets the internal clock time to the given timestamp |
| `EVM` | `evm_snapshot` | `NOT IMPLEMENTED`<br />[GitHub Issue #69](https://github.com/matter-labs/era-test-node/issues/69) | Snapshot the state of the blockchain at the current block |
| `HARDHAT` | `hardhat_addCompilationResult` | `NOT IMPLEMENTED` | Add information about compiled contracts |
| `HARDHAT` | `hardhat_dropTransaction` | `NOT IMPLEMENTED` | Remove a transaction from the mempool |
Expand Down
316 changes: 316 additions & 0 deletions src/evm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
use std::sync::{Arc, RwLock};

use crate::{fork::ForkSource, node::InMemoryNodeInner};
use jsonrpc_core::{BoxFuture, Result};
use jsonrpc_derive::rpc;
use zksync_basic_types::U64;
use zksync_core::api_server::web3::backend_jsonrpc::error::into_jsrpc_error;
use zksync_web3_decl::error::Web3Error;

/// Implementation of EvmNamespace
pub struct EvmNamespaceImpl<S> {
node: Arc<RwLock<InMemoryNodeInner<S>>>,
}

impl<S> EvmNamespaceImpl<S> {
/// Creates a new `Evm` instance with the given `node`.
pub fn new(node: Arc<RwLock<InMemoryNodeInner<S>>>) -> Self {
Self { node }
}
}

#[rpc]
pub trait EvmNamespaceT {
/// Increase the current timestamp for the node
///
/// # Parameters
/// - `time_delta`: The number of seconds to increase time by
///
/// # Returns
/// The applied time delta to `current_timestamp` value for the InMemoryNodeInner.
#[rpc(name = "evm_increaseTime")]
fn increase_time(&self, time_delta_seconds: U64) -> BoxFuture<Result<U64>>;

/// Set the current timestamp for the node.
/// Warning: This will allow you to move backwards in time, which may cause new blocks to appear to be
/// mined before old blocks. This will result in an invalid state.
///
/// # Parameters
/// - `time`: The timestamp to set the time to
///
/// # Returns
/// The difference between the `current_timestamp` and the new timestamp for the InMemoryNodeInner.
#[rpc(name = "evm_setTime")]
fn set_time(&self, time: U64) -> BoxFuture<Result<i64>>;
}

impl<S: Send + Sync + 'static + ForkSource + std::fmt::Debug> EvmNamespaceT
for EvmNamespaceImpl<S>
{
fn increase_time(&self, time_delta_seconds: U64) -> BoxFuture<Result<U64>> {
let inner = Arc::clone(&self.node);

Box::pin(async move {
if time_delta_seconds.is_zero() {
return Ok(time_delta_seconds);
}

let time_delta = time_delta_seconds.as_u64().saturating_mul(1000);
match inner.write() {
Ok(mut inner_guard) => {
inner_guard.current_timestamp =
inner_guard.current_timestamp.saturating_add(time_delta);
Ok(time_delta_seconds)
}
Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)),
}
})
}

fn set_time(&self, time: U64) -> BoxFuture<Result<i64>> {
let inner = Arc::clone(&self.node);

Box::pin(async move {
match inner.write() {
Ok(mut inner_guard) => {
let time_diff = (time.as_u64() as i128)
.saturating_sub(inner_guard.current_timestamp as i128)
as i64;
inner_guard.current_timestamp = time.as_u64();
Ok(time_diff)
}
Err(_) => Err(into_jsrpc_error(Web3Error::InternalError)),
}
})
}
}

#[cfg(test)]
mod tests {
use crate::{http_fork_source::HttpForkSource, node::InMemoryNode};

use super::*;

#[tokio::test]
async fn test_increase_time_zero_value() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let increase_value_seconds = 0u64;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
let expected_response = increase_value_seconds;

let actual_response = evm
.increase_time(U64::from(increase_value_seconds))
.await
.expect("failed increasing timestamp")
.as_u64();
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(
increase_value_seconds.saturating_mul(1000u64),
timestamp_after.saturating_sub(timestamp_before),
"timestamp did not increase by the specified amount",
);
}

#[tokio::test]
async fn test_increase_time_max_value() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let increase_value_seconds = u64::MAX;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
assert_ne!(0, timestamp_before, "initial timestamp must be non zero",);
let expected_response = increase_value_seconds;

let actual_response = evm
.increase_time(U64::from(increase_value_seconds))
.await
.expect("failed increasing timestamp")
.as_u64();
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(
u64::MAX,
timestamp_after,
"timestamp did not saturate upon increase",
);
}

#[tokio::test]
async fn test_increase_time() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let increase_value_seconds = 100u64;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
let expected_response = increase_value_seconds;

let actual_response = evm
.increase_time(U64::from(increase_value_seconds))
.await
.expect("failed increasing timestamp")
.as_u64();
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(
increase_value_seconds.saturating_mul(1000u64),
timestamp_after.saturating_sub(timestamp_before),
"timestamp did not increase by the specified amount",
);
}

#[tokio::test]
async fn test_set_time_future() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let new_time = 10_000u64;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
assert_ne!(timestamp_before, new_time, "timestamps must be different");
let expected_response = 9000;

let actual_response = evm
.set_time(U64::from(new_time))
.await
.expect("failed setting timestamp");
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(new_time, timestamp_after, "timestamp was not set correctly",);
}

#[tokio::test]
async fn test_set_time_past() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let new_time = 10u64;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
assert_ne!(timestamp_before, new_time, "timestamps must be different");
let expected_response = -990;

let actual_response = evm
.set_time(U64::from(new_time))
.await
.expect("failed setting timestamp");
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(new_time, timestamp_after, "timestamp was not set correctly",);
}

#[tokio::test]
async fn test_set_time_same_value() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

let new_time = 1000u64;
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");
assert_eq!(timestamp_before, new_time, "timestamps must be same");
let expected_response = 0;

let actual_response = evm
.set_time(U64::from(new_time))
.await
.expect("failed setting timestamp");
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.expect("failed reading timestamp");

assert_eq!(expected_response, actual_response, "erroneous response");
assert_eq!(
timestamp_before, timestamp_after,
"timestamp must not change",
);
}

#[tokio::test]
async fn test_set_time_edges() {
let node = InMemoryNode::<HttpForkSource>::default();
let evm = EvmNamespaceImpl::new(node.get_inner());

for new_time in [0, u64::MAX] {
let timestamp_before = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.unwrap_or_else(|_| panic!("case {}: failed reading timestamp", new_time));
assert_ne!(
timestamp_before, new_time,
"case {new_time}: timestamps must be different"
);
let expected_response =
(new_time as i128).saturating_sub(timestamp_before as i128) as i64;

let actual_response = evm
.set_time(U64::from(new_time))
.await
.expect("failed setting timestamp");
let timestamp_after = node
.get_inner()
.read()
.map(|inner| inner.current_timestamp)
.unwrap_or_else(|_| panic!("case {}: failed reading timestamp", new_time));

assert_eq!(
expected_response, actual_response,
"case {new_time}: erroneous response"
);
assert_eq!(
new_time, timestamp_after,
"case {new_time}: timestamp was not set correctly",
);
}
}
}
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
use crate::node::{ShowStorageLogs, ShowVMDetails};
use clap::{Parser, Subcommand};
use configuration_api::ConfigurationApiNamespaceT;
use evm::{EvmNamespaceImpl, EvmNamespaceT};
use fork::{ForkDetails, ForkSource};
use node::ShowCalls;
use zks::ZkMockNamespaceImpl;

mod configuration_api;
mod console_log;
mod deps;
mod evm;
mod fork;
mod formatter;
mod http_fork_source;
Expand Down Expand Up @@ -136,6 +138,7 @@ async fn build_json_http<
node: InMemoryNode<S>,
net: NetNamespace,
config_api: ConfigurationApiNamespace<S>,
evm: EvmNamespaceImpl<S>,
zks: ZkMockNamespaceImpl<S>,
) -> tokio::task::JoinHandle<()> {
let (sender, recv) = oneshot::channel::<()>();
Expand All @@ -145,6 +148,7 @@ async fn build_json_http<
io.extend_with(node.to_delegate());
io.extend_with(net.to_delegate());
io.extend_with(config_api.to_delegate());
io.extend_with(evm.to_delegate());
io.extend_with(zks.to_delegate());

io
Expand Down Expand Up @@ -302,13 +306,15 @@ async fn main() -> anyhow::Result<()> {

let net = NetNamespace::new(L2ChainId(TEST_NODE_NETWORK_ID));
let config_api = ConfigurationApiNamespace::new(node.get_inner());
let evm = EvmNamespaceImpl::new(node.get_inner());
let zks = ZkMockNamespaceImpl::new(node.get_inner());

let threads = build_json_http(
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), opt.port),
node,
net,
config_api,
evm,
zks,
)
.await;
Expand Down
Loading