Skip to content

Commit

Permalink
feat: Adapt server for new EVM bytecode hash encoding (#3396)
Browse files Browse the repository at this point in the history
## What ❔

After latest EVM emulator contract changes, EVM bytecode doesn't include
any king of prefix with bytecode length. Actual unpadded raw EVM
bytecode length is encoded directly in versioned bytecode hash.

## Checklist

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

- [X] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [X] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.
- [X] Code has been formatted via `zkstack dev fmt` and `zkstack dev
lint`.
  • Loading branch information
0xVolosnikov authored Dec 19, 2024
1 parent 3ce7e8a commit 5a1e6d2
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 96 deletions.
97 changes: 67 additions & 30 deletions core/lib/basic_types/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
//! Both bytecode kinds are right-padded to consist of an integer, odd number of 32-byte words. All methods
//! in this module operate on padded bytecodes unless explicitly specified otherwise.
use std::iter;

use anyhow::Context as _;
use sha2::{Digest, Sha256};

Expand Down Expand Up @@ -68,21 +70,31 @@ pub struct BytecodeHash(H256);
impl BytecodeHash {
/// Hashes the provided EraVM bytecode.
pub fn for_bytecode(bytecode: &[u8]) -> Self {
Self::for_generic_bytecode(BytecodeMarker::EraVm, bytecode)
Self::for_generic_bytecode(BytecodeMarker::EraVm, bytecode, bytecode.len())
}

/// Hashes the provided padded EVM bytecode.
pub fn for_evm_bytecode(bytecode: &[u8]) -> Self {
Self::for_generic_bytecode(BytecodeMarker::Evm, bytecode)
pub fn for_evm_bytecode(raw_bytecode_len: usize, bytecode: &[u8]) -> Self {
Self::for_generic_bytecode(BytecodeMarker::Evm, bytecode, raw_bytecode_len)
}

/// Hashes the provided raw EVM bytecode.
pub fn for_raw_evm_bytecode(bytecode: &[u8]) -> Self {
let padded_evm_bytecode = pad_evm_bytecode(bytecode);
Self::for_evm_bytecode(bytecode.len(), &padded_evm_bytecode)
}

fn for_generic_bytecode(kind: BytecodeMarker, bytecode: &[u8]) -> Self {
fn for_generic_bytecode(
kind: BytecodeMarker,
bytecode: &[u8],
bytecode_len_in_bytes: usize,
) -> Self {
validate_bytecode(bytecode).expect("invalid bytecode");

let mut hasher = Sha256::new();
let len = match kind {
BytecodeMarker::EraVm => (bytecode.len() / 32) as u16,
BytecodeMarker::Evm => bytecode.len() as u16,
BytecodeMarker::EraVm => (bytecode_len_in_bytes / 32) as u16,
BytecodeMarker::Evm => bytecode_len_in_bytes as u16,
};
hasher.update(bytecode);
let result = hasher.finalize();
Expand Down Expand Up @@ -157,46 +169,65 @@ impl BytecodeMarker {
}

/// Removes padding from an EVM bytecode, returning the original EVM bytecode.
pub fn trim_padded_evm_bytecode(raw: &[u8]) -> anyhow::Result<&[u8]> {
pub fn trim_padded_evm_bytecode(bytecode_hash: BytecodeHash, raw: &[u8]) -> anyhow::Result<&[u8]> {
if bytecode_hash.marker() != BytecodeMarker::Evm {
anyhow::bail!("only EVM bytecode hashes allowed")
}
validate_bytecode(raw).context("bytecode fails basic validity checks")?;

// EVM bytecodes are prefixed with a big-endian `U256` bytecode length.
let bytecode_len_bytes = raw.get(..32).context("length < 32")?;
let bytecode_len = U256::from_big_endian(bytecode_len_bytes);
let bytecode_len: usize = bytecode_len
.try_into()
.map_err(|_| anyhow::anyhow!("length ({bytecode_len}) overflow"))?;
let bytecode = raw.get(32..(32 + bytecode_len)).with_context(|| {
// Actual raw unpadded EVM bytecode length is encoded in bytecode hash
let bytecode_len: usize = bytecode_hash.len_in_bytes();
let bytecode = raw.get(0..bytecode_len).with_context(|| {
format!(
"prefixed length ({bytecode_len}) exceeds real length ({})",
raw.len() - 32
"encoded length ({bytecode_len}) exceeds real length ({})",
raw.len()
)
})?;
// Since slicing above succeeded, this one is safe.
let padding = &raw[(32 + bytecode_len)..];
let padding = &raw[bytecode_len..];
anyhow::ensure!(
padding.iter().all(|&b| b == 0),
"bytecode padding contains non-zero bytes"
);
Ok(bytecode)
}

/// Pads an EVM bytecode in the same ways it's done by system contracts.
pub fn pad_evm_bytecode(deployed_bytecode: &[u8]) -> Vec<u8> {
let mut padded = Vec::with_capacity(deployed_bytecode.len());
padded.extend_from_slice(deployed_bytecode);

// Pad to the 32-byte word boundary.
if padded.len() % 32 != 0 {
padded.extend(iter::repeat(0).take(32 - padded.len() % 32));
}
assert_eq!(padded.len() % 32, 0);

// Pad to contain the odd number of words.
if (padded.len() / 32) % 2 != 1 {
padded.extend_from_slice(&[0; 32]);
}
assert_eq!((padded.len() / 32) % 2, 1);
padded
}

#[doc(hidden)] // only useful for tests
pub mod testonly {
use const_decoder::Decoder;

pub const RAW_EVM_BYTECODE: &[u8] = &const_decoder::decode!(
pub const PADDED_EVM_BYTECODE: &[u8] = &const_decoder::decode!(
Decoder::Hex,
b"00000000000000000000000000000000000000000000000000000000000001266080604052348015\
600e575f80fd5b50600436106030575f3560e01c8063816898ff146034578063fb5343f314604c57\
5b5f80fd5b604a60048036038101906046919060a6565b6066565b005b6052606f565b604051605d\
919060d9565b60405180910390f35b805f8190555050565b5f5481565b5f80fd5b5f819050919050\
565b6088816078565b81146091575f80fd5b50565b5f8135905060a0816081565b92915050565b5f\
6020828403121560b85760b76074565b5b5f60c3848285016094565b91505092915050565b60d381\
6078565b82525050565b5f60208201905060ea5f83018460cc565b9291505056fea2646970667358\
221220caca1247066da378f2ec77c310f2ae51576272367b4fa11cc4350af4e9ce4d0964736f6c63\
4300081a00330000000000000000000000000000000000000000000000000000"
b"6080604052348015600e575f80fd5b50600436106030575f3560e01c8063816898ff146034578063\
fb5343f314604c575b5f80fd5b604a60048036038101906046919060a6565b6066565b005b605260\
6f565b604051605d919060d9565b60405180910390f35b805f8190555050565b5f5481565b5f80fd\
5b5f819050919050565b6088816078565b81146091575f80fd5b50565b5f8135905060a081608156\
5b92915050565b5f6020828403121560b85760b76074565b5b5f60c3848285016094565b91505092\
915050565b60d3816078565b82525050565b5f60208201905060ea5f83018460cc565b9291505056\
fea2646970667358221220caca1247066da378f2ec77c310f2ae51576272367b4fa11cc4350af4e9\
ce4d0964736f6c634300081a00330000000000000000000000000000000000000000000000000000\
0000000000000000000000000000000000000000000000000000000000000000"
);

pub const PROCESSED_EVM_BYTECODE: &[u8] = &const_decoder::decode!(
Decoder::Hex,
b"6080604052348015600e575f80fd5b50600436106030575f3560e01c8063816898ff146034578063\
Expand All @@ -213,7 +244,7 @@ pub mod testonly {
#[cfg(test)]
mod tests {
use super::{
testonly::{PROCESSED_EVM_BYTECODE, RAW_EVM_BYTECODE},
testonly::{PADDED_EVM_BYTECODE, PROCESSED_EVM_BYTECODE},
*,
};

Expand All @@ -223,14 +254,20 @@ mod tests {
assert_eq!(bytecode_hash.marker(), BytecodeMarker::EraVm);
assert_eq!(bytecode_hash.len_in_bytes(), 32);

let bytecode_hash = BytecodeHash::for_evm_bytecode(&[0; 32]);
let bytecode_hash = BytecodeHash::for_raw_evm_bytecode(&[0; 32]);
assert_eq!(bytecode_hash.marker(), BytecodeMarker::Evm);
assert_eq!(bytecode_hash.len_in_bytes(), 32);

let bytecode_hash = BytecodeHash::for_evm_bytecode(32, &[0; 96]);
assert_eq!(bytecode_hash.marker(), BytecodeMarker::Evm);
assert_eq!(bytecode_hash.len_in_bytes(), 32);
}

#[test]
fn preparing_evm_bytecode() {
let prepared = trim_padded_evm_bytecode(RAW_EVM_BYTECODE).unwrap();
let bytecode_hash =
BytecodeHash::for_evm_bytecode(PROCESSED_EVM_BYTECODE.len(), &PADDED_EVM_BYTECODE);
let prepared = trim_padded_evm_bytecode(bytecode_hash, PADDED_EVM_BYTECODE).unwrap();
assert_eq!(prepared, PROCESSED_EVM_BYTECODE);
}
}
10 changes: 7 additions & 3 deletions core/lib/contract_verifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use tokio::time;
use zksync_dal::{contract_verification_dal::DeployedContractData, ConnectionPool, Core, CoreDal};
use zksync_queued_job_processor::{async_trait, JobProcessor};
use zksync_types::{
bytecode::{trim_padded_evm_bytecode, BytecodeMarker},
bytecode::{trim_padded_evm_bytecode, BytecodeHash, BytecodeMarker},
contract_verification_api::{
self as api, CompilationArtifacts, VerificationIncomingRequest, VerificationInfo,
VerificationRequest,
Expand Down Expand Up @@ -257,8 +257,12 @@ impl ContractVerifier {

let deployed_bytecode = match bytecode_marker {
BytecodeMarker::EraVm => deployed_contract.bytecode.as_slice(),
BytecodeMarker::Evm => trim_padded_evm_bytecode(&deployed_contract.bytecode)
.context("invalid stored EVM bytecode")?,
BytecodeMarker::Evm => trim_padded_evm_bytecode(
BytecodeHash::try_from(deployed_contract.bytecode_hash)
.context("Invalid bytecode hash")?,
&deployed_contract.bytecode,
)
.context("invalid stored EVM bytecode")?,
};

if artifacts.deployed_bytecode() != deployed_bytecode {
Expand Down
31 changes: 3 additions & 28 deletions core/lib/contract_verifier/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//! Tests for the contract verifier.
use std::{
collections::{HashMap, HashSet},
iter,
};
use std::collections::{HashMap, HashSet};

use test_casing::{test_casing, Product};
use tokio::sync::watch;
use zksync_dal::Connection;
use zksync_node_test_utils::{create_l1_batch, create_l2_block};
use zksync_types::{
address_to_h256,
bytecode::BytecodeHash,
bytecode::{pad_evm_bytecode, BytecodeHash},
contract_verification_api::{CompilerVersions, SourceCodeData, VerificationIncomingRequest},
get_code_key, get_known_code_key,
l2::L2Tx,
Expand Down Expand Up @@ -114,28 +111,6 @@ impl TestContract {
}
}

/// Pads an EVM bytecode in the same ways it's done by system contracts.
fn pad_evm_bytecode(deployed_bytecode: &[u8]) -> Vec<u8> {
let mut padded = Vec::with_capacity(deployed_bytecode.len() + 32);
let len = U256::from(deployed_bytecode.len());
padded.extend_from_slice(&[0; 32]);
len.to_big_endian(&mut padded);
padded.extend_from_slice(deployed_bytecode);

// Pad to the 32-byte word boundary.
if padded.len() % 32 != 0 {
padded.extend(iter::repeat(0).take(32 - padded.len() % 32));
}
assert_eq!(padded.len() % 32, 0);

// Pad to contain the odd number of words.
if (padded.len() / 32) % 2 != 1 {
padded.extend_from_slice(&[0; 32]);
}
assert_eq!((padded.len() / 32) % 2, 1);
padded
}

async fn mock_deployment(
storage: &mut Connection<'_, Core>,
address: Address,
Expand Down Expand Up @@ -163,7 +138,7 @@ async fn mock_evm_deployment(
factory_deps: vec![],
};
let bytecode = pad_evm_bytecode(deployed_bytecode);
let bytecode_hash = BytecodeHash::for_evm_bytecode(&bytecode).value();
let bytecode_hash = BytecodeHash::for_evm_bytecode(deployed_bytecode.len(), &bytecode).value();
mock_deployment_inner(storage, address, bytecode_hash, bytecode, deployment).await;
}

Expand Down
22 changes: 16 additions & 6 deletions core/lib/multivm/src/versions/testonly/evm_emulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ impl EvmTestBuilder {
let mut system_env = default_system_env();
if self.deploy_emulator {
let evm_bytecode: Vec<_> = (0..32).collect();
let evm_bytecode_hash = BytecodeHash::for_evm_bytecode(&evm_bytecode).value();
let evm_bytecode_hash =
BytecodeHash::for_evm_bytecode(evm_bytecode.len(), &evm_bytecode).value();
storage.set_value(
get_known_code_key(&evm_bytecode_hash),
H256::from_low_u64_be(1),
Expand Down Expand Up @@ -131,7 +132,8 @@ pub(crate) fn test_tracing_evm_contract_deployment<VM: TestedVm>() {

let args = [Token::Bytes((0..32).collect())];
let evm_bytecode = ethabi::encode(&args);
let expected_bytecode_hash = BytecodeHash::for_evm_bytecode(&evm_bytecode).value();
let expected_bytecode_hash =
BytecodeHash::for_evm_bytecode(evm_bytecode.len(), &evm_bytecode).value();
let execute = Execute::for_deploy(expected_bytecode_hash, vec![0; 32], &args);
let deploy_tx = account.get_l2_tx_for_execute(execute, None);
let (_, vm_result) = vm
Expand All @@ -148,7 +150,8 @@ pub(crate) fn test_tracing_evm_contract_deployment<VM: TestedVm>() {
// "Deploy" a bytecode in another transaction and check that the first tx doesn't interfere with the returned `dynamic_factory_deps`.
let args = [Token::Bytes((0..32).rev().collect())];
let evm_bytecode = ethabi::encode(&args);
let expected_bytecode_hash = BytecodeHash::for_evm_bytecode(&evm_bytecode).value();
let expected_bytecode_hash =
BytecodeHash::for_evm_bytecode(evm_bytecode.len(), &evm_bytecode).value();
let execute = Execute::for_deploy(expected_bytecode_hash, vec![0; 32], &args);
let deploy_tx = account.get_l2_tx_for_execute(execute, None);
let (_, vm_result) = vm
Expand Down Expand Up @@ -324,7 +327,8 @@ pub(crate) fn test_mock_emulator_with_deployment<VM: TestedVm>(revert: bool) {

let mock_emulator_abi = &TestContract::mock_evm_emulator().abi;
let new_evm_bytecode = vec![0xfe; 96];
let new_evm_bytecode_hash = BytecodeHash::for_evm_bytecode(&new_evm_bytecode).value();
let new_evm_bytecode_hash =
BytecodeHash::for_evm_bytecode(new_evm_bytecode.len(), &new_evm_bytecode).value();

let test_fn = mock_emulator_abi.function("testDeploymentAndCall").unwrap();
let test_tx = account.get_l2_tx_for_execute(
Expand Down Expand Up @@ -402,7 +406,10 @@ pub(crate) fn test_mock_emulator_with_recursive_deployment<VM: TestedVm>() {
let bytecodes: HashMap<_, _> = (0_u8..10)
.map(|byte| {
let bytecode = vec![byte; 32];
(BytecodeHash::for_evm_bytecode(&bytecode).value(), bytecode)
(
BytecodeHash::for_evm_bytecode(bytecode.len(), &bytecode).value(),
bytecode,
)
})
.collect();
let test_fn = mock_emulator_abi
Expand Down Expand Up @@ -448,7 +455,10 @@ fn test_mock_emulator_with_partial_reverts_and_rng<VM: TestedVm>(rng: &mut impl
let all_bytecodes: HashMap<_, _> = (0_u8..10)
.map(|_| {
let bytecode = vec![rng.gen(); 32];
(BytecodeHash::for_evm_bytecode(&bytecode).value(), bytecode)
(
BytecodeHash::for_evm_bytecode(bytecode.len(), &bytecode).value(),
bytecode,
)
})
.collect();
let should_revert: Vec<_> = (0..10).map(|_| rng.gen::<bool>()).collect();
Expand Down
29 changes: 22 additions & 7 deletions core/lib/multivm/src/versions/vm_fast/tracers/evm_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ pub(super) struct EvmDeployTracer {

impl EvmDeployTracer {
pub(super) fn new(bytecodes: DynamicBytecodes) -> Self {
let tracked_signature =
ethabi::short_signature("publishEVMBytecode", &[ethabi::ParamType::Bytes]);
let tracked_signature = ethabi::short_signature(
"publishEVMBytecode",
&[ethabi::ParamType::Uint(256), ethabi::ParamType::Bytes],
);
Self {
tracked_signature,
bytecodes,
Expand All @@ -61,13 +63,26 @@ impl EvmDeployTracer {
return;
}

match ethabi::decode(&[ethabi::ParamType::Bytes], data) {
match ethabi::decode(
&[ethabi::ParamType::Uint(256), ethabi::ParamType::Bytes],
data,
) {
Ok(decoded) => {
// `unwrap`s should be safe since the function signature is checked above.
let published_bytecode = decoded.into_iter().next().unwrap().into_bytes().unwrap();
let bytecode_hash =
BytecodeHash::for_evm_bytecode(&published_bytecode).value_u256();
self.bytecodes.insert(bytecode_hash, published_bytecode);
let mut decoded_iter = decoded.into_iter();
let raw_bytecode_len = decoded_iter.next().unwrap().into_uint().unwrap().try_into();
match raw_bytecode_len {
Ok(raw_bytecode_len) => {
let published_bytecode = decoded_iter.next().unwrap().into_bytes().unwrap();
let bytecode_hash =
BytecodeHash::for_evm_bytecode(raw_bytecode_len, &published_bytecode)
.value_u256();
self.bytecodes.insert(bytecode_hash, published_bytecode);
}
Err(err) => {
tracing::error!("Invalid bytecode len in `publishEVMBytecode` call: {err}")
}
}
}
Err(err) => tracing::error!("Unable to decode `publishEVMBytecode` call: {err}"),
}
Expand Down
Loading

0 comments on commit 5a1e6d2

Please sign in to comment.