From 9046c1ca93f5367f5c2f7e411417ac75b78b87d0 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Tue, 14 May 2024 18:54:42 +0800 Subject: [PATCH] feat(rpc): implement Filecoin.EthGetCode (#4317) Co-authored-by: David Himmelstrup --- src/rpc/methods/eth.rs | 202 ++++++++++++++------------------ src/rpc/methods/eth/types.rs | 155 ++++++++++++++++++++++++ src/tool/subcommands/api_cmd.rs | 21 +++- 3 files changed, 262 insertions(+), 116 deletions(-) create mode 100644 src/rpc/methods/eth/types.rs diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index f03a38df5341..8120407fd2cf 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -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}; @@ -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}; @@ -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); @@ -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 { - 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 { - match addr.protocol() { - Protocol::ID => Ok(Self::from_actor_id(addr.id()?)), - Protocol::Delegated => { - let payload = addr.payload(); - let result: Result = 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 { - Ok(Address( - ethereum_types::Address::from_str(s).map_err(|e| anyhow::anyhow!("{e}"))?, - )) - } -} - -fn cast_eth_addr(bytes: &[u8]) -> Result
{ - 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); @@ -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, @@ -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
")] + pub from: EthAddress, + #[schemars(with = "Option")] #[serde( with = "crate::lotus_json", skip_serializing_if = "Option::is_none", default )] - pub to: Option
, + pub to: Option, pub value: BigInt, pub r#type: Uint64, pub input: Bytes, @@ -476,7 +386,7 @@ lotus_json_with_self!(Tx); struct TxArgs { pub chain_id: u64, pub nonce: u64, - pub to: Option
, + pub to: Option, pub value: BigInt, pub max_fee_per_gas: BigInt, pub max_priority_fee_per_gas: BigInt, @@ -531,7 +441,7 @@ fn format_bigint(value: &BigInt) -> Result { }) } -fn format_address(value: &Option
) -> BytesMut { +fn format_address(value: &Option) -> BytesMut { if let Some(addr) = value { addr.0.as_bytes().into() } else { @@ -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( @@ -897,7 +807,7 @@ fn eth_tx_args_from_unsigned_eth_message(msg: &Message) -> Result { 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!( @@ -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), @@ -995,9 +905,9 @@ fn eth_tx_from_signed_eth_message(smsg: &SignedMessage, chain_id: u32) -> Result fn lookup_eth_address( addr: &FilecoinAddress, state: &StateTree, -) -> Result> { +) -> Result> { // 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)); } @@ -1013,7 +923,7 @@ fn lookup_eth_address( 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)); @@ -1030,7 +940,7 @@ fn lookup_eth_address( } // Otherwise, use the masked address. - Ok(Some(Address::from_actor_id(id_addr))) + Ok(Some(EthAddress::from_actor_id(id_addr))) } /// See @@ -1133,7 +1043,7 @@ fn eth_tx_from_native_message( // 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) => { @@ -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, + (eth_address, block_number_or_hash): Self::Params, + ) -> Result { + let ts = tipset_by_block_number_or_hash(&ctx.chain_store, block_number_or_hash)?; + let to_address = FilecoinAddress::try_from(ð_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::*; @@ -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(), )), @@ -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) } @@ -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(ð_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(); @@ -1593,7 +1565,7 @@ mod test { .unwrap() .to .unwrap(), - Address(H160::from_str("0xa251031ed6b4779e2a0b913683e71043d88002a3").unwrap()) + EthAddress(H160::from_str("0xa251031ed6b4779e2a0b913683e71043d88002a3").unwrap()) ); } } diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs new file mode 100644 index 000000000000..757a9ef76d2b --- /dev/null +++ b/src/rpc/methods/eth/types.rs @@ -0,0 +1,155 @@ +// Copyright 2019-2024 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +pub const METHOD_GET_BYTE_CODE: u64 = 3; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetBytecodeReturn(pub Option); + +#[derive(PartialEq, Debug, Deserialize, Serialize, Default, Clone, JsonSchema)] +pub struct EthAddress( + #[schemars(with = "String")] + #[serde(with = "crate::lotus_json::hexify_bytes")] + pub ethereum_types::Address, +); + +lotus_json_with_self!(EthAddress); + +impl EthAddress { + pub fn to_filecoin_address(&self) -> anyhow::Result { + 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) -> anyhow::Result { + match addr.protocol() { + Protocol::ID => Ok(Self::from_actor_id(addr.id()?)), + Protocol::Delegated => { + let payload = addr.payload(); + let result: Result = 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: EthAddress = f4_addr.subaddress().try_into()?; + if eth_addr.is_masked_id() { + bail!( + "f410f addresses cannot embed masked-ID payloads: {}", + eth_addr.0 + ); + } + return Ok(eth_addr); + } + bail!("invalid delegated address namespace in: {addr}") + } + _ => { + bail!("invalid address {addr}"); + } + } + } + + pub fn is_masked_id(&self) -> bool { + self.0.as_bytes().starts_with(&MASKED_ID_PREFIX) + } + + pub 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 EthAddress { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(EthAddress( + ethereum_types::Address::from_str(s).map_err(|e| anyhow::anyhow!("{e}"))?, + )) + } +} + +impl TryFrom<&[u8]> for EthAddress { + type Error = anyhow::Error; + + fn try_from(value: &[u8]) -> Result { + if value.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(value); + Ok(EthAddress(payload)) + } +} + +impl TryFrom<&FilecoinAddress> for EthAddress { + type Error = anyhow::Error; + + fn try_from(value: &FilecoinAddress) -> Result { + Self::from_filecoin_address(value) + } +} + +impl TryFrom for EthAddress { + type Error = anyhow::Error; + + fn try_from(value: FilecoinAddress) -> Result { + Self::from_filecoin_address(&value) + } +} + +impl TryFrom<&EthAddress> for FilecoinAddress { + type Error = anyhow::Error; + + fn try_from(value: &EthAddress) -> Result { + value.to_filecoin_address() + } +} + +impl TryFrom for FilecoinAddress { + type Error = anyhow::Error; + + fn try_from(value: EthAddress) -> Result { + value.to_filecoin_address() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_bytecode_return_roundtrip() { + let bytes = hex::decode("d82a5827000155a0e40220fa0b7a54007ba2e76d5818b6e60793fb0b8bdbe177995e1b20dcfb6873d69779").unwrap(); + let des: GetBytecodeReturn = fvm_ipld_encoding::from_slice(&bytes).unwrap(); + assert_eq!( + des.0.unwrap().to_string(), + "bafk2bzaced5aw6suab52fz3nlamlnzqhsp5qxc634f3zsxq3edopw2dt22lxs" + ); + let ser = fvm_ipld_encoding::to_vec(&des).unwrap(); + assert_eq!(ser, bytes); + } +} diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 31c7200e7881..f0dbb34dc6c4 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -14,7 +14,7 @@ use crate::message::{Message as _, SignedMessage}; use crate::message_pool::{MessagePool, MpoolRpcProvider}; use crate::networks::{parse_bootstrap_peers, ChainConfig, NetworkChain}; use crate::rpc::beacon::BeaconGetEntry; -use crate::rpc::eth::Address as EthAddress; +use crate::rpc::eth::types::EthAddress; use crate::rpc::gas::GasEstimateGasLimit; use crate::rpc::miner::BlockTemplate; use crate::rpc::types::{ApiTipsetKey, MessageFilter, MessageLookup, SectorOnChainInfo}; @@ -1034,6 +1034,25 @@ fn eth_tests_with_tipset(shared_tipset: &Tipset) -> Vec { RpcTest::identity( EthGetBlockTransactionCountByNumber::request((Int64(shared_tipset.epoch()),)).unwrap(), ), + RpcTest::identity( + EthGetCode::request(( + // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq + EthAddress::from_str("0x7B90337f65fAA2B2B8ed583ba1Ba6EB0C9D7eA44").unwrap(), + BlockNumberOrHash::BlockNumber(shared_tipset.epoch()), + )) + .unwrap(), + ), + RpcTest::identity( + EthGetCode::request(( + // https://filfox.info/en/address/f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq + Address::from_str("f410fpoidg73f7krlfohnla52dotowde5p2sejxnd4mq") + .unwrap() + .try_into() + .unwrap(), + BlockNumberOrHash::BlockNumber(shared_tipset.epoch()), + )) + .unwrap(), + ), ] }