Skip to content

Commit 2768cc2

Browse files
feat: Improve tracer EVM compatibility for deployments (#245)
## What ❔ <!-- What are the changes this PR brings about? --> <!-- Example: This PR adds a PR template to the repo. --> <!-- (For bigger PRs adding more context is appreciated) --> ## Why ❔ <!-- Why are these changes done? What goal do they contribute to? What are the principles behind them? --> <!-- The `Why` has to be clear to non-Matter Labs entities running their own ZK Chain --> <!-- Example: PR templates ensure PR reviewers, observers, and future iterators are in context about the evolution of repos. --> ## Is this a breaking change? - [ ] Yes - [ ] No ## 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. --------- Co-authored-by: zksync-era-bot <zksync-era-bot@users.noreply.github.com>
1 parent edfd676 commit 2768cc2

File tree

18 files changed

+235
-28
lines changed

18 files changed

+235
-28
lines changed

basic_system/src/system_implementation/flat_storage_model/account_cache.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ where
708708
storage: &mut NewStorageWithAccountPropertiesUnderHash<A, SC, SCC, R, P>,
709709
preimages_cache: &mut BytecodeAndAccountDataPreimagesStorage<R, A>,
710710
oracle: &mut impl IOOracle,
711-
) -> Result<&'static [u8], SystemError> {
711+
) -> Result<(&'static [u8], Bytes32, u32), SystemError> {
712712
let alloc = self.alloc.clone();
713713
// Charge for code deposit cost
714714
match from_ee {
@@ -817,7 +817,7 @@ where
817817
})
818818
})?;
819819

820-
Ok(deployed_code)
820+
Ok((deployed_code, bytecode_hash, observable_bytecode_len))
821821
}
822822

823823
/// Assumes [code_hash] is of default version, which does not contain

basic_system/src/system_implementation/flat_storage_model/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,14 @@ where
349349
at_address: &<Self::IOTypes as SystemIOTypesConfig>::Address,
350350
bytecode: &[u8],
351351
oracle: &mut impl IOOracle,
352-
) -> Result<&'static [u8], SystemError> {
352+
) -> Result<
353+
(
354+
&'static [u8],
355+
<Self::IOTypes as SystemIOTypesConfig>::BytecodeHashValue,
356+
u32,
357+
),
358+
SystemError,
359+
> {
353360
self.account_data_cache.deploy_code::<PROOF_ENV>(
354361
from_ee,
355362
resources,

basic_system/src/system_implementation/system/io_subsystem.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,14 @@ where
10341034
resources: &mut Self::Resources,
10351035
at_address: &<Self::IOTypes as SystemIOTypesConfig>::Address,
10361036
bytecode: &[u8],
1037-
) -> Result<&'static [u8], SystemError> {
1037+
) -> Result<
1038+
(
1039+
&'static [u8],
1040+
<Self::IOTypes as SystemIOTypesConfig>::BytecodeHashValue,
1041+
u32,
1042+
),
1043+
SystemError,
1044+
> {
10381045
self.storage
10391046
.deploy_code(from_ee, resources, at_address, bytecode, &mut self.oracle)
10401047
}

evm_interpreter/src/instructions/host.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ impl<'ee, S: EthereumLikeTypes> Interpreter<'ee, S> {
280280
&mut self,
281281
system: &mut System<S>,
282282
external_call_dest: &mut Option<EVMCallRequest<S>>,
283+
tracer: &mut impl Tracer<S>,
283284
) -> InstructionResult {
284285
self.gas.spend_gas_and_native(
285286
gas_constants::CREATE,
@@ -349,6 +350,8 @@ impl<'ee, S: EthereumLikeTypes> Interpreter<'ee, S> {
349350

350351
self.pending_os_request = Some(PendingOsRequest::Create(deployed_address));
351352

353+
tracer.evm_tracer().on_create_request(IS_CREATE2);
354+
352355
*external_call_dest = Some(EVMCallRequest {
353356
ergs_to_pass: all_resources.ergs(),
354357
call_value: value,

evm_interpreter/src/interpreter.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ impl<'ee, S: EthereumLikeTypes> Interpreter<'ee, S> {
157157
.gas
158158
.spend_gas_and_native(0, STEP_NATIVE_COST)
159159
.and_then(|_| match opcode {
160-
opcodes::CREATE => self.create::<false>(system, external_call_dest),
161-
opcodes::CREATE2 => self.create::<true>(system, external_call_dest),
160+
opcodes::CREATE => self.create::<false>(system, external_call_dest, tracer),
161+
opcodes::CREATE2 => self.create::<true>(system, external_call_dest, tracer),
162162
opcodes::CALL => self.call(external_call_dest),
163163
opcodes::CALLCODE => self.call_code(external_call_dest),
164164
opcodes::DELEGATECALL => self.delegate_call(external_call_dest),
@@ -390,13 +390,25 @@ impl<'ee, S: EthereumLikeTypes> Interpreter<'ee, S> {
390390
&self.address,
391391
deployed_code,
392392
) {
393-
Ok(_) => {
393+
Ok((
394+
actual_deployed_bytecode,
395+
internal_bytecode_hash,
396+
observable_bytecode_len,
397+
)) => {
394398
// TODO: debug implementation for Bits uses global alloc, which panics in ZKsync OS
395399
#[cfg(not(target_arch = "riscv32"))]
396400
let _ = system.get_logger().write_fmt(format_args!(
397401
"Successfully deployed contract at {:?} \n",
398402
self.address
399403
));
404+
405+
tracer.on_bytecode_change(
406+
THIS_EE_TYPE,
407+
self.address,
408+
Some(actual_deployed_bytecode),
409+
internal_bytecode_hash,
410+
observable_bytecode_len,
411+
);
400412
}
401413
Err(SystemError::LeafRuntime(RuntimeError::OutOfErgs(_))) => {
402414
error_after_constructor = Some(EvmError::CodeStoreOutOfGas);

forward_system/src/system/tracers/call_tracer.rs

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Reference implementation of call tracer.
22

3-
use std::mem;
4-
53
use evm_interpreter::ERGS_PER_GAS;
64
use ruint::aliases::{B160, U256};
75
use zk_ee::system::{
@@ -17,7 +15,8 @@ use zk_ee::utils::Bytes32;
1715
pub enum CallType {
1816
#[default]
1917
Call,
20-
Constructor,
18+
Create,
19+
Create2,
2120
Delegate,
2221
Static,
2322
DelegateStatic,
@@ -28,19 +27,22 @@ pub enum CallType {
2827
Selfdestruct,
2928
}
3029

31-
impl From<CallModifier> for CallType {
32-
fn from(value: CallModifier) -> Self {
30+
impl CallType {
31+
fn from(value: CallModifier, is_create: &Option<CreateType>) -> Self {
3332
// Note: in our implementation Selfdestruct isn't actually implemented as a "call". But in traces it should be treated like one
3433
match value {
35-
CallModifier::Constructor => CallType::Constructor,
3634
CallModifier::NoModifier => CallType::Call,
3735
CallModifier::Delegate => CallType::Delegate,
3836
CallModifier::Static => CallType::Static,
3937
CallModifier::DelegateStatic => CallType::DelegateStatic,
4038
CallModifier::EVMCallcode => CallType::EVMCallcode,
4139
CallModifier::EVMCallcodeStatic => CallType::EVMCallcodeStatic,
42-
CallModifier::ZKVMSystem => CallType::ZKVMSystem, // Not used
43-
CallModifier::ZKVMSystemStatic => CallType::ZKVMSystemStatic, // Not used
40+
CallModifier::ZKVMSystem => CallType::ZKVMSystem,
41+
CallModifier::ZKVMSystemStatic => CallType::ZKVMSystemStatic,
42+
CallModifier::Constructor => match is_create.as_ref().expect("Should exist") {
43+
CreateType::Create => CallType::Create,
44+
CreateType::Create2 => CallType::Create2,
45+
},
4446
}
4547
}
4648
}
@@ -74,6 +76,12 @@ pub enum CallError {
7476
FatalError(String), // Some fatal internal error outside of EVM specification (ZKsync OS specific)
7577
}
7678

79+
#[derive(Debug)]
80+
pub enum CreateType {
81+
Create,
82+
Create2,
83+
}
84+
7785
#[derive(Default)]
7886
pub struct CallTracer {
7987
pub transactions: Vec<Call>,
@@ -82,6 +90,8 @@ pub struct CallTracer {
8290
pub current_call_depth: usize,
8391
pub collect_logs: bool,
8492
pub only_top_call: bool,
93+
94+
create_operation_requested: Option<CreateType>,
8595
}
8696

8797
impl CallTracer {
@@ -93,6 +103,7 @@ impl CallTracer {
93103
current_call_depth: 0,
94104
collect_logs,
95105
only_top_call,
106+
create_operation_requested: None,
96107
}
97108
}
98109
}
@@ -102,8 +113,21 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
102113
self.current_call_depth += 1;
103114

104115
if !self.only_top_call || self.current_call_depth == 1 {
116+
// Top-level deployment (initiated by EOA) won't trigger `on_create_request` hook
117+
// This is always a CREATE
118+
if self.current_call_depth == 1
119+
&& initial_state.external_call.modifier == CallModifier::Constructor
120+
{
121+
self.create_operation_requested = Some(CreateType::Create);
122+
}
123+
124+
let call_type = CallType::from(
125+
initial_state.external_call.modifier,
126+
&self.create_operation_requested,
127+
);
128+
105129
self.unfinished_calls.push(Call {
106-
call_type: CallType::from(initial_state.external_call.modifier),
130+
call_type,
107131
from: initial_state.external_call.caller,
108132
to: initial_state.external_call.callee,
109133
value: initial_state.external_call.nominal_token_value,
@@ -117,6 +141,11 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
117141
logs: vec![], // will be populated later
118142
})
119143
}
144+
145+
// Reset flag, required data is consumed
146+
if self.create_operation_requested.is_some() {
147+
self.create_operation_requested = None;
148+
}
120149
}
121150

122151
fn after_execution_frame_completed(&mut self, result: Option<(&S::Resources, &CallResult<S>)>) {
@@ -140,7 +169,14 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
140169
finished_call.output = return_values.returndata.to_vec();
141170
}
142171
zk_ee::system::CallResult::Successful { return_values } => {
143-
finished_call.output = return_values.returndata.to_vec();
172+
match finished_call.call_type {
173+
CallType::Create | CallType::Create2 => {
174+
// output should be already populated in `on_bytecode_change` hook
175+
}
176+
_ => {
177+
finished_call.output = return_values.returndata.to_vec();
178+
}
179+
}
144180
}
145181
};
146182
}
@@ -153,23 +189,36 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
153189
}
154190
}
155191

156-
finished_call.calls = mem::take(&mut self.finished_calls);
157-
158-
self.finished_calls.push(finished_call);
192+
if let Some(parent_call) = self.unfinished_calls.last_mut() {
193+
parent_call.calls.push(finished_call);
194+
} else {
195+
self.finished_calls.push(finished_call);
196+
}
159197
}
160198

161199
self.current_call_depth -= 1;
200+
201+
// Reset flag in case if frame terminated due to out-of-native / other internal ZKsync OS error
202+
if self.create_operation_requested.is_some() {
203+
self.create_operation_requested = None;
204+
}
162205
}
163206

164207
fn begin_tx(&mut self, _calldata: &[u8]) {
165208
self.current_call_depth = 0;
209+
210+
// Sanity check
211+
assert!(self.create_operation_requested.is_none());
166212
}
167213

168214
fn finish_tx(&mut self) {
169215
assert_eq!(self.current_call_depth, 0);
170216
assert!(self.unfinished_calls.is_empty());
171217
assert_eq!(self.finished_calls.len(), 1);
172218

219+
// Sanity check
220+
assert!(self.create_operation_requested.is_none());
221+
173222
self.transactions
174223
.push(self.finished_calls.pop().expect("Should exist"));
175224
}
@@ -196,7 +245,6 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
196245
) {
197246
}
198247

199-
#[inline(always)]
200248
fn on_event(
201249
&mut self,
202250
_ee_type: zk_ee::execution_environment_type::ExecutionEnvironmentType,
@@ -214,6 +262,35 @@ impl<S: EthereumLikeTypes> Tracer<S> for CallTracer {
214262
}
215263
}
216264

265+
/// Is called on a change of bytecode for some account.
266+
/// `new_bytecode` can be None if bytecode is unknown at the moment of change (e.g. force deploy by hash in system hook)
267+
fn on_bytecode_change(
268+
&mut self,
269+
_ee_type: zk_ee::execution_environment_type::ExecutionEnvironmentType,
270+
address: <S::IOTypes as SystemIOTypesConfig>::Address,
271+
new_bytecode: Option<&[u8]>,
272+
_new_bytecode_hash: <S::IOTypes as SystemIOTypesConfig>::BytecodeHashValue,
273+
new_observable_bytecode_length: u32,
274+
) {
275+
let call = self.unfinished_calls.last_mut().expect("Should exist");
276+
277+
match call.call_type {
278+
CallType::Create | CallType::Create2 => {
279+
assert_eq!(address, call.to);
280+
let deployed_raw_bytecode = new_bytecode.expect("Should be present");
281+
282+
assert!(deployed_raw_bytecode.len() >= new_observable_bytecode_length as usize);
283+
284+
// raw bytecode may include internal artifacts (jumptable), so we need to trim it
285+
call.output =
286+
deployed_raw_bytecode[..new_observable_bytecode_length as usize].to_vec();
287+
}
288+
_ => {
289+
// should not happen now (system hooks currently do not trigger this hook)
290+
}
291+
}
292+
}
293+
217294
#[inline(always)]
218295
fn evm_tracer(&mut self) -> &mut impl EvmTracer<S> {
219296
self
@@ -242,6 +319,11 @@ impl<S: EthereumLikeTypes> EvmTracer<S> for CallTracer {
242319
let current_call = self.unfinished_calls.last_mut().expect("Should exist");
243320
current_call.error = Some(CallError::EvmError(error.clone()));
244321
current_call.reverted = true;
322+
323+
// In case we fail after `on_create_request` hook, but before `on_new_execution_frame` hook
324+
if self.create_operation_requested.is_some() {
325+
self.create_operation_requested = None;
326+
}
245327
}
246328

247329
/// Special cases, when error happens in frame before any opcode is executed (unfortunately we can't provide access to state)
@@ -250,6 +332,8 @@ impl<S: EthereumLikeTypes> EvmTracer<S> for CallTracer {
250332
let current_call = self.unfinished_calls.last_mut().expect("Should exist");
251333
current_call.error = Some(CallError::EvmError(error.clone()));
252334
current_call.reverted = true;
335+
336+
assert!(self.create_operation_requested.is_none());
253337
}
254338

255339
/// We should treat selfdestruct as a special kind of a call
@@ -260,7 +344,7 @@ impl<S: EthereumLikeTypes> EvmTracer<S> for CallTracer {
260344
frame_state: &impl EvmFrameInterface<S>,
261345
) {
262346
// Following Geth implementation: https://github.com/ethereum/go-ethereum/blob/2dbb580f51b61d7ff78fceb44b06835827704110/core/vm/instructions.go#L894
263-
self.finished_calls.push(Call {
347+
let call_frame = Call {
264348
call_type: CallType::Selfdestruct,
265349
from: frame_state.address(),
266350
to: beneficiary,
@@ -273,6 +357,27 @@ impl<S: EthereumLikeTypes> EvmTracer<S> for CallTracer {
273357
reverted: false,
274358
calls: vec![],
275359
logs: vec![],
276-
})
360+
};
361+
362+
if let Some(parent_call) = self.unfinished_calls.last_mut() {
363+
parent_call.calls.push(call_frame);
364+
} else {
365+
self.finished_calls.push(call_frame);
366+
}
367+
}
368+
369+
/// Called on CREATE/CREATE2 system request.
370+
/// Hook is called *before* new execution frame is created.
371+
/// Note: CREATE/CREATE2 opcode execution can fail after this hook (and call on_opcode_error correspondingly)
372+
/// Note: top-level deployment won't trigger this hook
373+
fn on_create_request(&mut self, is_create2: bool) {
374+
// Can't be some - `on_new_execution_frame` or `on_opcode_error` should reset flag
375+
assert!(self.create_operation_requested.is_none());
376+
377+
self.create_operation_requested = if is_create2 {
378+
Some(CreateType::Create)
379+
} else {
380+
Some(CreateType::Create2)
381+
};
277382
}
278383
}

0 commit comments

Comments
 (0)