Skip to content

Commit

Permalink
EVM: return just the bytecode hash (#1015)
Browse files Browse the repository at this point in the history
* EVM: return just the bytecode hash

Previously, we returned a multihash. However, this got serialized as a map.

Now, we just return the raw digest encoded as 32 bytes.

* chore: state size changes

We've shaved quite a few bytes off by correctly serializing this.

* test_vm: fix hash method

We need to return the digest, not the encoded multihash. This only
affects the tests.
  • Loading branch information
Stebalien authored Jan 12, 2023
1 parent f881c9c commit 9191eb3
Show file tree
Hide file tree
Showing 29 changed files with 1,761 additions and 1,679 deletions.
28 changes: 6 additions & 22 deletions actors/evm/src/interpreter/instructions/ext.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
use crate::interpreter::instructions::memory::copy_to_memory;
use crate::interpreter::{address::EthAddress, precompiles::Precompiles};
use crate::U256;
use crate::{BytecodeHash, U256};
use cid::Cid;
use fil_actors_runtime::deserialize_block;
use fil_actors_runtime::runtime::builtins::Type;
use fil_actors_runtime::ActorError;
use fvm_ipld_blockstore::Blockstore;
use fvm_shared::crypto::hash::SupportedHashes;
use fvm_shared::sys::SendFlags;
use fvm_shared::{address::Address, econ::TokenAmount};
use multihash::Multihash;
use num_traits::Zero;
use {
crate::interpreter::{ExecutionState, StatusCode, System},
fil_actors_runtime::runtime::Runtime,
};

/// Keccak256 hash of `[0xfe]`, "native bytecode"
pub const NATIVE_BYTECODE_HASH: [u8; 32] =
hex_literal::hex!("bcc90f2d6dada5b18e155c17a1c0a55920aae94f39857d39d0d8ed07ae8f228b");

/// Keccak256 hash of `[]`, empty bytecode
pub const EMPTY_EVM_HASH: [u8; 32] =
hex_literal::hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470");

pub fn extcodesize(
_state: &mut ExecutionState,
system: &mut System<impl Runtime>,
Expand Down Expand Up @@ -52,34 +42,28 @@ pub fn extcodehash(
let addr = match get_contract_type(system.rt, addr) {
ContractType::EVM(a) => a,
// _Technically_ since we have native "bytecode" set as 0xfe this is valid, though we cant differentiate between different native actors.
ContractType::Native(_) => return Ok(NATIVE_BYTECODE_HASH.into()),
ContractType::Native(_) => return Ok(BytecodeHash::NATIVE_ACTOR.into()),
// Precompiles "exist" and therefore aren't empty (although spec-wise they can be either 0 or keccak("") ).
ContractType::Precompile => return Ok(EMPTY_EVM_HASH.into()),
ContractType::Precompile => return Ok(BytecodeHash::EMPTY.into()),
// NOTE: There may be accounts that in EVM would be considered "empty" (as defined in EIP-161) and give 0, but we will instead return keccak("").
// The FVM does not have chain state cleanup so contracts will never end up "empty" and be removed, they will either exist (in any state in the contract lifecycle)
// and return keccak(""), or not exist (where nothing has ever been deployed at that address) and return 0.
// TODO: With account abstraction, this may be something other than an empty hash!
ContractType::Account => return Ok(EMPTY_EVM_HASH.into()),
ContractType::Account => return Ok(BytecodeHash::EMPTY.into()),
// Not found
ContractType::NotFound => return Ok(U256::zero()),
};

// multihash { keccak256(bytecode) }
let bytecode_hash: Multihash = deserialize_block(system.send(
let bytecode_hash: BytecodeHash = deserialize_block(system.send(
&addr,
crate::Method::GetBytecodeHash as u64,
Default::default(),
TokenAmount::zero(),
None,
SendFlags::READ_ONLY,
)?)?;

let digest = bytecode_hash.digest();
debug_assert_eq!(SupportedHashes::Keccak256 as u64, bytecode_hash.code());

// Take the first 32 bytes of the Multihash
let digest_len = digest.len().min(32);
Ok(digest[..digest_len].into())
Ok(bytecode_hash.into())
}

pub fn extcodecopy(
Expand Down
15 changes: 7 additions & 8 deletions actors/evm/src/interpreter/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ use fvm_shared::{
sys::SendFlags,
MethodNum, IPLD_RAW,
};
use multihash::{Code, Multihash};
use multihash::Code;
use once_cell::unsync::OnceCell;

use crate::state::{State, Tombstone};
use crate::BytecodeHash;

use super::{address::EthAddress, Bytecode};

Expand Down Expand Up @@ -84,11 +85,11 @@ pub struct EvmBytecode {
/// CID of the contract
pub cid: Cid,
/// Keccak256 hash of the contract
pub evm_hash: Multihash,
pub evm_hash: BytecodeHash,
}

impl EvmBytecode {
fn new(cid: Cid, evm_hash: Multihash) -> Self {
fn new(cid: Cid, evm_hash: BytecodeHash) -> Self {
Self { cid, evm_hash }
}
}
Expand Down Expand Up @@ -318,11 +319,9 @@ impl<'r, RT: Runtime> System<'r, RT> {
));
}

let code_hash = multihash::Multihash::wrap(
SupportedHashes::Keccak256 as u64,
&self.rt.hash(SupportedHashes::Keccak256, bytecode),
)
.context_code(ExitCode::USR_ILLEGAL_STATE, "failed to hash bytecode with keccak")?;
let code_hash = self.rt.hash(SupportedHashes::Keccak256, bytecode)[..]
.try_into()
.context_code(ExitCode::USR_ASSERTION_FAILED, "expected a 32byte digest")?;

let cid = self
.rt
Expand Down
9 changes: 9 additions & 0 deletions actors/evm/src/interpreter/uints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use serde::{Deserialize, Serialize};
use substrate_bn::arith;

use crate::BytecodeHash;

use {
fvm_shared::bigint::BigInt, fvm_shared::econ::TokenAmount, std::cmp::Ordering, std::fmt,
uint::construct_uint,
Expand Down Expand Up @@ -109,6 +111,13 @@ impl From<&U256> for TokenAmount {
}
}

impl From<BytecodeHash> for U256 {
fn from(bytecode: BytecodeHash) -> Self {
let bytes: [u8; 32] = bytecode.into();
Self::from(bytes)
}
}

impl Serialize for U256 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand Down
11 changes: 4 additions & 7 deletions actors/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ use fil_actors_runtime::{actor_error, AsActorError, EAM_ACTOR_ADDR, INIT_ACTOR_A
use fvm_ipld_encoding::ipld_block::IpldBlock;
use fvm_ipld_encoding::{strict_bytes, BytesDe, BytesSer, DAG_CBOR};
use fvm_shared::address::Address;
use fvm_shared::crypto::hash::SupportedHashes;
use fvm_shared::error::ExitCode;
use interpreter::instructions::ext::EMPTY_EVM_HASH;
use interpreter::{address::EthAddress, system::load_bytecode};
use multihash::Multihash;

use crate::interpreter::output::Outcome;

Expand Down Expand Up @@ -240,14 +237,14 @@ impl EvmContractActor {
}
}

pub fn bytecode_hash(rt: &mut impl Runtime) -> Result<multihash::Multihash, ActorError> {
pub fn bytecode_hash(rt: &mut impl Runtime) -> Result<BytecodeHash, ActorError> {
// Any caller can fetch the bytecode hash of a contract; this is where EXTCODEHASH gets it's value for EVM contracts.
rt.validate_immediate_caller_accept_any()?;

// return value must be either keccak("") or keccak(bytecode)
let state: State = rt.state()?;
if is_dead(rt, &state) {
Ok(Multihash::wrap(SupportedHashes::Keccak256 as u64, &EMPTY_EVM_HASH).unwrap())
Ok(BytecodeHash::EMPTY)
} else {
Ok(state.bytecode_hash)
}
Expand Down Expand Up @@ -340,8 +337,8 @@ impl ActorCode for EvmContractActor {
Ok(IpldBlock::serialize_cbor(&ret)?)
}
Some(Method::GetBytecodeHash) => {
let multihash = Self::bytecode_hash(rt)?;
Ok(IpldBlock::serialize_cbor(&multihash)?)
let hash = Self::bytecode_hash(rt)?;
Ok(IpldBlock::serialize_cbor(&hash)?)
}
Some(Method::GetStorageAt) => {
let value = Self::storage_at(
Expand Down
101 changes: 100 additions & 1 deletion actors/evm/src/state.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::array::TryFromSliceError;

use fvm_shared::ActorID;

use {
cid::Cid,
fvm_ipld_encoding::strict_bytes,
fvm_ipld_encoding::tuple::*,
serde::{Deserialize, Serialize},
serde_tuple::{Deserialize_tuple, Serialize_tuple},
};

Expand All @@ -15,6 +19,71 @@ pub struct Tombstone {
pub nonce: u64,
}

/// A Keccak256 digest of EVM bytecode.
#[derive(Deserialize, Serialize, Clone, Copy, Eq, PartialEq)]
#[serde(transparent)]
pub struct BytecodeHash(#[serde(with = "strict_bytes")] [u8; 32]);

impl std::fmt::Debug for BytecodeHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("BytecodeHash").field(&format_args!("{}", self)).finish()
}
}

impl std::fmt::Display for BytecodeHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
write!(f, "0x")?;
}
for b in self.0 {
write!(f, "{b:02X}")?;
}
Ok(())
}
}

impl BytecodeHash {
pub const ZERO: Self = Self([0; 32]);

/// Keccak256 hash of `[0xfe]`, "native bytecode"
pub const NATIVE_ACTOR: Self =
Self(hex_literal::hex!("bcc90f2d6dada5b18e155c17a1c0a55920aae94f39857d39d0d8ed07ae8f228b"));

/// Keccak256 hash of `[]`, empty bytecode
pub const EMPTY: Self =
Self(hex_literal::hex!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"));

pub fn as_slice(&self) -> &[u8] {
&self.0
}
}

impl From<[u8; 32]> for BytecodeHash {
fn from(digest: [u8; 32]) -> Self {
BytecodeHash(digest)
}
}

impl From<BytecodeHash> for [u8; 32] {
fn from(digest: BytecodeHash) -> Self {
digest.0
}
}

impl From<BytecodeHash> for Vec<u8> {
fn from(digest: BytecodeHash) -> Self {
digest.0.into()
}
}

impl TryFrom<&[u8]> for BytecodeHash {
type Error = TryFromSliceError;

fn try_from(value: &[u8]) -> Result<Self, TryFromSliceError> {
Ok(Self(value.try_into()?))
}
}

/// Data stored by an EVM contract.
/// This runs on the fvm-evm-runtime actor code cid.
#[derive(Debug, Serialize_tuple, Deserialize_tuple)]
Expand All @@ -24,7 +93,7 @@ pub struct State {
pub bytecode: Cid,

/// The EVM contract bytecode hash keccak256(bytecode)
pub bytecode_hash: multihash::Multihash,
pub bytecode_hash: BytecodeHash,

/// The EVM contract state dictionary.
/// All eth contract state is a map of U256 -> U256 values.
Expand Down Expand Up @@ -54,3 +123,33 @@ pub struct State {
/// See https://github.com/filecoin-project/ref-fvm/issues/1174 for some context.
pub tombstone: Option<Tombstone>,
}

#[cfg(test)]
mod test {
use fvm_ipld_encoding::{from_slice, to_vec, BytesDe};

use crate::BytecodeHash;
#[test]
fn test_bytecode_hash_serde() {
let encoded = to_vec(&BytecodeHash::EMPTY).unwrap();
let BytesDe(decoded) = from_slice(&encoded).unwrap();
assert_eq!(BytecodeHash::try_from(&decoded[..]).unwrap(), BytecodeHash::EMPTY);
}

#[test]
fn test_bytecode_hash_format() {
assert_eq!(
BytecodeHash::ZERO.to_string(),
"0000000000000000000000000000000000000000000000000000000000000000"
);
assert_eq!(
format!("{:#}", BytecodeHash::ZERO),
"0x0000000000000000000000000000000000000000000000000000000000000000"
);

assert_eq!(
format!("{:?}", BytecodeHash::ZERO),
"BytecodeHash(0000000000000000000000000000000000000000000000000000000000000000)"
);
}
}
Loading

0 comments on commit 9191eb3

Please sign in to comment.