From 4f4efccab90638b63d2337290dba1d7d62007afd Mon Sep 17 00:00:00 2001 From: pgherveou Date: Mon, 30 Sep 2024 16:48:12 +0200 Subject: [PATCH] Add RPC integration --- Cargo.lock | 73 +- substrate/bin/node/runtime/src/lib.rs | 28 +- substrate/frame/revive/Cargo.toml | 24 +- substrate/frame/revive/src/address.rs | 4 +- substrate/frame/revive/src/evm.rs | 6 + substrate/frame/revive/src/evm/api.rs | 21 + substrate/frame/revive/src/evm/api/byte.rs | 132 ++++ .../frame/revive/src/evm/api/rlp_codec.rs | 205 ++++++ .../revive/src/evm/api/rpc_methods_gen.rs | 136 ++++ .../frame/revive/src/evm/api/rpc_types.rs | 16 + .../frame/revive/src/evm/api/rpc_types_gen.rs | 634 ++++++++++++++++++ .../frame/revive/src/evm/api/signature.rs | 67 ++ substrate/frame/revive/src/evm/api/type_id.rs | 70 ++ substrate/frame/revive/src/evm/runtime.rs | 269 ++++++++ substrate/frame/revive/src/exec.rs | 33 +- substrate/frame/revive/src/lib.rs | 139 +++- substrate/frame/revive/src/primitives.rs | 54 +- .../frame/revive/src/test_utils/builder.rs | 9 +- substrate/frame/revive/src/wasm/mod.rs | 5 +- umbrella/Cargo.toml | 1 - 20 files changed, 1884 insertions(+), 42 deletions(-) create mode 100644 substrate/frame/revive/src/evm.rs create mode 100644 substrate/frame/revive/src/evm/api.rs create mode 100644 substrate/frame/revive/src/evm/api/byte.rs create mode 100644 substrate/frame/revive/src/evm/api/rlp_codec.rs create mode 100644 substrate/frame/revive/src/evm/api/rpc_methods_gen.rs create mode 100644 substrate/frame/revive/src/evm/api/rpc_types.rs create mode 100644 substrate/frame/revive/src/evm/api/rpc_types_gen.rs create mode 100644 substrate/frame/revive/src/evm/api/signature.rs create mode 100644 substrate/frame/revive/src/evm/api/type_id.rs create mode 100644 substrate/frame/revive/src/evm/runtime.rs diff --git a/Cargo.lock b/Cargo.lock index 97bdc9351357..c9263036d5a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6789,6 +6789,10 @@ name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] [[package]] name = "futures-util" @@ -6924,6 +6928,27 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 1.1.0", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-timers" version = "0.2.6" @@ -6936,6 +6961,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glutton-westend-runtime" version = "3.0.0" @@ -8014,11 +8052,13 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ec465b607a36dc5dd45d48b7689bc83f679f66a3ac6b6b21cc787a11e0f8685" dependencies = [ + "jsonrpsee-client-transport 0.24.3", "jsonrpsee-core 0.24.3", "jsonrpsee-http-client 0.24.3", "jsonrpsee-proc-macros", "jsonrpsee-server", "jsonrpsee-types 0.24.3", + "jsonrpsee-wasm-client", "jsonrpsee-ws-client 0.24.3", "tokio", "tracing", @@ -8075,7 +8115,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90f0977f9c15694371b8024c35ab58ca043dbbf4b51ccb03db8858a021241df1" dependencies = [ "base64 0.22.1", + "futures-channel", "futures-util", + "gloo-net", "http 1.1.0", "jsonrpsee-core 0.24.3", "pin-project", @@ -8160,6 +8202,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "wasm-bindgen-futures", ] [[package]] @@ -8285,6 +8328,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0470d0ae043ffcb0cd323797a631e637fb4b55fe3eaa6002934819458bba62a7" +dependencies = [ + "jsonrpsee-client-transport 0.24.3", + "jsonrpsee-core 0.24.3", + "jsonrpsee-types 0.24.3", +] + [[package]] name = "jsonrpsee-ws-client" version = "0.23.2" @@ -9013,7 +9067,7 @@ dependencies = [ "futures", "js-sys", "libp2p-core", - "send_wrapper", + "send_wrapper 0.6.0", "wasm-bindgen", "wasm-bindgen-futures", ] @@ -12321,11 +12375,16 @@ dependencies = [ "array-bytes", "assert_matches", "bitflags 1.3.2", + "derive_more", "environmental", + "ethereum-types", "frame-benchmarking", "frame-support", "frame-system", + "hex", + "hex-literal", "impl-trait-for-tuples", + "jsonrpsee 0.24.3", "log", "pallet-assets", "pallet-balances", @@ -12335,6 +12394,7 @@ dependencies = [ "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-timestamp", + "pallet-transaction-payment", "pallet-utility", "parity-scale-codec", "paste", @@ -12342,17 +12402,20 @@ dependencies = [ "pretty_assertions", "rlp", "scale-info", + "secp256k1", "serde", + "serde_json", "sp-api 26.0.0", + "sp-arithmetic 23.0.0", "sp-core 28.0.0", "sp-io 30.0.0", "sp-keystore 0.34.0", "sp-runtime 31.0.1", "sp-std 14.0.0", "sp-tracing 16.0.0", + "sp-weights 27.0.0", "staging-xcm", "staging-xcm-builder", - "wat", ] [[package]] @@ -20317,6 +20380,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "send_wrapper" version = "0.6.0" diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 784d74973c32..c59316e3b8db 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -3005,6 +3005,30 @@ impl_runtime_apis! { impl pallet_revive::ReviveApi for Runtime { + fn eth_transact( + from: H160, + dest: Option, + value: Balance, + input: Vec, + gas_limit: Option, + storage_deposit_limit: Option, + ) -> pallet_revive::ContractResult + { + use pallet_revive::AddressMapper; + let blockweights: BlockWeights = ::BlockWeights::get(); + let origin = ::AddressMapper::to_account_id_contract(&from); + Revive::bare_eth_transact( + RuntimeOrigin::signed(origin), + dest, + value, + input, + gas_limit.unwrap_or(blockweights.max_block), + storage_deposit_limit.unwrap_or(u128::MAX), + pallet_revive::DebugInfo::UnsafeDebug, + pallet_revive::CollectEvents::UnsafeCollect, + ) + } + fn call( origin: AccountId, dest: H160, @@ -3012,7 +3036,7 @@ impl_runtime_apis! { gas_limit: Option, storage_deposit_limit: Option, input_data: Vec, - ) -> pallet_revive::ContractExecResult { + ) -> pallet_revive::ContractResult { Revive::bare_call( RuntimeOrigin::signed(origin), dest, @@ -3033,7 +3057,7 @@ impl_runtime_apis! { code: pallet_revive::Code, data: Vec, salt: Option<[u8; 32]>, - ) -> pallet_revive::ContractInstantiateResult + ) -> pallet_revive::ContractResult { Revive::bare_instantiate( RuntimeOrigin::signed(origin), diff --git a/substrate/frame/revive/Cargo.toml b/substrate/frame/revive/Cargo.toml index 177b3c3e9eec..4a83ef3eceaa 100644 --- a/substrate/frame/revive/Cargo.toml +++ b/substrate/frame/revive/Cargo.toml @@ -27,9 +27,13 @@ codec = { features = [ ], workspace = true } scale-info = { features = ["derive"], workspace = true } log = { workspace = true } -serde = { optional = true, features = ["derive"], workspace = true, default-features = true } +serde = { features = ["alloc", "derive"], workspace = true, default-features = false } impl-trait-for-tuples = { workspace = true } rlp = { workspace = true } +derive_more.workspace = true +hex = { workspace = true } +jsonrpsee = { workspace = true, features = ["full"], optional = true } +ethereum-types = { workspace = true, features = ["codec", "rlp", "serialize"] } # Polkadot SDK Dependencies frame-benchmarking = { optional = true, workspace = true } @@ -39,11 +43,14 @@ pallet-balances = { optional = true, workspace = true } pallet-revive-fixtures = { workspace = true, default-features = false } pallet-revive-uapi = { workspace = true, default-features = true } pallet-revive-proc-macro = { workspace = true, default-features = true } +pallet-transaction-payment = { workspace = true } sp-api = { workspace = true } +sp-arithmetic = { workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +sp-weights = { workspace = true } xcm = { workspace = true } xcm-builder = { workspace = true } @@ -51,7 +58,6 @@ xcm-builder = { workspace = true } array-bytes = { workspace = true, default-features = true } assert_matches = { workspace = true } pretty_assertions = { workspace = true } -wat = { workspace = true } pallet-revive-fixtures = { workspace = true, default-features = true } # Polkadot SDK Dependencies @@ -64,6 +70,9 @@ pallet-proxy = { workspace = true, default-features = true } sp-keystore = { workspace = true, default-features = true } sp-tracing = { workspace = true, default-features = true } xcm-builder = { workspace = true, default-features = true } +secp256k1 = { workspace = true, features = ["recovery"] } +serde_json.workspace = true +hex-literal.workspace = true [features] default = ["std"] @@ -77,6 +86,7 @@ std = [ "frame-benchmarking?/std", "frame-support/std", "frame-system/std", + "jsonrpsee", "log/std", "pallet-balances?/std", "pallet-proxy/std", @@ -86,13 +96,20 @@ std = [ "polkavm/std", "rlp/std", "scale-info/std", - "serde", + "ethereum-types/std", + "hex/std", + "pallet-transaction-payment/std", + "secp256k1/std", + "serde/std", + "serde_json/std", "sp-api/std", + "sp-arithmetic/std", "sp-core/std", "sp-io/std", "sp-keystore/std", "sp-runtime/std", "sp-std/std", + "sp-weights/std", "xcm-builder/std", "xcm/std", ] @@ -117,6 +134,7 @@ try-runtime = [ "pallet-message-queue/try-runtime", "pallet-proxy/try-runtime", "pallet-timestamp/try-runtime", + "pallet-transaction-payment/try-runtime", "pallet-utility/try-runtime", "sp-runtime/try-runtime", ] diff --git a/substrate/frame/revive/src/address.rs b/substrate/frame/revive/src/address.rs index c51940ba771e..3418e49e6f20 100644 --- a/substrate/frame/revive/src/address.rs +++ b/substrate/frame/revive/src/address.rs @@ -17,6 +17,7 @@ //! Functions that deal contract addresses. +use crate::LOG_TARGET; use alloc::vec::Vec; use sp_core::H160; use sp_io::hashing::keccak_256; @@ -34,7 +35,7 @@ use sp_runtime::AccountId32; /// case for all existing runtimes as of right now. Reasing is that this will allow /// us to reverse an address -> account_id mapping by just stripping the prefix. pub trait AddressMapper: private::Sealed { - /// Convert an account id to an ethereum adress. + /// Convert an account id to an ethereum address. /// /// This mapping is **not** required to be reversible. fn to_address(account_id: &T) -> H160; @@ -77,6 +78,7 @@ impl AddressMapper for DefaultAddressMapper { /// Determine the address of a contract using CREATE semantics. pub fn create1(deployer: &H160, nonce: u64) -> H160 { + log::debug!(target: LOG_TARGET, "create1: deployer={deployer:?}, nonce={nonce}"); let mut list = rlp::RlpStream::new_list(2); list.append(&deployer.as_bytes()); list.append(&nonce); diff --git a/substrate/frame/revive/src/evm.rs b/substrate/frame/revive/src/evm.rs new file mode 100644 index 000000000000..018c652aec75 --- /dev/null +++ b/substrate/frame/revive/src/evm.rs @@ -0,0 +1,6 @@ +//!Types, and traits to integrate pallet-revive with EVM. +#![warn(missing_docs)] + +mod api; +pub use api::*; +pub mod runtime; diff --git a/substrate/frame/revive/src/evm/api.rs b/substrate/frame/revive/src/evm/api.rs new file mode 100644 index 000000000000..3fb620df2423 --- /dev/null +++ b/substrate/frame/revive/src/evm/api.rs @@ -0,0 +1,21 @@ +//! JSON-RPC methods and types, for Ethereum. + +mod byte; +pub use byte::*; + +mod rlp_codec; +pub use rlp; + +mod type_id; +pub use type_id::*; + +mod rpc_types; +mod rpc_types_gen; +pub use rpc_types_gen::*; + +#[cfg(feature = "std")] +pub mod rpc_methods_gen; +#[cfg(feature = "std")] +pub use rpc_methods_gen::*; + +mod signature; diff --git a/substrate/frame/revive/src/evm/api/byte.rs b/substrate/frame/revive/src/evm/api/byte.rs new file mode 100644 index 000000000000..164182d5e5a4 --- /dev/null +++ b/substrate/frame/revive/src/evm/api/byte.rs @@ -0,0 +1,132 @@ +//! Define Byte wrapper types for encoding and decoding hex strings +use codec::{Decode, Encode}; +use core::{ + fmt::{Debug, Display, Formatter, Result as FmtResult}, + str::FromStr, +}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +#[cfg(not(feature = "std"))] +use alloc::vec; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +use hex_serde::HexCodec; + +mod hex_serde { + #[cfg(not(feature = "std"))] + use alloc::{format, string::String, vec::Vec}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub trait HexCodec: Sized { + type Error; + fn to_hex(&self) -> String; + fn from_hex(s: String) -> Result; + } + + impl HexCodec for u8 { + type Error = core::num::ParseIntError; + fn to_hex(&self) -> String { + format!("0x{:x}", self) + } + fn from_hex(s: String) -> Result { + u8::from_str_radix(s.trim_start_matches("0x"), 16) + } + } + + impl HexCodec for [u8; T] { + type Error = hex::FromHexError; + fn to_hex(&self) -> String { + format!("0x{}", hex::encode(self)) + } + fn from_hex(s: String) -> Result { + let data = hex::decode(s.trim_start_matches("0x"))?; + data.try_into().map_err(|_| hex::FromHexError::InvalidStringLength) + } + } + + impl HexCodec for Vec { + type Error = hex::FromHexError; + fn to_hex(&self) -> String { + format!("0x{}", hex::encode(self)) + } + fn from_hex(s: String) -> Result { + hex::decode(s.trim_start_matches("0x")) + } + } + + pub fn serialize(value: &T, serializer: S) -> Result + where + S: Serializer, + T: HexCodec, + { + let s = value.to_hex(); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: HexCodec, + ::Error: core::fmt::Debug, + { + let s = String::deserialize(deserializer)?; + let value = T::from_hex(s).map_err(|e| serde::de::Error::custom(format!("{:?}", e)))?; + Ok(value) + } +} + +impl FromStr for Bytes { + type Err = hex::FromHexError; + fn from_str(s: &str) -> Result { + let data = hex::decode(s.trim_start_matches("0x"))?; + Ok(Bytes(data)) + } +} + +macro_rules! impl_hex { + ($type:ident, $inner:ty, $default:expr) => { + #[derive(Encode, Decode, Eq, PartialEq, TypeInfo, Clone, Serialize, Deserialize)] + #[doc = concat!("`", stringify!($inner), "`", " wrapper type for encoding and decoding hex strings")] + pub struct $type(#[serde(with = "hex_serde")] pub $inner); + + impl Default for $type { + fn default() -> Self { + $type($default) + } + } + + impl From<$inner> for $type { + fn from(inner: $inner) -> Self { + $type(inner) + } + } + + impl Debug for $type { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, concat!(stringify!($type), "({})"), self.0.to_hex()) + } + } + + impl Display for $type { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.0.to_hex()) + } + } + }; +} + +impl_hex!(Byte, u8, 0u8); +impl_hex!(Bytes, Vec, vec![]); +impl_hex!(Bytes256, [u8; 256], [0u8; 256]); + +#[test] +fn serialize_byte() { + let a = Byte(42); + let s = serde_json::to_string(&a).unwrap(); + assert_eq!(s, "\"0x2a\""); + let b = serde_json::from_str::(&s).unwrap(); + assert_eq!(a, b); +} diff --git a/substrate/frame/revive/src/evm/api/rlp_codec.rs b/substrate/frame/revive/src/evm/api/rlp_codec.rs new file mode 100644 index 000000000000..1d4340eaf5d0 --- /dev/null +++ b/substrate/frame/revive/src/evm/api/rlp_codec.rs @@ -0,0 +1,205 @@ +//! RLP encoding and decoding for Ethereum transactions. +//! See for more information about RLP encoding. + +use super::*; +use rlp::{Decodable, Encodable}; + +/// See +impl Encodable for TransactionLegacyUnsigned { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + if let Some(chain_id) = self.chain_id { + s.begin_list(9); + s.append(&self.nonce); + s.append(&self.gas_price); + s.append(&self.gas); + match self.to { + Some(ref to) => s.append(to), + None => s.append_empty_data(), + }; + s.append(&self.value); + s.append(&self.input.0); + s.append(&chain_id); + s.append(&0_u8); + s.append(&0_u8); + } else { + s.begin_list(6); + s.append(&self.nonce); + s.append(&self.gas_price); + s.append(&self.gas); + match self.to { + Some(ref to) => s.append(to), + None => s.append_empty_data(), + }; + s.append(&self.value); + s.append(&self.input.0); + } + } +} + +/// See +impl Decodable for TransactionLegacyUnsigned { + fn decode(rlp: &rlp::Rlp) -> Result { + Ok(TransactionLegacyUnsigned { + nonce: rlp.val_at(0)?, + gas_price: rlp.val_at(1)?, + gas: rlp.val_at(2)?, + to: { + let to = rlp.at(3)?; + if to.is_empty() { + None + } else { + Some(to.as_val()?) + } + }, + value: rlp.val_at(4)?, + input: Bytes(rlp.val_at(5)?), + chain_id: { + if let Ok(chain_id) = rlp.val_at(6) { + Some(chain_id) + } else { + None + } + }, + ..Default::default() + }) + } +} + +impl Encodable for TransactionLegacySigned { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + s.begin_list(9); + s.append(&self.transaction_legacy_unsigned.nonce); + s.append(&self.transaction_legacy_unsigned.gas_price); + s.append(&self.transaction_legacy_unsigned.gas); + match self.transaction_legacy_unsigned.to { + Some(ref to) => s.append(to), + None => s.append_empty_data(), + }; + s.append(&self.transaction_legacy_unsigned.value); + s.append(&self.transaction_legacy_unsigned.input.0); + + s.append(&self.v); + s.append(&self.r); + s.append(&self.s); + } +} + +/// See +impl Decodable for TransactionLegacySigned { + fn decode(rlp: &rlp::Rlp) -> Result { + let v: U256 = rlp.val_at(6)?; + let extract_chain_id = |v: u64| { + if v >= 35 { + Some((v - 35) / 2) + } else { + None + } + }; + + Ok(TransactionLegacySigned { + transaction_legacy_unsigned: { + TransactionLegacyUnsigned { + nonce: rlp.val_at(0)?, + gas_price: rlp.val_at(1)?, + gas: rlp.val_at(2)?, + to: { + let to = rlp.at(3)?; + if to.is_empty() { + None + } else { + Some(to.as_val()?) + } + }, + value: rlp.val_at(4)?, + input: Bytes(rlp.val_at(5)?), + chain_id: extract_chain_id(v.as_u64()).map(|v| v.into()), + r#type: Type0 {}, + } + }, + v, + r: rlp.val_at(7)?, + s: rlp.val_at(8)?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use core::str::FromStr; + use secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; + use sp_core::keccak_256; + + struct Account { + sk: SecretKey, + } + + impl Default for Account { + fn default() -> Self { + Account { + sk: SecretKey::from_str( + "a872f6cbd25a0e04a08b1e21098017a9e6194d101d75e13111f71410c59cd57f", + ) + .unwrap(), + } + } + } + + impl Account { + fn address(&self) -> H160 { + let pub_key = + PublicKey::from_secret_key(&Secp256k1::new(), &self.sk).serialize_uncompressed(); + let hash = keccak_256(&pub_key[1..]); + H160::from_slice(&hash[12..]) + } + + fn sign_transaction(&self, tx: TransactionLegacyUnsigned) -> TransactionLegacySigned { + let rlp_encoded = tx.rlp_bytes(); + let tx_hash = keccak_256(&rlp_encoded); + let secp = Secp256k1::new(); + let msg = Message::from_digest(tx_hash); + let sig = secp.sign_ecdsa_recoverable(&msg, &self.sk); + let (recovery_id, sig) = sig.serialize_compact(); + let sig = sig.into_iter().chain([recovery_id.to_i32() as u8]).collect::>(); + TransactionLegacySigned::from(tx, &sig.try_into().unwrap()) + } + } + + #[test] + fn encode_decode_legacy_transaction_works() { + let tx = TransactionLegacyUnsigned { + chain_id: Some(596.into()), + gas: U256::from(21000), + nonce: U256::from(1), + gas_price: U256::from("0x640000006a"), + to: Some(H160::from_str("0x1111111111222222222233333333334444444444").unwrap()), + value: U256::from(123123), + input: Bytes(vec![]), + r#type: Type0, + }; + + let rlp_bytes = rlp::encode(&tx); + let decoded = rlp::decode::(&rlp_bytes).unwrap(); + dbg!(&decoded); + } + + #[test] + fn recover_address_works() { + let account = Account::default(); + + let unsigned_tx = TransactionLegacyUnsigned { + value: 200_000_000_000_000_000_000u128.into(), + gas_price: 100_000_000_200u64.into(), + gas: 100_107u32.into(), + nonce: 3.into(), + to: Some(H160::from_str("75e480db528101a381ce68544611c169ad7eb342").unwrap()), + chain_id: Some(596.into()), + ..Default::default() + }; + + let tx = account.sign_transaction(unsigned_tx.clone()); + let recovered_address = tx.recover_eth_address().unwrap(); + + assert_eq!(account.address(), recovered_address); + } +} diff --git a/substrate/frame/revive/src/evm/api/rpc_methods_gen.rs b/substrate/frame/revive/src/evm/api/rpc_methods_gen.rs new file mode 100644 index 000000000000..525615bf957b --- /dev/null +++ b/substrate/frame/revive/src/evm/api/rpc_methods_gen.rs @@ -0,0 +1,136 @@ +//! Generated JSON-RPC methods. +#![allow(missing_docs)] + +use super::*; +use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + +#[rpc(server, client)] +pub trait EthRpc { + /// Returns a list of addresses owned by client. + #[method(name = "eth_accounts")] + async fn accounts(&self) -> RpcResult>; + + /// Returns the number of most recent block. + #[method(name = "eth_blockNumber")] + async fn block_number(&self) -> RpcResult; + + /// Executes a new message call immediately without creating a transaction on the block chain. + #[method(name = "eth_call")] + async fn call( + &self, + transaction: GenericTransaction, + block: Option, + ) -> RpcResult; + + /// Returns the chain ID of the current network. + #[method(name = "eth_chainId")] + async fn chain_id(&self) -> RpcResult; + + /// Generates and returns an estimate of how much gas is necessary to allow the transaction to + /// complete. + #[method(name = "eth_estimateGas")] + async fn estimate_gas( + &self, + transaction: GenericTransaction, + block: Option, + ) -> RpcResult; + + /// Returns the current price per gas in wei. + #[method(name = "eth_gasPrice")] + async fn gas_price(&self) -> RpcResult; + + /// Returns the balance of the account of given address. + #[method(name = "eth_getBalance")] + async fn get_balance(&self, address: Address, block: BlockNumberOrTagOrHash) + -> RpcResult; + + /// Returns information about a block by hash. + #[method(name = "eth_getBlockByHash")] + async fn get_block_by_hash( + &self, + block_hash: H256, + hydrated_transactions: bool, + ) -> RpcResult>; + + /// Returns information about a block by number. + #[method(name = "eth_getBlockByNumber")] + async fn get_block_by_number( + &self, + block: BlockNumberOrTag, + hydrated_transactions: bool, + ) -> RpcResult>; + + /// Returns the number of transactions in a block from a block matching the given block hash. + #[method(name = "eth_getBlockTransactionCountByHash")] + async fn get_block_transaction_count_by_hash( + &self, + block_hash: Option, + ) -> RpcResult>; + + /// Returns the number of transactions in a block matching the given block number. + #[method(name = "eth_getBlockTransactionCountByNumber")] + async fn get_block_transaction_count_by_number( + &self, + block: Option, + ) -> RpcResult>; + + /// Returns code at a given address. + #[method(name = "eth_getCode")] + async fn get_code(&self, address: Address, block: BlockNumberOrTagOrHash) -> RpcResult; + + /// Returns the value from a storage position at a given address. + #[method(name = "eth_getStorageAt")] + async fn get_storage_at( + &self, + address: Address, + storage_slot: U256, + block: BlockNumberOrTagOrHash, + ) -> RpcResult; + + /// Returns information about a transaction by block hash and transaction index position. + #[method(name = "eth_getTransactionByBlockHashAndIndex")] + async fn get_transaction_by_block_hash_and_index( + &self, + block_hash: H256, + transaction_index: U256, + ) -> RpcResult>; + + /// Returns information about a transaction by block number and transaction index position. + #[method(name = "eth_getTransactionByBlockNumberAndIndex")] + async fn get_transaction_by_block_number_and_index( + &self, + block: BlockNumberOrTag, + transaction_index: U256, + ) -> RpcResult>; + + /// Returns the information about a transaction requested by transaction hash. + #[method(name = "eth_getTransactionByHash")] + async fn get_transaction_by_hash( + &self, + transaction_hash: H256, + ) -> RpcResult>; + + /// Returns the number of transactions sent from an address. + #[method(name = "eth_getTransactionCount")] + async fn get_transaction_count( + &self, + address: Address, + block: BlockNumberOrTagOrHash, + ) -> RpcResult; + + /// Returns the receipt of a transaction by transaction hash. + #[method(name = "eth_getTransactionReceipt")] + async fn get_transaction_receipt( + &self, + transaction_hash: H256, + ) -> RpcResult>; + + /// Submits a raw transaction. For EIP-4844 transactions, the raw form must be the network form. + /// This means it includes the blobs, KZG commitments, and KZG proofs. + #[method(name = "eth_sendRawTransaction")] + async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult; + + /// The string value of current network id + #[method(name = "net_version")] + async fn net_version(&self) -> RpcResult; +} diff --git a/substrate/frame/revive/src/evm/api/rpc_types.rs b/substrate/frame/revive/src/evm/api/rpc_types.rs new file mode 100644 index 000000000000..22a2c3f7c263 --- /dev/null +++ b/substrate/frame/revive/src/evm/api/rpc_types.rs @@ -0,0 +1,16 @@ +//! Utility impl for the RPC types. +use super::{ReceiptInfo, TransactionInfo, TransactionSigned}; + +impl TransactionInfo { + /// Create a new [`TransactionInfo`] from a receipt and a signed transaction. + pub fn new(receipt: ReceiptInfo, transaction_signed: TransactionSigned) -> Self { + Self { + block_hash: receipt.block_hash, + block_number: receipt.block_number, + from: receipt.from, + hash: receipt.transaction_hash, + transaction_index: receipt.transaction_index, + transaction_signed, + } + } +} diff --git a/substrate/frame/revive/src/evm/api/rpc_types_gen.rs b/substrate/frame/revive/src/evm/api/rpc_types_gen.rs new file mode 100644 index 000000000000..dc4e309f2568 --- /dev/null +++ b/substrate/frame/revive/src/evm/api/rpc_types_gen.rs @@ -0,0 +1,634 @@ +//! Generated JSON-RPC types. +#![allow(missing_docs)] + +use super::{byte::*, Type0, Type1, Type2}; +use codec::{Decode, Encode}; +use derive_more::{From, TryInto}; +pub use ethereum_types::*; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; + +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; + +/// Block object +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Block { + /// Base fee per gas + #[serde(rename = "baseFeePerGas", skip_serializing_if = "Option::is_none")] + pub base_fee_per_gas: Option, + /// Blob gas used + #[serde(rename = "blobGasUsed", skip_serializing_if = "Option::is_none")] + pub blob_gas_used: Option, + /// Difficulty + #[serde(skip_serializing_if = "Option::is_none")] + pub difficulty: Option, + /// Excess blob gas + #[serde(rename = "excessBlobGas", skip_serializing_if = "Option::is_none")] + pub excess_blob_gas: Option, + /// Extra data + #[serde(rename = "extraData")] + pub extra_data: Bytes, + /// Gas limit + #[serde(rename = "gasLimit")] + pub gas_limit: U256, + /// Gas used + #[serde(rename = "gasUsed")] + pub gas_used: U256, + /// Hash + pub hash: H256, + /// Bloom filter + #[serde(rename = "logsBloom")] + pub logs_bloom: Bytes256, + /// Coinbase + pub miner: Address, + /// Mix hash + #[serde(rename = "mixHash")] + pub mix_hash: H256, + /// Nonce + pub nonce: String, + /// Number + pub number: U256, + /// Parent Beacon Block Root + #[serde(rename = "parentBeaconBlockRoot", skip_serializing_if = "Option::is_none")] + pub parent_beacon_block_root: Option, + /// Parent block hash + #[serde(rename = "parentHash")] + pub parent_hash: H256, + /// Receipts root + #[serde(rename = "receiptsRoot")] + pub receipts_root: H256, + /// Ommers hash + #[serde(rename = "sha3Uncles")] + pub sha_3_uncles: H256, + /// Block size + pub size: U256, + /// State root + #[serde(rename = "stateRoot")] + pub state_root: H256, + /// Timestamp + pub timestamp: U256, + /// Total difficulty + #[serde(rename = "totalDifficulty", skip_serializing_if = "Option::is_none")] + pub total_difficulty: Option, + pub transactions: H256OrTransactionInfo, + /// Transactions root + #[serde(rename = "transactionsRoot")] + pub transactions_root: H256, + /// Uncles + pub uncles: Vec, + /// Withdrawals + #[serde(skip_serializing_if = "Option::is_none")] + pub withdrawals: Option>, + /// Withdrawals root + #[serde(rename = "withdrawalsRoot", skip_serializing_if = "Option::is_none")] + pub withdrawals_root: Option, +} + +/// Block number or tag +#[derive( + Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq, +)] +#[serde(untagged)] +pub enum BlockNumberOrTag { + /// Block number + U256(U256), + /// Block tag + BlockTag(BlockTag), +} +impl Default for BlockNumberOrTag { + fn default() -> Self { + BlockNumberOrTag::U256(Default::default()) + } +} + +/// Block number, tag, or block hash +#[derive( + Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq, +)] +#[serde(untagged)] +pub enum BlockNumberOrTagOrHash { + /// Block number + U256(U256), + /// Block tag + BlockTag(BlockTag), + /// Block hash + H256(H256), +} +impl Default for BlockNumberOrTagOrHash { + fn default() -> Self { + BlockNumberOrTagOrHash::U256(Default::default()) + } +} + +/// Transaction object generic to all types +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct GenericTransaction { + /// accessList + /// EIP-2930 access list + #[serde(rename = "accessList", skip_serializing_if = "Option::is_none")] + pub access_list: Option, + /// blobVersionedHashes + /// List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. + #[serde(rename = "blobVersionedHashes", skip_serializing_if = "Option::is_none")] + pub blob_versioned_hashes: Option>, + /// blobs + /// Raw blob data. + #[serde(skip_serializing_if = "Option::is_none")] + pub blobs: Option>, + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId", skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + /// from address + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option
, + /// gas limit + #[serde(skip_serializing_if = "Option::is_none")] + pub gas: Option, + /// gas price + /// The gas price willing to be paid by the sender in wei + #[serde(rename = "gasPrice", skip_serializing_if = "Option::is_none")] + pub gas_price: Option, + /// input data + #[serde(alias = "data", skip_serializing_if = "Option::is_none")] + pub input: Option, + /// max fee per blob gas + /// The maximum total fee per gas the sender is willing to pay for blob gas in wei + #[serde(rename = "maxFeePerBlobGas", skip_serializing_if = "Option::is_none")] + pub max_fee_per_blob_gas: Option, + /// max fee per gas + /// The maximum total fee per gas the sender is willing to pay (includes the network / base fee + /// and miner / priority fee) in wei + #[serde(rename = "maxFeePerGas", skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// max priority fee per gas + /// Maximum fee per gas the sender is willing to pay to miners in wei + #[serde(rename = "maxPriorityFeePerGas", skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + /// nonce + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, + /// to address + pub to: Option
, + /// type + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + /// value + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +/// Receipt information +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct ReceiptInfo { + /// blob gas price + /// The actual value per gas deducted from the sender's account for blob gas. Only specified + /// for blob transactions as defined by EIP-4844. + #[serde(rename = "blobGasPrice", skip_serializing_if = "Option::is_none")] + pub blob_gas_price: Option, + /// blob gas used + /// The amount of blob gas used for this specific transaction. Only specified for blob + /// transactions as defined by EIP-4844. + #[serde(rename = "blobGasUsed", skip_serializing_if = "Option::is_none")] + pub blob_gas_used: Option, + /// block hash + #[serde(rename = "blockHash")] + pub block_hash: H256, + /// block number + #[serde(rename = "blockNumber")] + pub block_number: U256, + /// contract address + /// The contract address created, if the transaction was a contract creation, otherwise null. + #[serde(rename = "contractAddress")] + pub contract_address: Option
, + /// cumulative gas used + /// The sum of gas used by this transaction and all preceding transactions in the same block. + #[serde(rename = "cumulativeGasUsed")] + pub cumulative_gas_used: U256, + /// effective gas price + /// The actual value per gas deducted from the sender's account. Before EIP-1559, this is equal + /// to the transaction's gas price. After, it is equal to baseFeePerGas + min(maxFeePerGas - + /// baseFeePerGas, maxPriorityFeePerGas). + #[serde(rename = "effectiveGasPrice")] + pub effective_gas_price: U256, + /// from + pub from: Address, + /// gas used + /// The amount of gas used for this specific transaction alone. + #[serde(rename = "gasUsed")] + pub gas_used: U256, + /// logs + pub logs: Vec, + /// logs bloom + #[serde(rename = "logsBloom")] + pub logs_bloom: Bytes256, + /// state root + /// The post-transaction state root. Only specified for transactions included before the + /// Byzantium upgrade. + #[serde(skip_serializing_if = "Option::is_none")] + pub root: Option, + /// status + /// Either 1 (success) or 0 (failure). Only specified for transactions included after the + /// Byzantium upgrade. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// to + /// Address of the receiver or null in a contract creation transaction. + pub to: Option
, + /// transaction hash + #[serde(rename = "transactionHash")] + pub transaction_hash: H256, + /// transaction index + #[serde(rename = "transactionIndex")] + pub transaction_index: U256, + /// type + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, +} + +/// Transaction information +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct TransactionInfo { + /// block hash + #[serde(rename = "blockHash")] + pub block_hash: H256, + /// block number + #[serde(rename = "blockNumber")] + pub block_number: U256, + /// from address + pub from: Address, + /// transaction hash + pub hash: H256, + /// transaction index + #[serde(rename = "transactionIndex")] + pub transaction_index: U256, + #[serde(flatten)] + pub transaction_signed: TransactionSigned, +} + +#[derive( + Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq, +)] +#[serde(untagged)] +pub enum TransactionUnsigned { + Transaction4844Unsigned(Transaction4844Unsigned), + Transaction1559Unsigned(Transaction1559Unsigned), + Transaction2930Unsigned(Transaction2930Unsigned), + TransactionLegacyUnsigned(TransactionLegacyUnsigned), +} +impl Default for TransactionUnsigned { + fn default() -> Self { + TransactionUnsigned::Transaction4844Unsigned(Default::default()) + } +} + +/// Access list +pub type AccessList = Vec; + +/// Block tag +/// `earliest`: The lowest numbered block the client has available; `finalized`: The most recent +/// crypto-economically secure block, cannot be re-orged outside of manual intervention driven by +/// community coordination; `safe`: The most recent block that is safe from re-orgs under honest +/// majority and certain synchronicity assumptions; `latest`: The most recent block in the canonical +/// chain observed by the client, this block may be re-orged out of the canonical chain even under +/// healthy/normal conditions; `pending`: A sample next block built by the client on top of `latest` +/// and containing the set of transactions usually taken from local mempool. Before the merge +/// transition is finalized, any call querying for `finalized` or `safe` block MUST be responded to +/// with `-39001: Unknown block` error +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub enum BlockTag { + #[serde(rename = "earliest")] + #[default] + Earliest, + #[serde(rename = "finalized")] + Finalized, + #[serde(rename = "safe")] + Safe, + #[serde(rename = "latest")] + Latest, + #[serde(rename = "pending")] + Pending, +} + +#[derive( + Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq, +)] +#[serde(untagged)] +pub enum H256OrTransactionInfo { + /// Transaction hashes + H256s(Vec), + /// Full transactions + TransactionInfos(Vec), +} +impl Default for H256OrTransactionInfo { + fn default() -> Self { + H256OrTransactionInfo::H256s(Default::default()) + } +} + +/// log +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Log { + /// address + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option
, + /// block hash + #[serde(rename = "blockHash", skip_serializing_if = "Option::is_none")] + pub block_hash: Option, + /// block number + #[serde(rename = "blockNumber", skip_serializing_if = "Option::is_none")] + pub block_number: Option, + /// data + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + /// log index + #[serde(rename = "logIndex", skip_serializing_if = "Option::is_none")] + pub log_index: Option, + /// removed + #[serde(skip_serializing_if = "Option::is_none")] + pub removed: Option, + /// topics + #[serde(skip_serializing_if = "Option::is_none")] + pub topics: Option>, + /// transaction hash + #[serde(rename = "transactionHash")] + pub transaction_hash: H256, + /// transaction index + #[serde(rename = "transactionIndex", skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, +} + +/// EIP-1559 transaction. +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction1559Unsigned { + /// accessList + /// EIP-2930 access list + #[serde(rename = "accessList")] + pub access_list: AccessList, + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId")] + pub chain_id: U256, + /// gas limit + pub gas: U256, + /// gas price + /// The effective gas price paid by the sender in wei. For transactions not yet included in a + /// block, this value should be set equal to the max fee per gas. This field is DEPRECATED, + /// please transition to using effectiveGasPrice in the receipt object going forward. + #[serde(rename = "gasPrice")] + pub gas_price: U256, + /// input data + pub input: Bytes, + /// max fee per gas + /// The maximum total fee per gas the sender is willing to pay (includes the network / base fee + /// and miner / priority fee) in wei + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: U256, + /// max priority fee per gas + /// Maximum fee per gas the sender is willing to pay to miners in wei + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: U256, + /// nonce + pub nonce: U256, + /// to address + pub to: Option
, + /// type + pub r#type: Type2, + /// value + pub value: U256, +} + +/// EIP-2930 transaction. +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction2930Unsigned { + /// accessList + /// EIP-2930 access list + #[serde(rename = "accessList")] + pub access_list: AccessList, + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId")] + pub chain_id: U256, + /// gas limit + pub gas: U256, + /// gas price + /// The gas price willing to be paid by the sender in wei + #[serde(rename = "gasPrice")] + pub gas_price: U256, + /// input data + pub input: Bytes, + /// nonce + pub nonce: U256, + /// to address + pub to: Option
, + /// type + pub r#type: Type1, + /// value + pub value: U256, +} + +/// EIP-4844 transaction. +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction4844Unsigned { + /// accessList + /// EIP-2930 access list + #[serde(rename = "accessList")] + pub access_list: AccessList, + /// blobVersionedHashes + /// List of versioned blob hashes associated with the transaction's EIP-4844 data blobs. + #[serde(rename = "blobVersionedHashes")] + pub blob_versioned_hashes: Vec, + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId")] + pub chain_id: U256, + /// gas limit + pub gas: U256, + /// input data + pub input: Bytes, + /// max fee per blob gas + /// The maximum total fee per gas the sender is willing to pay for blob gas in wei + #[serde(rename = "maxFeePerBlobGas")] + pub max_fee_per_blob_gas: U256, + /// max fee per gas + /// The maximum total fee per gas the sender is willing to pay (includes the network / base fee + /// and miner / priority fee) in wei + #[serde(rename = "maxFeePerGas")] + pub max_fee_per_gas: U256, + /// max priority fee per gas + /// Maximum fee per gas the sender is willing to pay to miners in wei + #[serde(rename = "maxPriorityFeePerGas")] + pub max_priority_fee_per_gas: U256, + /// nonce + pub nonce: U256, + /// to address + pub to: Address, + /// type + pub r#type: Byte, + /// value + pub value: U256, +} + +/// Legacy transaction. +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct TransactionLegacyUnsigned { + /// chainId + /// Chain ID that this transaction is valid on. + #[serde(rename = "chainId", skip_serializing_if = "Option::is_none")] + pub chain_id: Option, + /// gas limit + pub gas: U256, + /// gas price + /// The gas price willing to be paid by the sender in wei + #[serde(rename = "gasPrice")] + pub gas_price: U256, + /// input data + pub input: Bytes, + /// nonce + pub nonce: U256, + /// to address + pub to: Option
, + /// type + pub r#type: Type0, + /// value + pub value: U256, +} + +#[derive( + Debug, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, From, TryInto, Eq, PartialEq, +)] +#[serde(untagged)] +pub enum TransactionSigned { + Transaction4844Signed(Transaction4844Signed), + Transaction1559Signed(Transaction1559Signed), + Transaction2930Signed(Transaction2930Signed), + TransactionLegacySigned(TransactionLegacySigned), +} +impl Default for TransactionSigned { + fn default() -> Self { + TransactionSigned::Transaction4844Signed(Default::default()) + } +} + +/// Validator withdrawal +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Withdrawal { + /// recipient address for withdrawal value + pub address: Address, + /// value contained in withdrawal + pub amount: U256, + /// index of withdrawal + pub index: U256, + /// index of validator that generated withdrawal + #[serde(rename = "validatorIndex")] + pub validator_index: U256, +} + +/// Access list entry +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct AccessListEntry { + pub address: Address, + #[serde(rename = "storageKeys")] + pub storage_keys: Vec, +} + +/// Signed 1559 Transaction +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction1559Signed { + #[serde(flatten)] + pub transaction_1559_unsigned: Transaction1559Unsigned, + /// r + pub r: U256, + /// s + pub s: U256, + /// v + /// For backwards compatibility, `v` is optionally provided as an alternative to `yParity`. + /// This field is DEPRECATED and all use of it should migrate to `yParity`. + #[serde(skip_serializing_if = "Option::is_none")] + pub v: Option, + /// yParity + /// The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + #[serde(rename = "yParity", skip_serializing_if = "Option::is_none")] + pub y_parity: Option, +} + +/// Signed 2930 Transaction +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction2930Signed { + #[serde(flatten)] + pub transaction_2930_unsigned: Transaction2930Unsigned, + /// r + pub r: U256, + /// s + pub s: U256, + /// v + /// For backwards compatibility, `v` is optionally provided as an alternative to `yParity`. + /// This field is DEPRECATED and all use of it should migrate to `yParity`. + #[serde(skip_serializing_if = "Option::is_none")] + pub v: Option, + /// yParity + /// The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + #[serde(rename = "yParity")] + pub y_parity: U256, +} + +/// Signed 4844 Transaction +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct Transaction4844Signed { + #[serde(flatten)] + pub transaction_4844_unsigned: Transaction4844Unsigned, + /// r + pub r: U256, + /// s + pub s: U256, + /// yParity + /// The parity (0 for even, 1 for odd) of the y-value of the secp256k1 signature. + #[serde(rename = "yParity", skip_serializing_if = "Option::is_none")] + pub y_parity: Option, +} + +/// Signed Legacy Transaction +#[derive( + Debug, Default, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize, Eq, PartialEq, +)] +pub struct TransactionLegacySigned { + #[serde(flatten)] + pub transaction_legacy_unsigned: TransactionLegacyUnsigned, + /// r + pub r: U256, + /// s + pub s: U256, + /// v + pub v: U256, +} diff --git a/substrate/frame/revive/src/evm/api/signature.rs b/substrate/frame/revive/src/evm/api/signature.rs new file mode 100644 index 000000000000..583a9f00c577 --- /dev/null +++ b/substrate/frame/revive/src/evm/api/signature.rs @@ -0,0 +1,67 @@ +//! Ethereum signature utilities +use super::{TransactionLegacySigned, TransactionLegacyUnsigned}; +use rlp::Encodable; +use sp_core::{H160, U256}; +use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256}; + +impl TransactionLegacyUnsigned { + /// Recover the Ethereum address, from an RLP encoded transaction and a 65 bytes signature. + pub fn recover_eth_address(rlp_encoded: &[u8], signature: &[u8; 65]) -> Result { + let hash = keccak_256(rlp_encoded); + + let mut addr = H160::default(); + addr.assign_from_slice( + &keccak_256(&secp256k1_ecdsa_recover(&signature, &hash).ok().unwrap()[..])[12..], + ); + + Ok(addr) + } +} + +impl TransactionLegacySigned { + /// Create a signed transaction from an [`TransactionLegacyUnsigned`] and a + /// [`RecoverableSignature`]. + pub fn from( + transaction_legacy_unsigned: TransactionLegacyUnsigned, + signature: &[u8; 65], + ) -> TransactionLegacySigned { + let r = U256::from_big_endian(&signature[..32]); + let s = U256::from_big_endian(&signature[32..64]); + let recovery_id = signature[64] as u32; + let v = transaction_legacy_unsigned + .chain_id + .map(|chain_id| chain_id * 2 + 35 + recovery_id) + .unwrap_or_else(|| U256::from(27) + recovery_id); + + TransactionLegacySigned { transaction_legacy_unsigned, r, s, v } + } + + /// Get the raw 65 bytes signature from the signed transaction. + pub fn raw_signature(&self) -> Result<[u8; 65], ()> { + let mut s = [0u8; 65]; + self.r.to_big_endian(s[0..32].as_mut()); + self.s.to_big_endian(s[32..64].as_mut()); + s[64] = self.extract_recovery_id().ok_or(())?; + Ok(s) + } + + /// Get the recovery ID from the signed transaction. + /// See https://eips.ethereum.org/EIPS/eip-155 + fn extract_recovery_id(&self) -> Option { + if let Some(chain_id) = self.transaction_legacy_unsigned.chain_id { + // self.v - chain_id * 2 - 35 + let v: u64 = self.v.try_into().ok()?; + let chain_id: u64 = chain_id.try_into().ok()?; + let r = v.checked_sub(chain_id.checked_mul(2)?)?.checked_sub(35)?; + r.try_into().ok() + } else { + self.v.try_into().ok() + } + } + + /// Recover the Ethereum address from the signed transaction. + pub fn recover_eth_address(&self) -> Result { + let rlp_encoded = self.transaction_legacy_unsigned.rlp_bytes(); + TransactionLegacyUnsigned::recover_eth_address(&rlp_encoded, &self.raw_signature()?) + } +} diff --git a/substrate/frame/revive/src/evm/api/type_id.rs b/substrate/frame/revive/src/evm/api/type_id.rs new file mode 100644 index 000000000000..368a4da3c1ac --- /dev/null +++ b/substrate/frame/revive/src/evm/api/type_id.rs @@ -0,0 +1,70 @@ +//! Ethereum Typed Transaction types +use super::Byte; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// A macro to generate Transaction type identifiers +/// See +macro_rules! transaction_type { + ($name:ident, $value:literal) => { + #[doc = concat!("Transaction type identifier: ", $value)] + #[derive(Clone, Default, Debug, Eq, PartialEq)] + pub struct $name; + + impl $name { + /// Convert to Byte + pub fn as_byte(&self) -> Byte { + Byte::from($value) + } + } + + impl Encode for $name { + fn using_encoded R>(&self, f: F) -> R { + f(&[$value]) + } + } + impl Decode for $name { + fn decode(input: &mut I) -> Result { + if $value == input.read_byte()? { + Ok(Self {}) + } else { + Err(codec::Error::from(concat!("expected ", $value))) + } + } + } + + impl TypeInfo for $name { + type Identity = u8; + fn type_info() -> scale_info::Type { + ::type_info() + } + } + + impl Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(concat!("0x", $value)) + } + } + impl<'de> Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: &str = Deserialize::deserialize(deserializer)?; + if s == concat!("0x", $value) { + Ok($name {}) + } else { + Err(serde::de::Error::custom(concat!("expected ", $value))) + } + } + } + }; +} + +transaction_type!(Type0, 0); +transaction_type!(Type1, 1); +transaction_type!(Type2, 2); diff --git a/substrate/frame/revive/src/evm/runtime.rs b/substrate/frame/revive/src/evm/runtime.rs new file mode 100644 index 000000000000..e150202bef50 --- /dev/null +++ b/substrate/frame/revive/src/evm/runtime.rs @@ -0,0 +1,269 @@ +//! Runtime types for integrating `pallet-revive` with the EVM. +#![allow(unused_imports, unused_variables)] +use crate::{ + evm::api::{TransactionLegacySigned, TransactionLegacyUnsigned, TransactionUnsigned}, + AccountIdOf, AddressMapper, BalanceOf, Config, EthInstantiateInput, EthTransactKind, MomentOf, + Weight, LOG_TARGET, +}; +use codec::{Decode, Encode}; +use core::marker::PhantomData; +use frame_support::{ + dispatch::{DispatchInfo, GetDispatchInfo}, + traits::ExtrinsicCall, + CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound, +}; +use scale_info::TypeInfo; +use sp_arithmetic::Percent; +use sp_core::{ecdsa, ed25519, sr25519, Get, H160, U256}; +use sp_runtime::{ + generic::{self, CheckedExtrinsic}, + traits::{ + self, Checkable, Convert, DispatchInfoOf, Dispatchable, Extrinsic, ExtrinsicMetadata, Lazy, + Member, SignedExtension, Verify, + }, + transaction_validity::{ + InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, + }, + AccountId32, MultiSignature, MultiSigner, RuntimeDebug, Saturating, +}; + +#[cfg(not(feature = "std"))] +use alloc::{vec, vec::Vec}; + +/// Some way of identifying an account on the chain. +pub type AccountId = AccountId32; + +/// The type for looking up accounts. We don't expect more than 4 billion of them. +pub type AccountIndex = u32; + +/// The address format for describing accounts. +pub type MultiAddress = sp_runtime::MultiAddress; + +/// Wraps [`generic::UncheckedExtrinsic`] to support checking [`crate::Call::eth_transact`]. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[scale_info(skip_type_params(E))] +pub struct UncheckedExtrinsic( + generic::UncheckedExtrinsic, +); + +impl Extrinsic for UncheckedExtrinsic { + type Call = Call; + + type SignaturePayload = (MultiAddress, MultiSignature, E::Extra); + + fn is_signed(&self) -> Option { + self.0.is_signed() + } + + fn new(function: Call, signed_data: Option) -> Option { + Some(if let Some((address, signature, extra)) = signed_data { + Self(generic::UncheckedExtrinsic::new_signed(function, address, signature, extra)) + } else { + Self(generic::UncheckedExtrinsic::new_unsigned(function)) + }) + } +} + +impl ExtrinsicMetadata for UncheckedExtrinsic { + const VERSION: u8 = + generic::UncheckedExtrinsic::::VERSION; + type SignedExtensions = E::Extra; +} + +impl ExtrinsicCall for UncheckedExtrinsic { + fn call(&self) -> &Self::Call { + self.0.call() + } +} + +impl Checkable for UncheckedExtrinsic +where + Call: Encode + Member, + E: EthExtra, + ::Nonce: Into, + BalanceOf: Into + TryFrom, + MomentOf: Into, + + AccountIdOf: Into, + Call: From> + TryInto>, + E::Extra: SignedExtension, + Lookup: traits::Lookup, +{ + type Checked = CheckedExtrinsic; + + fn check(self, lookup: &Lookup) -> Result { + if self.0.signature.is_none() { + if let Ok(call) = self.0.function.clone().try_into() { + if let crate::Call::eth_transact { + payload, + gas_limit, + storage_deposit_limit, + transact_kind, + } = call + { + let checked = E::try_into_checked_extrinsic( + payload, + gas_limit, + storage_deposit_limit, + transact_kind, + )?; + return Ok(checked) + }; + } + } + self.0.check(lookup) + } + + #[cfg(feature = "try-runtime")] + fn unchecked_into_checked_i_know_what_i_am_doing( + self, + _: &Lookup, + ) -> Result { + unreachable!(); + } +} + +impl GetDispatchInfo for UncheckedExtrinsic +where + Call: GetDispatchInfo, + E: EthExtra, +{ + fn get_dispatch_info(&self) -> DispatchInfo { + self.0.get_dispatch_info() + } +} + +impl serde::Serialize for UncheckedExtrinsic { + fn serialize(&self, seq: S) -> Result + where + S: ::serde::Serializer, + { + self.0.serialize(seq) + } +} + +impl<'a, Call: Decode, E: EthExtra> serde::Deserialize<'a> for UncheckedExtrinsic { + fn deserialize(de: D) -> Result + where + D: serde::Deserializer<'a>, + { + let r = sp_core::bytes::deserialize(de)?; + Decode::decode(&mut &r[..]) + .map_err(|e| serde::de::Error::custom(sp_runtime::format!("Decode error: {}", e))) + } +} + +/// EthExtra convert an unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`]. +pub trait EthExtra { + /// The Runtime configuration. + type Config: crate::Config; + + /// The Runtime's signed extension. + type Extra: SignedExtension; + + /// Get the signed extensions to apply to an unsigned [`crate::Call::eth_transact`] extrinsic. + fn get_eth_transact_extra(nonce: U256) -> Self::Extra; + + /// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`]. + fn try_into_checked_extrinsic( + payload: Vec, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + transact_kind: EthTransactKind, + ) -> Result, InvalidTransaction> + where + ::Nonce: Into, + BalanceOf: Into + TryFrom, + MomentOf: Into, + AccountIdOf: Into, + Call: From> + TryInto>, + { + let tx = rlp::decode::(&payload).map_err(|err| { + log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}"); + InvalidTransaction::Call + })?; + + let signer = tx.recover_eth_address().map_err(|err| { + log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}"); + InvalidTransaction::BadProof + })?; + + let signer = + ::AddressMapper::to_account_id_contract(&signer); + let TransactionLegacyUnsigned { nonce, chain_id, to, value, input, gas, gas_price, .. } = + tx.transaction_legacy_unsigned; + + if chain_id.unwrap_or_default() != ::ChainId::get().into() { + log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}"); + return Err(InvalidTransaction::Call); + } + + let account_nonce: U256 = >::account_nonce(&signer).into(); + if nonce > account_nonce { + log::debug!(target: LOG_TARGET, "Invalid nonce: expected {account_nonce}, got {nonce}"); + return Err(InvalidTransaction::Future); + } else if nonce < account_nonce { + log::debug!(target: LOG_TARGET, "Invalid nonce: expected {account_nonce}, got {nonce}"); + return Err(InvalidTransaction::Stale); + } + + let call = if let Some(dest) = to { + if !matches!(transact_kind, EthTransactKind::Call) { + log::debug!(target: LOG_TARGET, "Invalid transact_kind, expected Call"); + return Err(InvalidTransaction::Call); + } + + crate::Call::call:: { + dest, + value: value.try_into().map_err(|_| InvalidTransaction::Call)?, + gas_limit, + storage_deposit_limit, + data: input.0, + } + } else { + let EthTransactKind::InstantiateWithCode { code_len, data_len } = transact_kind else { + log::debug!(target: LOG_TARGET, "Invalid transact_kind, expected InstantiateWithCode"); + return Err(InvalidTransaction::Call); + }; + + let EthInstantiateInput { code, data } = EthInstantiateInput::decode(&mut &input.0[..]) + .map_err(|_| { + log::debug!(target: LOG_TARGET, "Failed to decoded eth_transact input"); + InvalidTransaction::Call + })?; + + if code.len() as u32 != code_len || data.len() as u32 != data_len { + log::debug!(target: LOG_TARGET, "Invalid code or data length"); + return Err(InvalidTransaction::Call); + } + + crate::Call::instantiate_with_code:: { + value: value.try_into().map_err(|_| InvalidTransaction::Call)?, + gas_limit, + storage_deposit_limit, + code, + data, + salt: None, + } + }; + + let dispatch_info = call.get_dispatch_info(); + let call_fee = ::WeightPrice::convert(dispatch_info.weight) + .saturating_add(storage_deposit_limit); + + let eth_fee = + gas_price.saturating_mul(gas).try_into().map_err(|_| InvalidTransaction::Call)?; + + // Make sure that that the fee computed from the signed payload is no more than 5% greater + // than the actual fee computed with the injected transaction parameters. + if Percent::from_rational(eth_fee, call_fee) > Percent::from_percent(105) { + log::debug!(target: LOG_TARGET, "Expected fees {eth_fee:?} to be within 5% of calculated fees {call_fee:?}"); + return Err(InvalidTransaction::Call.into()) + } + + Ok(CheckedExtrinsic { + signed: Some((signer.into(), Self::get_eth_transact_extra(nonce))), + function: call.into(), + }) + } +} diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 2e48bab29255..4581e332b762 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -890,7 +890,11 @@ where let address = if let Some(salt) = salt { address::create2(&deployer, executable.code(), input_data, salt) } else { - address::create1(&deployer, account_nonce.saturated_into()) + use sp_runtime::Saturating; + address::create1( + &deployer, + account_nonce.saturating_sub(1u32.into()).saturated_into(), + ) }; let contract = ContractInfo::new( &address, @@ -4153,7 +4157,14 @@ mod tests { // Successful instantiation should set the output let address = ctx .ext - .instantiate(Weight::zero(), U256::zero(), ok_ch, value, vec![], None) + .instantiate( + Weight::zero(), + U256::zero(), + ok_ch, + value, + vec![], + Some(&[0u8; 32]), + ) .unwrap(); assert_eq!( ctx.ext.last_frame_output(), @@ -4169,7 +4180,14 @@ mod tests { // Reverted instantiation should set the output ctx.ext - .instantiate(Weight::zero(), U256::zero(), revert_ch, value, vec![], None) + .instantiate( + Weight::zero(), + U256::zero(), + revert_ch, + value, + vec![], + Some(&[1u8; 32]), + ) .unwrap(); assert_eq!( ctx.ext.last_frame_output(), @@ -4178,7 +4196,14 @@ mod tests { // Trapped instantiation should clear the output ctx.ext - .instantiate(Weight::zero(), U256::zero(), trap_ch, value, vec![], None) + .instantiate( + Weight::zero(), + U256::zero(), + trap_ch, + value, + vec![], + Some(&[2u8; 32]), + ) .unwrap_err(); assert_eq!( ctx.ext.last_frame_output(), diff --git a/substrate/frame/revive/src/lib.rs b/substrate/frame/revive/src/lib.rs index 114d51c89695..6d63f06f6b92 100644 --- a/substrate/frame/revive/src/lib.rs +++ b/substrate/frame/revive/src/lib.rs @@ -21,17 +21,19 @@ #![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "1024")] extern crate alloc; + mod address; mod benchmarking; mod benchmarking_dummy; mod exec; mod gas; mod primitives; -use crate::exec::MomentOf; +pub use crate::exec::MomentOf; use frame_support::traits::IsType; pub use primitives::*; use sp_core::U256; +pub mod evm; mod limits; mod storage; mod transient_storage; @@ -78,7 +80,7 @@ use sp_runtime::{ }; pub use crate::{ - address::{AddressMapper, DefaultAddressMapper}, + address::{create1, create2, AddressMapper, DefaultAddressMapper}, debug::Tracing, pallet::*, }; @@ -88,7 +90,7 @@ pub use weights::WeightInfo; pub use crate::wasm::SyscallDoc; type TrieId = BoundedVec>; -type BalanceOf = +pub type BalanceOf = <::Currency as Inspect<::AccountId>>::Balance; type CodeVec = BoundedVec>; type EventRecordOf = @@ -159,6 +161,7 @@ pub mod pallet { type RuntimeCall: Dispatchable + GetDispatchInfo + codec::Decode + + core::fmt::Debug + IsType<::RuntimeCall>; /// Overarching hold reason. @@ -715,6 +718,41 @@ pub mod pallet { BalanceOf: Into + TryFrom, MomentOf: Into, { + /// A raw EVM transaction, typically dispatched by an Ethereum JSON-RPC server. + /// + /// # Parameters + /// + /// * `payload`: The RLP-encoded [`pallet_revive_evm::api::TransactionLegacySigned`]. + /// * `gas_limit`: The gas limit enforced during contract execution. + /// * `storage_deposit_limit`: The maximum balance that can be charged to the caller for + /// storage usage. + /// * `transact_kind`: The type of transaction to execute. + /// + /// # Note + /// + /// This call cannot be dispatched directly; attempting to do so will result in a failed + /// transaction. It serves as a wrapper for an Ethereum transaction. When submitted, the + /// runtime converts it into a [`sp_runtime::generic::CheckedExtrinsic`] by recovering the + /// signer and validating the transaction. + #[allow(unused_variables)] + #[pallet::call_index(0)] + #[pallet::weight( + match transact_kind { + EthTransactKind::Call => T::WeightInfo::call().saturating_add(*gas_limit), + EthTransactKind::InstantiateWithCode{code_len, data_len} => T::WeightInfo::instantiate_with_code(*code_len, *data_len) + .saturating_add(*gas_limit) + } + )] + pub fn eth_transact( + origin: OriginFor, + payload: Vec, + gas_limit: Weight, + #[pallet::compact] storage_deposit_limit: BalanceOf, + transact_kind: EthTransactKind, + ) -> DispatchResultWithPostInfo { + Err(frame_system::Error::CallFiltered::.into()) + } + /// Makes a call to an account, optionally transferring some balance. /// /// # Parameters @@ -731,7 +769,7 @@ pub mod pallet { /// * If the account is a regular account, any value will be transferred. /// * If no account exists and the call value is not less than `existential_deposit`, /// a regular account will be created and any value will be transferred. - #[pallet::call_index(0)] + #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::call().saturating_add(*gas_limit))] pub fn call( origin: OriginFor, @@ -741,6 +779,7 @@ pub mod pallet { #[pallet::compact] storage_deposit_limit: BalanceOf, data: Vec, ) -> DispatchResultWithPostInfo { + log::info!(target: LOG_TARGET, "Call: {:?} {:?} {:?}", dest, value, data); let mut output = Self::bare_call( origin, dest, @@ -764,7 +803,7 @@ pub mod pallet { /// This function is identical to [`Self::instantiate_with_code`] but without the /// code deployment step. Instead, the `code_hash` of an on-chain deployed wasm binary /// must be supplied. - #[pallet::call_index(1)] + #[pallet::call_index(2)] #[pallet::weight( T::WeightInfo::instantiate(data.len() as u32).saturating_add(*gas_limit) )] @@ -828,7 +867,7 @@ pub mod pallet { /// - The smart-contract account is created at the computed address. /// - The `value` is transferred to the new account. /// - The `deploy` function is executed in the context of the newly-created account. - #[pallet::call_index(2)] + #[pallet::call_index(3)] #[pallet::weight( T::WeightInfo::instantiate_with_code(code.len() as u32, data.len() as u32) .saturating_add(*gas_limit) @@ -879,7 +918,7 @@ pub mod pallet { /// To avoid this situation a constructor could employ access control so that it can /// only be instantiated by permissioned entities. The same is true when uploading /// through [`Self::instantiate_with_code`]. - #[pallet::call_index(3)] + #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::upload_code_determinism_enforced(code.len() as u32))] pub fn upload_code( origin: OriginFor, @@ -893,7 +932,7 @@ pub mod pallet { /// /// A code can only be removed by its original uploader (its owner) and only if it is /// not used by any contract. - #[pallet::call_index(4)] + #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::remove_code())] pub fn remove_code( origin: OriginFor, @@ -915,7 +954,7 @@ pub mod pallet { /// This does **not** change the address of the contract in question. This means /// that the contract address is no longer derived from its code hash after calling /// this dispatchable. - #[pallet::call_index(5)] + #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::set_code())] pub fn set_code( origin: OriginFor, @@ -979,7 +1018,7 @@ where data: Vec, debug: DebugInfo, collect_events: CollectEvents, - ) -> ContractExecResult, EventRecordOf> { + ) -> ContractResult, EventRecordOf> { let mut gas_meter = GasMeter::new(gas_limit); let mut storage_deposit = Default::default(); let mut debug_message = if matches!(debug, DebugInfo::UnsafeDebug) { @@ -1008,7 +1047,7 @@ where } else { None }; - ContractExecResult { + ContractResult { result: result.map_err(|r| r.error), gas_consumed: gas_meter.gas_consumed(), gas_required: gas_meter.gas_required(), @@ -1034,7 +1073,7 @@ where salt: Option<[u8; 32]>, debug: DebugInfo, collect_events: CollectEvents, - ) -> ContractInstantiateResult, EventRecordOf> { + ) -> ContractResult, EventRecordOf> { let mut gas_meter = GasMeter::new(gas_limit); let mut storage_deposit = Default::default(); let mut debug_message = @@ -1076,7 +1115,7 @@ where } else { None }; - ContractInstantiateResult { + ContractResult { result: output .map(|(addr, result)| InstantiateReturnValue { result, addr }) .map_err(|e| e.error), @@ -1088,6 +1127,63 @@ where } } + /// A version of [`Self::eth_transact`] used to dry-run Ethereum calls. + pub fn bare_eth_transact( + origin: OriginFor, + dest: Option, + value: BalanceOf, + input: Vec, + gas_limit: Weight, + storage_deposit_limit: BalanceOf, + debug: DebugInfo, + collect_events: CollectEvents, + ) -> ContractResult, EventRecordOf> { + if let Some(dest) = dest { + crate::Pallet::::bare_call( + origin, + dest, + value, + gas_limit, + storage_deposit_limit, + input, + debug, + collect_events, + ) + .map(|result| EthTransactReturnValue { kind: EthTransactKind::Call, result }) + } else { + let Ok(EthInstantiateInput { code, data }) = + EthInstantiateInput::decode(&mut &input[..]) + else { + return ContractResult { + result: Err(>::DecodingFailed.into()), + gas_consumed: Default::default(), + gas_required: Default::default(), + storage_deposit: Default::default(), + debug_message: Default::default(), + events: Default::default(), + } + }; + + let code_len = code.len() as u32; + let data_len = data.len() as u32; + crate::Pallet::::bare_instantiate( + origin, + value, + gas_limit, + storage_deposit_limit, + Code::Upload(code), + data, + None, + DebugInfo::Skip, + CollectEvents::Skip, + ) + .map(|r| EthTransactReturnValue { + kind: EthTransactKind::InstantiateWithCode { code_len, data_len }, + result: r.result, + }) + } + } + /// A generalized version of [`Self::upload_code`]. /// /// It is identical to [`Self::upload_code`] and only differs in the information it returns. @@ -1176,7 +1272,7 @@ sp_api::decl_runtime_apis! { gas_limit: Option, storage_deposit_limit: Option, input_data: Vec, - ) -> ContractExecResult; + ) -> ContractResult; /// Instantiate a new contract. /// @@ -1189,7 +1285,20 @@ sp_api::decl_runtime_apis! { code: Code, data: Vec, salt: Option<[u8; 32]>, - ) -> ContractInstantiateResult; + ) -> ContractResult; + + + /// Perform an Ethereum call. + /// + /// See [`crate::Pallet::bare_eth_transact`] + fn eth_transact( + origin: H160, + dest: Option, + value: Balance, + input: Vec, + gas_limit: Option, + storage_deposit_limit: Option, + ) -> ContractResult; /// Upload new code without instantiating a contract from it. /// diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index 67bc144c3dd2..33e4052c96b8 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -76,19 +76,25 @@ pub struct ContractResult { /// RPC calls. pub debug_message: Vec, /// The execution result of the wasm code. - pub result: R, + pub result: Result, /// The events that were emitted during execution. It is an option as event collection is /// optional. pub events: Option>, } -/// Result type of a `bare_call` call as well as `ContractsApi::call`. -pub type ContractExecResult = - ContractResult, Balance, EventRecord>; - -/// Result type of a `bare_instantiate` call as well as `ContractsApi::instantiate`. -pub type ContractInstantiateResult = - ContractResult, Balance, EventRecord>; +impl ContractResult { + /// Maps the result of a contract execution, using the provided function. + pub fn map(self, f: impl FnOnce(R) -> V) -> ContractResult { + ContractResult { + gas_consumed: self.gas_consumed, + gas_required: self.gas_required, + storage_deposit: self.storage_deposit, + debug_message: self.debug_message, + result: self.result.map(f), + events: self.events, + } + } +} /// Result type of a `bare_code_upload` call. pub type CodeUploadResult = Result, DispatchError>; @@ -121,6 +127,29 @@ impl ExecReturnValue { } } +/// Describes the type of an [`crate::Call::eth_transact`] call. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum EthTransactKind { + /// A Call to an existing contract. + Call, + /// The instantiation of a new contract. + InstantiateWithCode { + #[codec(compact)] + code_len: u32, + #[codec(compact)] + data_len: u32, + }, +} + +/// The result of a successful eth_transact call +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct EthTransactReturnValue { + /// The kind of transaction that was executed. + pub kind: EthTransactKind, + /// The output of the called constructor. + pub result: ExecReturnValue, +} + /// The result of a successful contract instantiation. #[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct InstantiateReturnValue { @@ -148,6 +177,15 @@ pub enum Code { Existing(sp_core::H256), } +/// A type used to encode the `input` field of an Ethereum transaction +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct EthInstantiateInput { + /// The bytecode of the contract. + pub code: Vec, + /// The data to pass to the constructor. + pub data: Vec, +} + /// The amount of balance that was either charged or refunded in order to pay for storage. #[derive( Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo, diff --git a/substrate/frame/revive/src/test_utils/builder.rs b/substrate/frame/revive/src/test_utils/builder.rs index d361590df95a..e64f58894432 100644 --- a/substrate/frame/revive/src/test_utils/builder.rs +++ b/substrate/frame/revive/src/test_utils/builder.rs @@ -17,9 +17,8 @@ use super::{deposit_limit, GAS_LIMIT}; use crate::{ - address::AddressMapper, AccountIdOf, BalanceOf, Code, CollectEvents, Config, - ContractExecResult, ContractInstantiateResult, DebugInfo, EventRecordOf, ExecReturnValue, - InstantiateReturnValue, OriginFor, Pallet, Weight, + address::AddressMapper, AccountIdOf, BalanceOf, Code, CollectEvents, Config, ContractResult, + DebugInfo, EventRecordOf, ExecReturnValue, InstantiateReturnValue, OriginFor, Pallet, Weight, }; use frame_support::pallet_prelude::DispatchResultWithPostInfo; use paste::paste; @@ -140,7 +139,7 @@ builder!( salt: Option<[u8; 32]>, debug: DebugInfo, collect_events: CollectEvents, - ) -> ContractInstantiateResult, EventRecordOf>; + ) -> ContractResult, EventRecordOf>; /// Build the instantiate call and unwrap the result. pub fn build_and_unwrap_result(self) -> InstantiateReturnValue { @@ -203,7 +202,7 @@ builder!( data: Vec, debug: DebugInfo, collect_events: CollectEvents, - ) -> ContractExecResult, EventRecordOf>; + ) -> ContractResult, EventRecordOf>; /// Build the call and unwrap the result. pub fn build_and_unwrap_result(self) -> ExecReturnValue { diff --git a/substrate/frame/revive/src/wasm/mod.rs b/substrate/frame/revive/src/wasm/mod.rs index cd274873975d..4b0422b2ff19 100644 --- a/substrate/frame/revive/src/wasm/mod.rs +++ b/substrate/frame/revive/src/wasm/mod.rs @@ -200,7 +200,10 @@ where &self.code_info.owner, deposit, ) - .map_err(|_| >::StorageDepositNotEnoughFunds)?; + .map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to store code for owner: {:?}: {err:?}", self.code_info.owner); + >::StorageDepositNotEnoughFunds + })?; self.code_info.refcount = 0; >::insert(code_hash, &self.code); diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 83cbebbc61cd..bd073e2b91f0 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -492,7 +492,6 @@ serde = [ "pallet-parameters?/serde", "pallet-referenda?/serde", "pallet-remark?/serde", - "pallet-revive?/serde", "pallet-state-trie-migration?/serde", "pallet-tips?/serde", "pallet-transaction-payment?/serde",