Skip to content

Commit

Permalink
feat: Optimized block tip seal criterion (#968)
Browse files Browse the repository at this point in the history
## What ❔

## Why ❔

<!-- Why are these changes done? What goal do they contribute to? What
are the principles behind them? -->
<!-- Example: PR templates ensure PR reviewers, observers, and future
iterators are in context about the evolution of repos. -->

## Checklist

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [ ] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.
- [ ] Code has been formatted via `zk fmt` and `zk lint`.
- [ ] Spellcheck has been run via `zk spellcheck`.
- [ ] Linkcheck has been run via `zk linkcheck`.
  • Loading branch information
StanislavBreadless committed Jan 30, 2024
1 parent 0759fb7 commit 8049eb3
Show file tree
Hide file tree
Showing 20 changed files with 433 additions and 32 deletions.
3 changes: 3 additions & 0 deletions core/lib/multivm/src/interface/traits/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ pub trait VmInterface<S, H: HistoryMode> {
/// Record VM memory metrics.
fn record_vm_memory_metrics(&self) -> VmMemoryMetrics;

/// Whether the VM still has enough gas to execute the batch tip
fn has_enough_gas_for_batch_tip(&self) -> bool;

/// Execute batch till the end and return the result, with final execution state
/// and bootloader memory.
fn finish_batch(&mut self) -> FinishedL1Batch {
Expand Down
6 changes: 6 additions & 0 deletions core/lib/multivm/src/versions/vm_1_3_2/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ impl<S: WriteStorage, H: HistoryMode> VmInterface<S, H> for Vm<S, H> {
}
}

fn has_enough_gas_for_batch_tip(&self) -> bool {
// For this version this overhead has not been calculated and it has not been used with those versions.
// We return some value just in case for backwards compatibility
true
}

fn finish_batch(&mut self) -> FinishedL1Batch {
self.vm
.execute_till_block_end(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use zksync_system_constants::{L1_GAS_PER_PUBDATA_BYTE, MAX_L2_TX_GAS_LIMIT, MAX_

use crate::vm_boojum_integration::old_vm::utils::heap_page_from_base;

/// The amount of ergs to be reserved at the end of the batch to ensure that it has enough ergs to verify compression, etc.
pub(crate) const BOOTLOADER_BATCH_TIP_OVERHEAD: u32 = 80_000_000;

/// The size of the bootloader memory in bytes which is used by the protocol.
/// While the maximal possible size is a lot higher, we restrict ourselves to a certain limit to reduce
/// the requirements on RAM.
Expand Down
5 changes: 5 additions & 0 deletions core/lib/multivm/src/versions/vm_boojum_integration/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use zksync_types::{
};
use zksync_utils::bytecode::CompressedBytecodeInfo;

use super::constants::BOOTLOADER_BATCH_TIP_OVERHEAD;
use crate::{
glue::GlueInto,
interface::{
Expand Down Expand Up @@ -164,6 +165,10 @@ impl<S: WriteStorage, H: HistoryMode> VmInterface<S, H> for Vm<S, H> {
self.record_vm_memory_metrics_inner()
}

fn has_enough_gas_for_batch_tip(&self) -> bool {
self.state.local_state.callstack.current.ergs_remaining >= BOOTLOADER_BATCH_TIP_OVERHEAD
}

fn finish_batch(&mut self) -> FinishedL1Batch {
let result = self.execute(VmExecutionMode::Batch);
let execution_state = self.get_current_execution_state();
Expand Down
3 changes: 3 additions & 0 deletions core/lib/multivm/src/versions/vm_latest/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use zksync_system_constants::{MAX_L2_TX_GAS_LIMIT, MAX_NEW_FACTORY_DEPS};

use crate::vm_latest::old_vm::utils::heap_page_from_base;

/// The amount of ergs to be reserved at the end of the batch to ensure that it has enough ergs to verify compression, etc.
pub(crate) const BOOTLOADER_BATCH_TIP_OVERHEAD: u32 = 80_000_000;

/// The size of the bootloader memory in bytes which is used by the protocol.
/// While the maximal possible size is a lot higher, we restrict ourselves to a certain limit to reduce
/// the requirements on RAM.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl<S: WriteStorage, H: HistoryMode> Vm<S, H> {
&mut self,
dispatcher: TracerDispatcher<S, H::Vm1_4_1>,
execution_mode: VmExecutionMode,
custom_pubdata_tracer: Option<PubdataTracer<S>>,
) -> VmExecutionResultAndLogs {
let mut enable_refund_tracer = false;
if let VmExecutionMode::OneTx = execution_mode {
Expand All @@ -30,8 +31,12 @@ impl<S: WriteStorage, H: HistoryMode> Vm<S, H> {
enable_refund_tracer = true;
}

let (_, result) =
self.inspect_and_collect_results(dispatcher, execution_mode, enable_refund_tracer);
let (_, result) = self.inspect_and_collect_results(
dispatcher,
execution_mode,
enable_refund_tracer,
custom_pubdata_tracer,
);
result
}

Expand All @@ -42,6 +47,7 @@ impl<S: WriteStorage, H: HistoryMode> Vm<S, H> {
dispatcher: TracerDispatcher<S, H::Vm1_4_1>,
execution_mode: VmExecutionMode,
with_refund_tracer: bool,
custom_pubdata_tracer: Option<PubdataTracer<S>>,
) -> (VmExecutionStopReason, VmExecutionResultAndLogs) {
let refund_tracers =
with_refund_tracer.then_some(RefundsTracer::new(self.batch_env.clone()));
Expand All @@ -51,7 +57,8 @@ impl<S: WriteStorage, H: HistoryMode> Vm<S, H> {
dispatcher,
self.storage.clone(),
refund_tracers,
Some(PubdataTracer::new(self.batch_env.clone(), execution_mode)),
custom_pubdata_tracer
.or_else(|| Some(PubdataTracer::new(self.batch_env.clone(), execution_mode))),
);

let timestamp_initial = Timestamp(self.state.local_state.timestamp);
Expand Down
284 changes: 284 additions & 0 deletions core/lib/multivm/src/versions/vm_latest/tests/block_tip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
use std::borrow::BorrowMut;

use ethabi::Token;
use zk_evm_1_4_1::{
aux_structures::Timestamp, zkevm_opcode_defs::system_params::MAX_PUBDATA_PER_BLOCK,
};
use zksync_contracts::load_sys_contract;
use zksync_system_constants::{
CONTRACT_FORCE_DEPLOYER_ADDRESS, KNOWN_CODES_STORAGE_ADDRESS, L1_MESSENGER_ADDRESS,
};
use zksync_types::{
commitment::SerializeCommitment, get_code_key, l2_to_l1_log::L2ToL1Log,
writes::StateDiffRecord, Address, Execute, H256, U256,
};
use zksync_utils::{bytecode::hash_bytecode, bytes_to_be_words, h256_to_u256, u256_to_h256};

use super::utils::{get_complex_upgrade_abi, read_complex_upgrade};
use crate::{
interface::{TxExecutionMode, VmExecutionMode, VmInterface},
vm_latest::{
constants::BOOTLOADER_BATCH_TIP_OVERHEAD,
tests::tester::{get_empty_storage, InMemoryStorageView, VmTesterBuilder},
tracers::PubdataTracer,
HistoryEnabled, TracerDispatcher,
},
};

#[derive(Debug, Clone, Default)]
struct L1MessengerTestData {
l2_to_l1_logs: usize,
messages: Vec<Vec<u8>>,
bytecodes: Vec<Vec<u8>>,
state_diffs: Vec<StateDiffRecord>,
}

struct MimicCallInfo {
to: Address,
who_to_mimic: Address,
data: Vec<u8>,
}

fn populate_mimic_calls(data: L1MessengerTestData) -> Vec<u8> {
let complex_upgrade = get_complex_upgrade_abi();
let l1_messenger = load_sys_contract("L1Messenger");

let logs_mimic_calls = (0..data.l2_to_l1_logs).map(|_| MimicCallInfo {
to: L1_MESSENGER_ADDRESS,
who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS,
data: l1_messenger
.function("sendL2ToL1Log")
.unwrap()
.encode_input(&[
Token::Bool(false),
Token::FixedBytes(H256::random().0.to_vec()),
Token::FixedBytes(H256::random().0.to_vec()),
])
.unwrap(),
});
let messages_mimic_calls = data.messages.iter().map(|message| MimicCallInfo {
to: L1_MESSENGER_ADDRESS,
who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS,
data: l1_messenger
.function("sendToL1")
.unwrap()
.encode_input(&[Token::Bytes(message.clone())])
.unwrap(),
});
let bytecodes_mimic_calls = data.bytecodes.iter().map(|bytecode| MimicCallInfo {
to: L1_MESSENGER_ADDRESS,
who_to_mimic: KNOWN_CODES_STORAGE_ADDRESS,
data: l1_messenger
.function("requestBytecodeL1Publication")
.unwrap()
.encode_input(&[Token::FixedBytes(hash_bytecode(bytecode).0.to_vec())])
.unwrap(),
});

let encoded_calls = logs_mimic_calls
.chain(messages_mimic_calls)
.chain(bytecodes_mimic_calls)
.map(|call| {
Token::Tuple(vec![
Token::Address(call.to),
Token::Address(call.who_to_mimic),
Token::Bytes(call.data),
])
})
.collect::<Vec<_>>();

complex_upgrade
.function("mimicCalls")
.unwrap()
.encode_input(&[Token::Array(encoded_calls)])
.unwrap()
}

fn execute_test(test_data: L1MessengerTestData) -> u32 {
let mut storage = get_empty_storage();
let complex_upgrade_code = read_complex_upgrade();

// For this test we'll just put the bytecode onto the force deployer address
storage.borrow_mut().set_value(
get_code_key(&CONTRACT_FORCE_DEPLOYER_ADDRESS),
hash_bytecode(&complex_upgrade_code),
);
storage
.borrow_mut()
.store_factory_dep(hash_bytecode(&complex_upgrade_code), complex_upgrade_code);

let mut vm = VmTesterBuilder::new(HistoryEnabled)
.with_storage(storage)
.with_execution_mode(TxExecutionMode::VerifyExecute)
.with_random_rich_accounts(1)
.build();

let bytecodes = test_data
.bytecodes
.iter()
.map(|bytecode| {
let hash = hash_bytecode(bytecode);
let words = bytes_to_be_words(bytecode.clone());
(h256_to_u256(hash), words)
})
.collect();
vm.vm
.state
.decommittment_processor
.populate(bytecodes, Timestamp(0));

let data = populate_mimic_calls(test_data.clone());
let account = &mut vm.rich_accounts[0];
let tx = account.get_l2_tx_for_execute(
Execute {
contract_address: CONTRACT_FORCE_DEPLOYER_ADDRESS,
calldata: data,
value: U256::zero(),
factory_deps: None,
},
None,
);

vm.vm.push_transaction(tx);
let result = vm.vm.execute(VmExecutionMode::OneTx);
assert!(!result.result.is_failed(), "Transaction wasn't successful");

// Now we count how much ergs were spent at the end of the batch
// It is assumed that the top level frame is the bootloader

let ergs_before = vm.vm.state.local_state.callstack.current.ergs_remaining;

// We ensure that indeed the provided state diffs are used
let pubdata_tracer = PubdataTracer::<InMemoryStorageView>::new_with_forced_state_diffs(
vm.vm.batch_env.clone(),
VmExecutionMode::Batch,
test_data.state_diffs,
);

let result = vm.vm.inspect_inner(
TracerDispatcher::default(),
VmExecutionMode::Batch,
Some(pubdata_tracer),
);

assert!(!result.result.is_failed(), "Batch wasn't successful");

let ergs_after = vm.vm.state.local_state.callstack.current.ergs_remaining;

ergs_before - ergs_after
}

fn generate_state_diffs(
repeated_writes: bool,
small_diff: bool,
number_of_state_diffs: usize,
) -> Vec<StateDiffRecord> {
(0..number_of_state_diffs)
.map(|i| {
let address = Address::from_low_u64_be(i as u64);
let key = U256::from(i);
let enumeration_index = if repeated_writes { i + 1 } else { 0 };

let (initial_value, final_value) = if small_diff {
// As small as it gets, one byte to denote zeroing out the value
(U256::from(1), U256::from(0))
} else {
// As large as it gets
(U256::from(0), U256::from(2).pow(255.into()))
};

StateDiffRecord {
address,
key,
derived_key: u256_to_h256(i.into()).0,
enumeration_index: enumeration_index as u64,
initial_value,
final_value,
}
})
.collect()
}

#[test]
fn test_dry_run_upper_bound() {
// We are re-using the `ComplexUpgrade` contract as it already has the `mimicCall` functionality.
// To get the upper bound, we'll try to do the following:
// 1. Max number of logs.
// 2. Lots of small L2->L1 messages / one large L2->L1 message.
// 3. Lots of small bytecodes / one large bytecode.
// 4. Lots of storage slot updates.

let max_logs = execute_test(L1MessengerTestData {
l2_to_l1_logs: L2ToL1Log::MIN_L2_L1_LOGS_TREE_SIZE,
..Default::default()
});

let max_messages = execute_test(L1MessengerTestData {
// Each L2->L1 message is accompanied by a Log, so the max number of pubdata is bound by it
messages: vec![vec![0; 0]; MAX_PUBDATA_PER_BLOCK as usize / L2ToL1Log::SERIALIZED_SIZE],
..Default::default()
});

let long_message = execute_test(L1MessengerTestData {
// Each L2->L1 message is accompanied by a Log, so the max number of pubdata is bound by it
messages: vec![vec![0; MAX_PUBDATA_PER_BLOCK as usize]; 1],
..Default::default()
});

let max_bytecodes = execute_test(L1MessengerTestData {
// Each bytecode must be at least 32 bytes long
bytecodes: vec![vec![0; 32]; MAX_PUBDATA_PER_BLOCK as usize / 32],
..Default::default()
});

let long_bytecode = execute_test(L1MessengerTestData {
// We have to add 48 since a valid bytecode must have an odd number of 32 byte words
bytecodes: vec![vec![0; MAX_PUBDATA_PER_BLOCK as usize + 48]; 1],
..Default::default()
});

let lots_of_small_repeated_writes = execute_test(L1MessengerTestData {
// In theory each state diff can require only 5 bytes to be published (enum index + 4 bytes for the key)
state_diffs: generate_state_diffs(true, true, MAX_PUBDATA_PER_BLOCK as usize / 5),
..Default::default()
});

let lots_of_big_repeated_writes = execute_test(L1MessengerTestData {
// Each big write will approximately require 32 bytes to encode
state_diffs: generate_state_diffs(true, false, MAX_PUBDATA_PER_BLOCK as usize / 32),
..Default::default()
});

let lots_of_small_initial_writes = execute_test(L1MessengerTestData {
// Each initial write will take at least 32 bytes for derived key + 5 bytes for value
state_diffs: generate_state_diffs(false, true, MAX_PUBDATA_PER_BLOCK as usize / 37),
..Default::default()
});

let lots_of_large_initial_writes = execute_test(L1MessengerTestData {
// Each big write will take at least 32 bytes for derived key + 32 bytes for value
state_diffs: generate_state_diffs(false, false, MAX_PUBDATA_PER_BLOCK as usize / 64),
..Default::default()
});

let max_used_gas = vec![
max_logs,
max_messages,
long_message,
max_bytecodes,
long_bytecode,
lots_of_small_repeated_writes,
lots_of_big_repeated_writes,
lots_of_small_initial_writes,
lots_of_large_initial_writes,
]
.into_iter()
.max()
.unwrap();

// We use 2x overhead for the batch tip compared to the worst estimated scenario.
assert!(
max_used_gas * 2 <= BOOTLOADER_BATCH_TIP_OVERHEAD,
"BOOTLOADER_BATCH_TIP_OVERHEAD is too low"
);
}
1 change: 1 addition & 0 deletions core/lib/multivm/src/versions/vm_latest/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod bootloader;
mod default_aa;
// TODO - fix this test
// `mod invalid_bytecode;`
mod block_tip;
mod bytecode_publishing;
mod call_tracer;
mod circuits;
Expand Down

0 comments on commit 8049eb3

Please sign in to comment.