Skip to content

Commit

Permalink
feat(rpc): implement Filecoin.EthGetCode (#4317)
Browse files Browse the repository at this point in the history
Co-authored-by: David Himmelstrup <[email protected]>
  • Loading branch information
hanabi1224 and lemmih authored May 14, 2024
1 parent 38f99d7 commit 9046c1c
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 116 deletions.
202 changes: 87 additions & 115 deletions src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright 2019-2024 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod types;

use self::types::*;
use super::gas;
use crate::blocks::{Tipset, TipsetKey};
use crate::chain::{index::ResolveNullTipset, ChainStore};
Expand All @@ -19,7 +22,7 @@ use crate::shim::fvm_shared_latest::address::{Address as VmAddress, DelegatedAdd
use crate::shim::fvm_shared_latest::MethodNum;
use crate::shim::message::Message;
use crate::shim::{clock::ChainEpoch, state_tree::StateTree};

use crate::utils::db::BlockstoreExt as _;
use anyhow::{bail, Result};
use bytes::{Buf, BytesMut};
use cbor4ii::core::{dec::Decode, utils::SliceReader, Value};
Expand Down Expand Up @@ -47,6 +50,7 @@ macro_rules! for_each_method {
$callback!(crate::rpc::eth::EthAccounts);
$callback!(crate::rpc::eth::EthBlockNumber);
$callback!(crate::rpc::eth::EthChainId);
$callback!(crate::rpc::eth::EthGetCode);
$callback!(crate::rpc::eth::EthGasPrice);
$callback!(crate::rpc::eth::EthGetBalance);
$callback!(crate::rpc::eth::EthGetBlockByNumber);
Expand Down Expand Up @@ -165,100 +169,6 @@ pub struct Bytes(

lotus_json_with_self!(Bytes);

#[derive(PartialEq, Debug, Deserialize, Serialize, Default, Clone, JsonSchema)]
pub struct Address(
#[schemars(with = "String")]
#[serde(with = "crate::lotus_json::hexify_bytes")]
pub ethereum_types::Address,
);

lotus_json_with_self!(Address);

impl Address {
pub fn to_filecoin_address(&self) -> Result<FilecoinAddress, anyhow::Error> {
if self.is_masked_id() {
const PREFIX_LEN: usize = MASKED_ID_PREFIX.len();
// This is a masked ID address.
let arr = self.0.as_fixed_bytes();
let mut bytes = [0; 8];
bytes.copy_from_slice(&arr[PREFIX_LEN..]);
Ok(FilecoinAddress::new_id(u64::from_be_bytes(bytes)))
} else {
// Otherwise, translate the address into an address controlled by the
// Ethereum Address Manager.
Ok(FilecoinAddress::new_delegated(
FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id()?,
self.0.as_bytes(),
)?)
}
}

// See https://github.com/filecoin-project/lotus/blob/v1.26.2/chain/types/ethtypes/eth_types.go#L347-L375 for reference implementation
pub fn from_filecoin_address(addr: &FilecoinAddress) -> Result<Self> {
match addr.protocol() {
Protocol::ID => Ok(Self::from_actor_id(addr.id()?)),
Protocol::Delegated => {
let payload = addr.payload();
let result: Result<DelegatedAddress, _> = payload.try_into();
if let Ok(f4_addr) = result {
let namespace = f4_addr.namespace();
if namespace != FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id()? {
bail!("invalid address {addr}");
}
let eth_addr = cast_eth_addr(f4_addr.subaddress())?;
if eth_addr.is_masked_id() {
bail!(
"f410f addresses cannot embed masked-ID payloads: {}",
eth_addr.0
);
}
Ok(eth_addr)
} else {
bail!("invalid delegated address namespace in: {addr}")
}
}
_ => {
bail!("invalid address {addr}");
}
}
}

fn is_masked_id(&self) -> bool {
self.0.as_bytes().starts_with(&MASKED_ID_PREFIX)
}

fn from_actor_id(id: u64) -> Self {
let pfx = MASKED_ID_PREFIX;
let arr = id.to_be_bytes();
let payload = [
pfx[0], pfx[1], pfx[2], pfx[3], pfx[4], pfx[5], pfx[6], pfx[7], //
pfx[8], pfx[9], pfx[10], pfx[11], //
arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7],
];

Self(ethereum_types::H160(payload))
}
}

impl FromStr for Address {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Address(
ethereum_types::Address::from_str(s).map_err(|e| anyhow::anyhow!("{e}"))?,
))
}
}

fn cast_eth_addr(bytes: &[u8]) -> Result<Address> {
if bytes.len() != ADDRESS_LENGTH {
bail!("cannot parse bytes into an Ethereum address: incorrect input length")
}
let mut payload = ethereum_types::H160::default();
payload.as_bytes_mut().copy_from_slice(bytes);
Ok(Address(payload))
}

#[derive(PartialEq, Debug, Deserialize, Serialize, Default, Clone, JsonSchema)]
pub struct Hash(#[schemars(with = "String")] pub ethereum_types::H256);

Expand Down Expand Up @@ -395,7 +305,7 @@ pub struct Block {
pub hash: Hash,
pub parent_hash: Hash,
pub sha3_uncles: Hash,
pub miner: Address,
pub miner: EthAddress,
pub state_root: Hash,
pub transactions_root: Hash,
pub receipts_root: Hash,
Expand Down Expand Up @@ -443,14 +353,14 @@ pub struct Tx {
pub block_hash: Hash,
pub block_number: Uint64,
pub transaction_index: Uint64,
pub from: Address,
#[schemars(with = "Option<Address>")]
pub from: EthAddress,
#[schemars(with = "Option<EthAddress>")]
#[serde(
with = "crate::lotus_json",
skip_serializing_if = "Option::is_none",
default
)]
pub to: Option<Address>,
pub to: Option<EthAddress>,
pub value: BigInt,
pub r#type: Uint64,
pub input: Bytes,
Expand All @@ -476,7 +386,7 @@ lotus_json_with_self!(Tx);
struct TxArgs {
pub chain_id: u64,
pub nonce: u64,
pub to: Option<Address>,
pub to: Option<EthAddress>,
pub value: BigInt,
pub max_fee_per_gas: BigInt,
pub max_priority_fee_per_gas: BigInt,
Expand Down Expand Up @@ -531,7 +441,7 @@ fn format_bigint(value: &BigInt) -> Result<BytesMut> {
})
}

fn format_address(value: &Option<Address>) -> BytesMut {
fn format_address(value: &Option<EthAddress>) -> BytesMut {
if let Some(addr) = value {
addr.0.as_bytes().into()
} else {
Expand Down Expand Up @@ -781,7 +691,7 @@ impl RpcMethod<2> for EthGetBalance {
const API_VERSION: ApiVersion = ApiVersion::V1;
const PERMISSION: Permission = Permission::Read;

type Params = (Address, BlockNumberOrHash);
type Params = (EthAddress, BlockNumberOrHash);
type Ok = BigInt;

async fn handle(
Expand Down Expand Up @@ -897,7 +807,7 @@ fn eth_tx_args_from_unsigned_eth_message(msg: &Message) -> Result<TxArgs> {
bail!("unsupported EAM method");
}
} else if msg.method_num() == EVMMethod::InvokeContract as u64 {
let addr = Address::from_filecoin_address(&msg.to)?;
let addr = EthAddress::from_filecoin_address(&msg.to)?;
to = Some(addr);
} else {
bail!(
Expand Down Expand Up @@ -971,7 +881,7 @@ fn eth_tx_from_signed_eth_message(smsg: &SignedMessage, chain_id: u32) -> Result

// This should be impossible to fail as we've already asserted that we have an
// Ethereum Address sender...
let from = Address::from_filecoin_address(&from)?;
let from = EthAddress::from_filecoin_address(&from)?;

Ok(Tx {
nonce: Uint64(tx_args.nonce),
Expand All @@ -995,9 +905,9 @@ fn eth_tx_from_signed_eth_message(smsg: &SignedMessage, chain_id: u32) -> Result
fn lookup_eth_address<DB: Blockstore>(
addr: &FilecoinAddress,
state: &StateTree<DB>,
) -> Result<Option<Address>> {
) -> Result<Option<EthAddress>> {
// Attempt to convert directly, if it's an f4 address.
if let Ok(eth_addr) = Address::from_filecoin_address(addr) {
if let Ok(eth_addr) = EthAddress::from_filecoin_address(addr) {
if !eth_addr.is_masked_id() {
return Ok(Some(eth_addr));
}
Expand All @@ -1013,7 +923,7 @@ fn lookup_eth_address<DB: Blockstore>(
let result = state.get_actor(addr);
if let Ok(Some(actor_state)) = result {
if let Some(addr) = actor_state.delegated_address {
if let Ok(eth_addr) = Address::from_filecoin_address(&addr.into()) {
if let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into()) {
if !eth_addr.is_masked_id() {
// Conversable into an eth address, use it.
return Ok(Some(eth_addr));
Expand All @@ -1030,7 +940,7 @@ fn lookup_eth_address<DB: Blockstore>(
}

// Otherwise, use the masked address.
Ok(Some(Address::from_actor_id(id_addr)))
Ok(Some(EthAddress::from_actor_id(id_addr)))
}

/// See <https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector-and-argument-encoding>
Expand Down Expand Up @@ -1133,7 +1043,7 @@ fn eth_tx_from_native_message<DB: Blockstore>(
// known sentinel address.
let mut to = match lookup_eth_address(&msg.to(), state) {
Ok(Some(addr)) => Some(addr),
Ok(None) => Some(Address(
Ok(None) => Some(EthAddress(
ethereum_types::H160::from_str(REVERTED_ETH_ADDRESS).unwrap(),
)),
Err(err) => {
Expand Down Expand Up @@ -1373,6 +1283,68 @@ impl RpcMethod<0> for EthSyncing {
}
}

pub enum EthGetCode {}
impl RpcMethod<2> for EthGetCode {
const NAME: &'static str = "Filecoin.EthGetCode";
const PARAM_NAMES: [&'static str; 2] = ["eth_address", "block_number_or_hash"];
const API_VERSION: ApiVersion = ApiVersion::V1;
const PERMISSION: Permission = Permission::Read;

type Params = (EthAddress, BlockNumberOrHash);
type Ok = Bytes;

async fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(eth_address, block_number_or_hash): Self::Params,
) -> Result<Self::Ok, ServerError> {
let ts = tipset_by_block_number_or_hash(&ctx.chain_store, block_number_or_hash)?;
let to_address = FilecoinAddress::try_from(&eth_address)?;
let actor = ctx
.state_manager
.get_required_actor(&to_address, *ts.parent_state())?;
// Not a contract. We could try to distinguish between accounts and "native" contracts here,
// but it's not worth it.
if !fil_actor_interface::is_evm_actor(&actor.code) {
return Ok(Default::default());
}

let message = Message {
from: FilecoinAddress::SYSTEM_ACTOR,
to: to_address,
method_num: METHOD_GET_BYTE_CODE,
gas_limit: BLOCK_GAS_LIMIT,
..Default::default()
};
let mut api_invoc_result = None;
for ts in ts.chain_arc(ctx.store()) {
match ctx.state_manager.call(&message, Some(ts)) {
Ok(res) => {
api_invoc_result = Some(res);
break;
}
Err(e) => tracing::warn!(%e),
}
}
let Some(api_invoc_result) = api_invoc_result else {
return Err(anyhow::anyhow!("no message receipt").into());
};
let Some(msg_rct) = api_invoc_result.msg_rct else {
return Err(anyhow::anyhow!("no message receipt").into());
};
if !api_invoc_result.error.is_empty() {
return Err(anyhow::anyhow!("GetBytecode failed: {}", api_invoc_result.error).into());
}

let get_bytecode_return: GetBytecodeReturn =
fvm_ipld_encoding::from_slice(msg_rct.return_data().as_slice())?;
if let Some(cid) = get_bytecode_return.0 {
Ok(Bytes(ctx.store().get_required(&cid)?))
} else {
Ok(Default::default())
}
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -1435,7 +1407,7 @@ mod test {
let eth_tx_args = TxArgs {
chain_id: 314159,
nonce: 486,
to: Some(Address(
to: Some(EthAddress(
ethereum_types::H160::from_str("0xeb4a9cdb9f42d3a503d580a39b6e3736eb21fffd")
.unwrap(),
)),
Expand Down Expand Up @@ -1519,7 +1491,7 @@ mod test {
let addr = FilecoinAddress::new_id(id);

// roundtrip
let eth_addr = Address::from_filecoin_address(&addr).unwrap();
let eth_addr = EthAddress::from_filecoin_address(&addr).unwrap();
let fil_addr = eth_addr.to_filecoin_address().unwrap();
assert_eq!(addr, fil_addr)
}
Expand All @@ -1534,19 +1506,19 @@ mod test {
];

for addr in test_cases {
let eth_addr: Address = serde_json::from_str(addr).unwrap();
let eth_addr: EthAddress = serde_json::from_str(addr).unwrap();

let encoded = serde_json::to_string(&eth_addr).unwrap();
assert_eq!(encoded, addr.to_lowercase());

let decoded: Address = serde_json::from_str(&encoded).unwrap();
let decoded: EthAddress = serde_json::from_str(&encoded).unwrap();
assert_eq!(eth_addr, decoded);
}
}

#[quickcheck]
fn test_fil_address_roundtrip(addr: FilecoinAddress) {
if let Ok(eth_addr) = Address::from_filecoin_address(&addr) {
if let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr) {
let fil_addr = eth_addr.to_filecoin_address().unwrap();

let protocol = addr.protocol();
Expand Down Expand Up @@ -1593,7 +1565,7 @@ mod test {
.unwrap()
.to
.unwrap(),
Address(H160::from_str("0xa251031ed6b4779e2a0b913683e71043d88002a3").unwrap())
EthAddress(H160::from_str("0xa251031ed6b4779e2a0b913683e71043d88002a3").unwrap())
);
}
}
Loading

0 comments on commit 9046c1c

Please sign in to comment.