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: asynchronous and configurable block sealing #392

Merged
merged 13 commits into from
Nov 27, 2024
Merged
34 changes: 34 additions & 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ openrpc-types = "0.4.0"
alloy = { version = "0.5", features = ["full"] }
test-log = "0.2.16"
fs2 = "0.4.3"
test-case = "3.3.1"

[profile.dev]
debug = 0
23 changes: 14 additions & 9 deletions e2e-tests/test/debug-apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { RichAccounts } from "../helpers/constants";
import { deployContract, expectThrowsAsync, getTestProvider } from "../helpers/utils";
import { BigNumber } from "ethers";
import { TransactionResponse } from "@ethersproject/abstract-provider";

const provider = getTestProvider();

Expand Down Expand Up @@ -88,15 +89,16 @@ describe("debug_traceTransaction", function () {

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const trace = await provider.send("debug_traceTransaction", [txReceipt.hash]);
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
const txReceipt = await txResponse.wait();
const trace = await provider.send("debug_traceTransaction", [txReceipt.transactionHash]);

// call should be successful
expect(trace.error).to.equal(null);
expect(trace.calls.length).to.equal(1);

// gas limit should match
expect(BigNumber.from(trace.gas).toNumber()).to.equal(txReceipt.gasLimit.toNumber());
expect(BigNumber.from(trace.gas).toNumber()).to.equal(txResponse.gasLimit.toNumber());
});

it("Should respect only_top_calls option", async function () {
Expand All @@ -105,9 +107,10 @@ describe("debug_traceTransaction", function () {

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
const txReceipt = await txResponse.wait();
const trace = await provider.send("debug_traceTransaction", [
txReceipt.hash,
txReceipt.transactionHash,
{ tracer: "callTracer", tracerConfig: { onlyTopCall: true } },
]);

Expand All @@ -124,7 +127,8 @@ describe("debug_traceBlockByHash", function () {

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
await txResponse.wait();
const latestBlock = await provider.getBlock("latest");
const block = await provider.getBlock(latestBlock.number - 1);

Expand All @@ -135,7 +139,7 @@ describe("debug_traceBlockByHash", function () {

// should contain trace for our tx
const trace = traces[0].result;
expect(trace.input).to.equal(txReceipt.data);
expect(trace.input).to.equal(txResponse.data);
});
});

Expand All @@ -146,7 +150,8 @@ describe("debug_traceBlockByNumber", function () {

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
await txResponse.wait();

// latest block will be empty, check we get no traces for it
const empty_traces = await provider.send("debug_traceBlockByNumber", ["latest"]);
Expand All @@ -161,6 +166,6 @@ describe("debug_traceBlockByNumber", function () {

// should contain trace for our tx
const trace = traces[0].result;
expect(trace.input).to.equal(txReceipt.data);
expect(trace.input).to.equal(txResponse.data);
});
});
9 changes: 6 additions & 3 deletions e2e-tests/test/evm-apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ describe("evm_increaseTime", function () {
// Act
await provider.send("evm_increaseTime", [timeIncreaseInSeconds]);

await wallet.sendTransaction({
const txResponse = await wallet.sendTransaction({
to: userWallet.address,
value: ethers.utils.parseEther("0.1"),
});
await txResponse.wait();
expectedTimestamp += 2; // New transaction will add two blocks

// Assert
Expand All @@ -73,10 +74,11 @@ describe("evm_setNextBlockTimestamp", function () {
// Act
await provider.send("evm_setNextBlockTimestamp", [expectedTimestamp.toString(16)]);

await wallet.sendTransaction({
const txResponse = await wallet.sendTransaction({
to: userWallet.address,
value: ethers.utils.parseEther("0.1"),
});
await txResponse.wait();
expectedTimestamp += 1; // After executing a transaction, the node puts it into a block and increases its current timestamp

// Assert
Expand All @@ -97,10 +99,11 @@ describe("evm_setTime", function () {
// Act
await provider.send("evm_setTime", [expectedTimestamp]);

await wallet.sendTransaction({
const txResponse = await wallet.sendTransaction({
to: userWallet.address,
value: ethers.utils.parseEther("0.1"),
});
await txResponse.wait();
expectedTimestamp += 2; // New transaction will add two blocks

// Assert
Expand Down
3 changes: 2 additions & 1 deletion e2e-tests/test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ describe("Greeter Smart Contract", function () {

// setup user wallet with 3 ETH
const userWallet = Wallet.createRandom().connect(provider);
await wallet.sendTransaction({
const txResponse = await wallet.sendTransaction({
to: userWallet.address,
value: ethers.utils.parseEther("3"),
});
await txResponse.wait();

// deploy Greeter contract
const artifact = await deployer.loadArtifact("Greeter");
Expand Down
13 changes: 8 additions & 5 deletions e2e-tests/test/zks-apis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BigNumber, ethers } from "ethers";
import * as hre from "hardhat";
import { TransactionRequest } from "zksync-web3/build/src/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { TransactionResponse } from "@ethersproject/abstract-provider";

const provider = getTestProvider();

Expand Down Expand Up @@ -66,8 +67,9 @@ describe("zks_getTransactionDetails", function () {

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);

const txReceipt = await greeter.setGreeting("Luke Skywalker");
const details = await provider.send("zks_getTransactionDetails", [txReceipt.hash]);
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
const txReceipt = await txResponse.wait();
const details = await provider.send("zks_getTransactionDetails", [txReceipt.transactionHash]);

expect(details["status"]).to.equal("included");
expect(details["initiatorAddress"].toLowerCase()).to.equal(wallet.address.toLowerCase());
Expand Down Expand Up @@ -96,7 +98,7 @@ describe("zks_getBlockDetails", function () {
const deployer = new Deployer(hre, wallet);

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);
await greeter.setGreeting("Luke Skywalker");
await (await greeter.setGreeting("Luke Skywalker")).wait();

const latestBlock = await provider.getBlock("latest");
const details = await provider.send("zks_getBlockDetails", [latestBlock.number]);
Expand Down Expand Up @@ -145,13 +147,14 @@ describe("zks_getRawBlockTransactions", function () {
const deployer = new Deployer(hre, wallet);

const greeter = await deployContract(deployer, "Greeter", ["Hi"]);
const receipt = await greeter.setGreeting("Luke Skywalker");
const txResponse: TransactionResponse = await greeter.setGreeting("Luke Skywalker");
await txResponse.wait();

const latestBlock = await provider.getBlock("latest");
const txns = await provider.send("zks_getRawBlockTransactions", [latestBlock.number - 1]);

expect(txns.length).to.equal(1);
expect(txns[0]["execute"]["calldata"]).to.equal(receipt.data);
expect(txns[0]["execute"]["calldata"]).to.equal(txResponse.data);
});
});

Expand Down
17 changes: 16 additions & 1 deletion src/config/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::env;
use std::time::Duration;

use clap::{arg, command, Parser, Subcommand};
use rand::{rngs::StdRng, SeedableRng};
Expand Down Expand Up @@ -175,6 +176,11 @@ pub struct Cli {
/// [default: m/44'/60'/0'/0/]
#[arg(long, help_heading = "Account Configuration")]
pub derivation_path: Option<String>,

/// Block time in seconds for interval sealing.
/// If unset, node seals a new block as soon as there is at least one transaction.
#[arg(short, long, value_name = "SECONDS", value_parser = duration_from_secs_f64, help_heading = "Block Sealing")]
pub block_time: Option<Duration>,
itegulov marked this conversation as resolved.
Show resolved Hide resolved
}

#[derive(Debug, Subcommand, Clone)]
Expand Down Expand Up @@ -293,7 +299,8 @@ impl Cli {
}))
.with_chain_id(self.chain_id)
.set_config_out(self.config_out)
.with_evm_emulator(if self.emulate_evm { Some(true) } else { None });
.with_evm_emulator(if self.emulate_evm { Some(true) } else { None })
.with_block_time(self.block_time);

if self.emulate_evm && self.dev_system_contracts != Some(SystemContractsOptions::Local) {
return Err(eyre::eyre!(
Expand Down Expand Up @@ -335,3 +342,11 @@ impl Cli {
gen
}
}

fn duration_from_secs_f64(s: &str) -> Result<Duration, String> {
let s = s.parse::<f64>().map_err(|e| e.to_string())?;
if s == 0.0 {
return Err("Duration must be greater than 0".to_string());
}
Duration::try_from_secs_f64(s).map_err(|e| e.to_string())
}
25 changes: 21 additions & 4 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use serde::Deserialize;
use serde_json::{json, to_writer, Value};
use std::collections::HashMap;
use std::fs::File;
use std::time::Duration;
use zksync_types::fee_model::FeeModelConfigV2;
use zksync_types::U256;

Expand All @@ -29,12 +30,12 @@ pub mod show_details;
pub const VERSION_MESSAGE: &str = concat!(env!("CARGO_PKG_VERSION"));

const BANNER: &str = r#"
_ _ _____ _ __
__ _ _ __ __ __(_)| | |__ /| |/ / ___ _ _ _ __ ___
_ _ _____ _ __
__ _ _ __ __ __(_)| | |__ /| |/ / ___ _ _ _ __ ___
/ _` || '_ \ \ \ / /| || | _____ / / | ' / / __|| | | || '_ \ / __|
| (_| || | | | \ V / | || ||_____| / /_ | . \ \__ \| |_| || | | || (__
| (_| || | | | \ V / | || ||_____| / /_ | . \ \__ \| |_| || | | || (__
\__,_||_| |_| \_/ |_||_| /____||_|\_\|___/ \__, ||_| |_| \___|
|___/
|___/
"#;
/// Struct to hold the details of the fork for display purposes
pub struct ForkPrintInfo {
Expand Down Expand Up @@ -99,6 +100,11 @@ pub struct TestNodeConfig {
pub signer_accounts: Vec<PrivateKeySigner>,
/// Whether the node operates in offline mode
pub offline: bool,
/// Block time in seconds for interval sealing.
/// If unset, node seals a new block as soon as there is at least one transaction.
pub block_time: Option<Duration>,
/// Maximum number of transactions per block
pub max_transactions: usize,
}

impl Default for TestNodeConfig {
Expand Down Expand Up @@ -143,6 +149,10 @@ impl Default for TestNodeConfig {

// Offline mode disabled by default
offline: false,

// Block sealing configuration default
block_time: None,
max_transactions: 1000,
}
}
}
Expand Down Expand Up @@ -623,6 +633,13 @@ impl TestNodeConfig {
pub fn is_offline(&self) -> bool {
self.offline
}

/// Set the block time
#[must_use]
pub fn with_block_time(mut self, block_time: Option<Duration>) -> Self {
self.block_time = block_time;
self
}
}

/// Account Generator
Expand Down
36 changes: 32 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ use crate::namespaces::{
EthTestNodeNamespaceT, EvmNamespaceT, HardhatNamespaceT, NetNamespaceT, Web3NamespaceT,
ZksNamespaceT,
};
use crate::node::{BlockProducer, BlockSealer, ImpersonationManager, TimestampManager, TxPool};
use crate::system_contracts::SystemContracts;

#[allow(clippy::too_many_arguments)]
async fn build_json_http<
Expand Down Expand Up @@ -265,8 +267,17 @@ async fn main() -> anyhow::Result<()> {
None
};

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

if let Some(ref bytecodes_dir) = config.override_bytecodes_dir {
override_bytecodes(&node, bytecodes_dir.to_string()).unwrap();
Expand All @@ -292,13 +303,30 @@ async fn main() -> anyhow::Result<()> {
let threads = build_json_http(
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), config.port),
log_level_filter,
node,
node.clone(),
)
.await;

let block_sealer = 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(
node,
pool,
block_sealer,
system_contracts,
));

config.print(fork_print_info.as_ref());

future::select_all(vec![threads]).await.0.unwrap();
future::select_all(vec![threads, block_producer_handle])
.await
.0
.unwrap();

Ok(())
}
Loading
Loading