// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Runtime types for integrating `pezpallet-revive` with the EVM. use crate::{ evm::{ api::{GenericTransaction, TransactionSigned}, create_call, fees::InfoT, }, AccountIdOf, AddressMapper, BalanceOf, CallOf, Config, Pezpallet, Zero, LOG_TARGET, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use pezframe_support::{ dispatch::{DispatchInfo, GetDispatchInfo}, traits::{ fungible::Balanced, tokens::{Fortitude, Precision, Preservation}, InherentBuilder, IsSubType, SignedTransactionBuilder, }, }; use pezpallet_transaction_payment::Config as TxConfig; use pezsp_core::U256; use pezsp_runtime::{ generic::{self, CheckedExtrinsic, ExtrinsicFormat}, traits::{ Checkable, ExtrinsicCall, ExtrinsicLike, ExtrinsicMetadata, LazyExtrinsic, TransactionExtension, }, transaction_validity::{InvalidTransaction, TransactionValidityError}, OpaqueExtrinsic, RuntimeDebug, Weight, }; use scale_info::{StaticTypeInfo, TypeInfo}; /// Used to set the weight limit argument of a `eth_call` or `eth_instantiate_with_code` call. pub trait SetWeightLimit { /// Set the weight limit of this call. /// /// Returns the replaced weight. fn set_weight_limit(&mut self, weight_limit: Weight) -> Weight; } /// Wraps [`generic::UncheckedExtrinsic`] to support checking unsigned /// [`crate::Call::eth_transact`] extrinsic. #[derive(Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, RuntimeDebug)] pub struct UncheckedExtrinsic( pub generic::UncheckedExtrinsic, Signature, E::Extension>, ); impl TypeInfo for UncheckedExtrinsic where Address: StaticTypeInfo, Signature: StaticTypeInfo, E::Extension: StaticTypeInfo, { type Identity = generic::UncheckedExtrinsic, Signature, E::Extension>; fn type_info() -> scale_info::Type { generic::UncheckedExtrinsic::, Signature, E::Extension>::type_info() } } impl From, Signature, E::Extension>> for UncheckedExtrinsic { fn from( utx: generic::UncheckedExtrinsic, Signature, E::Extension>, ) -> Self { Self(utx) } } impl ExtrinsicLike for UncheckedExtrinsic { fn is_bare(&self) -> bool { ExtrinsicLike::is_bare(&self.0) } } impl ExtrinsicMetadata for UncheckedExtrinsic { const VERSIONS: &'static [u8] = generic::UncheckedExtrinsic::< Address, CallOf, Signature, E::Extension, >::VERSIONS; type TransactionExtensions = E::Extension; } impl ExtrinsicCall for UncheckedExtrinsic { type Call = CallOf; fn call(&self) -> &Self::Call { self.0.call() } fn into_call(self) -> Self::Call { self.0.into_call() } } impl Checkable for UncheckedExtrinsic where E: EthExtra, Self: Encode, ::Nonce: TryFrom, CallOf: SetWeightLimit, // required by Checkable for `generic::UncheckedExtrinsic` generic::UncheckedExtrinsic, Signature, E::Extension>: Checkable< Lookup, Checked = CheckedExtrinsic, CallOf, E::Extension>, >, { type Checked = CheckedExtrinsic, CallOf, E::Extension>; fn check(self, lookup: &Lookup) -> Result { if !self.0.is_signed() { if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() { let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?; return Ok(checked); }; } self.0.check(lookup) } #[cfg(feature = "try-runtime")] fn unchecked_into_checked_i_know_what_i_am_doing( self, lookup: &Lookup, ) -> Result { self.0.unchecked_into_checked_i_know_what_i_am_doing(lookup) } } impl GetDispatchInfo for UncheckedExtrinsic { 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, Address: Decode, Signature: Decode, E: EthExtra> serde::Deserialize<'a> for UncheckedExtrinsic { fn deserialize(de: D) -> Result where D: serde::Deserializer<'a>, { let r = pezsp_core::bytes::deserialize(de)?; Decode::decode(&mut &r[..]) .map_err(|e| serde::de::Error::custom(alloc::format!("Decode error: {}", e))) } } impl SignedTransactionBuilder for UncheckedExtrinsic where Address: TypeInfo, Signature: TypeInfo, E::Extension: TypeInfo, { type Address = Address; type Signature = Signature; type Extension = E::Extension; fn new_signed_transaction( call: Self::Call, signed: Address, signature: Signature, tx_ext: E::Extension, ) -> Self { generic::UncheckedExtrinsic::new_signed(call, signed, signature, tx_ext).into() } } impl InherentBuilder for UncheckedExtrinsic where Address: TypeInfo, Signature: TypeInfo, E::Extension: TypeInfo, { fn new_inherent(call: Self::Call) -> Self { generic::UncheckedExtrinsic::new_bare(call).into() } } impl From> for OpaqueExtrinsic where Address: Encode, Signature: Encode, E::Extension: Encode, { fn from(extrinsic: UncheckedExtrinsic) -> Self { extrinsic.0.into() } } impl LazyExtrinsic for UncheckedExtrinsic where generic::UncheckedExtrinsic, Signature, E::Extension>: LazyExtrinsic, { fn decode_unprefixed(data: &[u8]) -> Result { Ok(Self(LazyExtrinsic::decode_unprefixed(data)?)) } } /// EthExtra convert an unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`]. pub trait EthExtra { /// The Runtime configuration. type Config: Config + TxConfig; /// The Runtime's transaction extension. /// It should include at least: /// - [`pezframe_system::CheckNonce`] to ensure that the nonce from the Ethereum transaction is /// correct. type Extension: TransactionExtension>; /// Get the transaction extension to apply to an unsigned [`crate::Call::eth_transact`] /// extrinsic. /// /// # Parameters /// - `nonce`: The nonce extracted from the Ethereum transaction. /// - `tip`: The transaction tip calculated from the Ethereum transaction. fn get_eth_extension( nonce: ::Nonce, tip: BalanceOf, ) -> Self::Extension; /// Convert the unsigned [`crate::Call::eth_transact`] into a [`CheckedExtrinsic`]. /// and ensure that the fees from the Ethereum transaction correspond to the fees computed from /// the encoded_len, the injected gas_limit and storage_deposit_limit. /// /// # Parameters /// - `payload`: The RLP-encoded Ethereum transaction. /// - `encoded_len`: The encoded length of the extrinsic. fn try_into_checked_extrinsic( payload: &[u8], encoded_len: usize, ) -> Result< CheckedExtrinsic, CallOf, Self::Extension>, InvalidTransaction, > where ::Nonce: TryFrom, CallOf: SetWeightLimit, { let tx = TransactionSigned::decode(&payload).map_err(|err| { log::debug!(target: LOG_TARGET, "Failed to decode transaction: {err:?}"); InvalidTransaction::Call })?; // Check transaction type and reject unsupported transaction types match &tx { crate::evm::api::TransactionSigned::Transaction1559Signed(_) | crate::evm::api::TransactionSigned::Transaction2930Signed(_) | crate::evm::api::TransactionSigned::TransactionLegacySigned(_) => { // Supported transaction types, continue processing }, crate::evm::api::TransactionSigned::Transaction7702Signed(_) => { log::debug!(target: LOG_TARGET, "EIP-7702 transactions are not supported"); return Err(InvalidTransaction::Call); }, crate::evm::api::TransactionSigned::Transaction4844Signed(_) => { log::debug!(target: LOG_TARGET, "EIP-4844 transactions are not supported"); return Err(InvalidTransaction::Call); }, } let signer_addr = tx.recover_eth_address().map_err(|err| { log::debug!(target: LOG_TARGET, "Failed to recover signer: {err:?}"); InvalidTransaction::BadProof })?; let signer = ::AddressMapper::to_fallback_account_id(&signer_addr); let base_fee = >::evm_base_fee(); let tx = GenericTransaction::from_signed(tx, base_fee, None); let nonce = tx.nonce.unwrap_or_default().try_into().map_err(|_| { log::debug!(target: LOG_TARGET, "Failed to convert nonce"); InvalidTransaction::Call })?; log::trace!(target: LOG_TARGET, "Decoded Ethereum transaction with signer: {signer_addr:?} nonce: {nonce:?}"); let call_info = create_call::(tx, Some((encoded_len as u32, payload.to_vec())), true)?; let storage_credit = ::Currency::withdraw( &signer, call_info.storage_deposit, Precision::Exact, Preservation::Preserve, Fortitude::Polite, ).map_err(|_| { log::debug!(target: LOG_TARGET, "Not enough balance to hold additional storage deposit of {:?}", call_info.storage_deposit); InvalidTransaction::Payment })?; ::FeeInfo::deposit_txfee(storage_credit); crate::tracing::if_tracing(|tracer| { tracer.watch_address(&Pezpallet::::block_author()); tracer.watch_address(&signer_addr); }); log::debug!(target: LOG_TARGET, "\ Created checked Ethereum transaction with: \ from={signer_addr:?} \ eth_gas={} \ encoded_len={encoded_len} \ tx_fee={:?} \ storage_deposit={:?} \ weight_limit={} \ nonce={nonce:?}\ ", call_info.gas, call_info.tx_fee, call_info.storage_deposit, call_info.weight_limit, ); // We can't calculate a tip because it needs to be based on the actual gas used which we // cannot know pre-dispatch. Hence we never supply a tip here or it would be way too high. Ok(CheckedExtrinsic { format: ExtrinsicFormat::Signed( signer.into(), Self::get_eth_extension(nonce, Zero::zero()), ), function: call_info.call, }) } } #[cfg(test)] mod test { use super::*; use crate::{ evm::*, test_utils::*, tests::{ Address, ExtBuilder, RuntimeCall, RuntimeOrigin, SignedExtra, Test, UncheckedExtrinsic, }, EthTransactInfo, Weight, RUNTIME_PALLETS_ADDR, }; use pezframe_support::{error::LookupError, traits::fungible::Mutate}; use pezpallet_revive_fixtures::compile_module; use pezsp_runtime::traits::{self, Checkable, DispatchTransaction}; type AccountIdOf = ::AccountId; struct TestContext; impl traits::Lookup for TestContext { type Source = Address; type Target = AccountIdOf; fn lookup(&self, s: Self::Source) -> Result { match s { Self::Source::Id(id) => Ok(id), _ => Err(LookupError), } } } /// A builder for creating an unchecked extrinsic, and test that the check function works. #[derive(Clone)] struct UncheckedExtrinsicBuilder { tx: GenericTransaction, before_validate: Option>, dry_run: Option>>, } impl UncheckedExtrinsicBuilder { /// Create a new builder with default values. fn new() -> Self { Self { tx: GenericTransaction { from: Some(Account::default().address()), chain_id: Some(::ChainId::get().into()), ..Default::default() }, before_validate: None, dry_run: None, } } fn data(mut self, data: Vec) -> Self { self.tx.input = Bytes(data).into(); self } fn fund_account(account: &Account) { let _ = ::Currency::set_balance( &account.bizinikiwi_account(), 100_000_000_000_000, ); } fn estimate_gas(&mut self) { let account = Account::default(); Self::fund_account(&account); let dry_run = crate::Pezpallet::::dry_run_eth_transact(self.tx.clone(), Default::default()); self.tx.gas_price = Some(>::evm_base_fee()); match dry_run { Ok(dry_run) => { self.tx.gas = Some(dry_run.eth_gas); self.dry_run = Some(dry_run); }, Err(err) => { log::debug!(target: LOG_TARGET, "Failed to estimate gas: {:?}", err); }, } } /// Create a new builder with a call to the given address. fn call_with(dest: H160) -> Self { let mut builder = Self::new(); builder.tx.to = Some(dest); builder } /// Create a new builder with an instantiate call. fn instantiate_with(code: Vec, data: Vec) -> Self { let mut builder = Self::new(); builder.tx.input = Bytes(code.into_iter().chain(data.into_iter()).collect()).into(); builder } /// Set before_validate function. fn before_validate(mut self, f: impl Fn() + Send + Sync + 'static) -> Self { self.before_validate = Some(std::sync::Arc::new(f)); self } fn check( self, ) -> Result< (u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned), TransactionValidityError, > { self.mutate_estimate_and_check(Box::new(|_| ())) } /// Call `check` on the unchecked extrinsic, and `pre_dispatch` on the signed extension. fn mutate_estimate_and_check( mut self, f: Box ()>, ) -> Result< (u32, RuntimeCall, SignedExtra, GenericTransaction, Weight, TransactionSigned), TransactionValidityError, > { ExtBuilder::default().build().execute_with(|| self.estimate_gas()); ExtBuilder::default().build().execute_with(|| { f(&mut self.tx); let UncheckedExtrinsicBuilder { tx, before_validate, .. } = self.clone(); // Fund the account. let account = Account::default(); Self::fund_account(&account); let signed_transaction = account.sign_transaction(tx.clone().try_into_unsigned().unwrap()); let call = RuntimeCall::Contracts(crate::Call::eth_transact { payload: signed_transaction.signed_payload().clone(), }); let uxt: UncheckedExtrinsic = generic::UncheckedExtrinsic::new_bare(call).into(); let encoded_len = uxt.encoded_size(); let result: CheckedExtrinsic<_, _, _> = uxt.check(&TestContext {})?; let (account_id, extra): (AccountId32, SignedExtra) = match result.format { ExtrinsicFormat::Signed(signer, extra) => (signer, extra), _ => unreachable!(), }; before_validate.map(|f| f()); extra.clone().validate_and_prepare( RuntimeOrigin::signed(account_id), &result.function, &result.function.get_dispatch_info(), encoded_len, 0, )?; Ok(( encoded_len as u32, result.function, extra, tx, self.dry_run.unwrap().gas_required, signed_transaction, )) }) } } #[test] fn check_eth_transact_call_works() { let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])); let (expected_encoded_len, call, _, tx, gas_required, signed_transaction) = builder.check().unwrap(); let expected_effective_gas_price = ExtBuilder::default().build().execute_with(|| Pezpallet::::evm_base_fee()); match call { RuntimeCall::Contracts(crate::Call::eth_call:: { dest, value, data, gas_limit, transaction_encoded, effective_gas_price, encoded_len, }) if dest == tx.to.unwrap() && value == tx.value.unwrap_or_default().as_u64().into() && data == tx.input.to_vec() && transaction_encoded == signed_transaction.signed_payload() && effective_gas_price == expected_effective_gas_price => { assert_eq!(encoded_len, expected_encoded_len); assert!( gas_limit.all_gte(gas_required), "Assert failed: gas_limit={gas_limit:?} >= gas_required={gas_required:?}" ); }, _ => panic!("Call does not match."), } } #[test] fn check_eth_transact_instantiate_works() { let (expected_code, _) = compile_module("dummy").unwrap(); let expected_data = vec![]; let builder = UncheckedExtrinsicBuilder::instantiate_with( expected_code.clone(), expected_data.clone(), ); let (expected_encoded_len, call, _, tx, gas_required, signed_transaction) = builder.check().unwrap(); let expected_effective_gas_price = ExtBuilder::default().build().execute_with(|| Pezpallet::::evm_base_fee()); let expected_value = tx.value.unwrap_or_default().as_u64().into(); match call { RuntimeCall::Contracts(crate::Call::eth_instantiate_with_code:: { value, code, data, gas_limit, transaction_encoded, effective_gas_price, encoded_len, }) if value == expected_value && code == expected_code && data == expected_data && transaction_encoded == signed_transaction.signed_payload() && effective_gas_price == expected_effective_gas_price => { assert_eq!(encoded_len, expected_encoded_len); assert!( gas_limit.all_gte(gas_required), "Assert failed: gas_limit={gas_limit:?} >= gas_required={gas_required:?}" ); }, _ => panic!("Call does not match."), } } #[test] fn check_eth_transact_nonce_works() { let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])); assert_eq!( builder.mutate_estimate_and_check(Box::new(|tx| tx.nonce = Some(1u32.into()))), Err(TransactionValidityError::Invalid(InvalidTransaction::Future)) ); let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])).before_validate(|| { >::inc_account_nonce(Account::default().bizinikiwi_account()); }); assert_eq!( builder.check(), Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)) ); } #[test] fn check_eth_transact_chain_id_works() { let builder = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])); assert_eq!( builder.mutate_estimate_and_check(Box::new(|tx| tx.chain_id = Some(42.into()))), Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) ); } #[test] fn check_instantiate_data() { let code: Vec = polkavm_common::program::BLOB_MAGIC .into_iter() .chain(b"invalid code".iter().cloned()) .collect(); let data = vec![1]; let builder = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone()); // Fail because the tx input fail to get the blob length assert_eq!( builder.check(), Err(TransactionValidityError::Invalid(InvalidTransaction::Call)) ); } #[test] fn check_transaction_fees() { let scenarios: Vec<(_, Box, _)> = vec![ ( "Eth fees too low", Box::new(|tx| { tx.gas_price = Some(100u64.into()); }), InvalidTransaction::Payment, ), ( "Gas fees too low", Box::new(|tx| { tx.gas = Some(tx.gas.unwrap() / 2); }), InvalidTransaction::Payment, ), ]; for (msg, update_tx, err) in scenarios { let res = UncheckedExtrinsicBuilder::call_with(H160::from([1u8; 20])) .mutate_estimate_and_check(update_tx); assert_eq!(res, Err(TransactionValidityError::Invalid(err)), "{}", msg); } } #[test] fn check_transaction_tip() { let (code, _) = compile_module("dummy").unwrap(); // create some dummy data to increase the gas fee let data = vec![42u8; crate::limits::CALLDATA_BYTES as usize]; let (_, _, extra, _tx, _gas_required, _) = UncheckedExtrinsicBuilder::instantiate_with(code.clone(), data.clone()) .mutate_estimate_and_check(Box::new(|tx| { tx.gas_price = Some(tx.gas_price.unwrap() * 103 / 100); log::debug!(target: LOG_TARGET, "Gas price: {:?}", tx.gas_price); })) .unwrap(); assert_eq!(U256::from(extra.1.tip()), 0u32.into()); } #[test] fn check_runtime_pallets_addr_works() { let remark: CallOf = pezframe_system::Call::remark { remark: b"Hello, world!".to_vec() }.into(); let builder = UncheckedExtrinsicBuilder::call_with(RUNTIME_PALLETS_ADDR).data(remark.encode()); let (_, call, _, _, _, _) = builder.check().unwrap(); match call { RuntimeCall::Contracts(crate::Call::eth_bizinikiwi_call { call: inner_call, .. }) => { assert_eq!(*inner_call, remark); }, _ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call), } } }