Skip to content

Commit 3451201

Browse files
paritytech-release-backport-bot[bot]0xOmarAgithub-actions[bot]TorstenStueberathei
authored
[stable2512] Backport #10476 (#10737)
Backport #10476 into `stable2512` from 0xOmarA. See the [documentation](https://github.com/paritytech/polkadot-sdk/blob/master/docs/BACKPORT.md) on how to use this bot. <!-- # To be used by other automation, do not modify: original-pr-number: #${pull_number} --> --------- Co-authored-by: Omar <OmarAbdulla7@hotmail.com> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Torsten Stüber <15174476+TorstenStueber@users.noreply.github.com> Co-authored-by: Alexander Theißen <alex.theissen@me.com>
1 parent 0daad4a commit 3451201

5 files changed

Lines changed: 136 additions & 11 deletions

File tree

prdoc/pr_10476.prdoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
title: Allow for "Nick's Method" style deployments
2+
doc:
3+
- audience: Runtime Dev
4+
description: Allow eth legacy transactions to not contain a chain id.
5+
crates:
6+
- name: pallet-revive
7+
bump: patch
8+
- name: pallet-revive-eth-rpc
9+
bump: minor
10+
validate: false

substrate/frame/revive/rpc/src/cli.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ pub struct CliCommand {
7676
#[allow(missing_docs)]
7777
#[clap(flatten)]
7878
pub prometheus_params: PrometheusParams,
79+
80+
/// By default, the node rejects any transaction that's unprotected (i.e., that doesn't have a
81+
/// chain-id). If the user wishes the submit such a transaction then they can use this flag to
82+
/// instruct the RPC to ignore this check.
83+
#[arg(long)]
84+
pub allow_unprotected_txs: bool,
7985
}
8086

8187
/// Initialize the logger
@@ -164,6 +170,7 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
164170
earliest_receipt_block,
165171
index_last_n_blocks,
166172
shared_params,
173+
allow_unprotected_txs,
167174
..
168175
} = cmd;
169176

@@ -222,7 +229,7 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
222229
&rpc_config,
223230
prometheus_registry,
224231
tokio_handle,
225-
|| rpc_module(is_dev, client.clone()),
232+
|| rpc_module(is_dev, client.clone(), allow_unprotected_txs),
226233
None,
227234
)?;
228235

@@ -250,7 +257,11 @@ pub fn run(cmd: CliCommand) -> anyhow::Result<()> {
250257
}
251258

252259
/// Create the JSON-RPC module.
253-
fn rpc_module(is_dev: bool, client: Client) -> Result<RpcModule<()>, sc_service::Error> {
260+
fn rpc_module(
261+
is_dev: bool,
262+
client: Client,
263+
allow_unprotected_txs: bool,
264+
) -> Result<RpcModule<()>, sc_service::Error> {
254265
let eth_api = EthRpcServerImpl::new(client.clone())
255266
.with_accounts(if is_dev {
256267
vec![
@@ -263,6 +274,7 @@ fn rpc_module(is_dev: bool, client: Client) -> Result<RpcModule<()>, sc_service:
263274
} else {
264275
vec![]
265276
})
277+
.with_allow_unprotected_txs(allow_unprotected_txs)
266278
.into_rpc();
267279

268280
let health_api = SystemHealthRpcServerImpl::new(client.clone()).into_rpc();

substrate/frame/revive/rpc/src/lib.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,28 @@ pub struct EthRpcServerImpl {
5959

6060
/// The accounts managed by the server.
6161
accounts: Vec<Account>,
62+
63+
/// Controls if unprotected txs are allowed or not.
64+
allow_unprotected_txs: bool,
6265
}
6366

6467
impl EthRpcServerImpl {
6568
/// Creates a new [`EthRpcServerImpl`].
6669
pub fn new(client: client::Client) -> Self {
67-
Self { client, accounts: vec![] }
70+
Self { client, accounts: vec![], allow_unprotected_txs: false }
6871
}
6972

7073
/// Sets the accounts managed by the server.
7174
pub fn with_accounts(mut self, accounts: Vec<Account>) -> Self {
7275
self.accounts = accounts;
7376
self
7477
}
78+
79+
/// Sets whether unprotected transactions are allowed or not.
80+
pub fn with_allow_unprotected_txs(mut self, allow_unprotected_txs: bool) -> Self {
81+
self.allow_unprotected_txs = allow_unprotected_txs;
82+
self
83+
}
7584
}
7685

7786
/// The error type for the EVM RPC server.
@@ -168,6 +177,33 @@ impl EthRpcServer for EthRpcServerImpl {
168177
async fn send_raw_transaction(&self, transaction: Bytes) -> RpcResult<H256> {
169178
let hash = H256(keccak_256(&transaction.0));
170179
log::trace!(target: LOG_TARGET, "send_raw_transaction transaction: {transaction:?} ethereum_hash: {hash:?}");
180+
181+
if !self.allow_unprotected_txs {
182+
let signed_transaction = TransactionSigned::decode(transaction.0.as_slice())
183+
.map_err(|err| {
184+
log::trace!(target: LOG_TARGET, "Transaction decoding failed. ethereum_hash: {hash:?}, error: {err:?}");
185+
EthRpcError::InvalidTransaction
186+
})?;
187+
188+
let is_chain_id_provided = match signed_transaction {
189+
TransactionSigned::Transaction7702Signed(tx) =>
190+
tx.transaction_7702_unsigned.chain_id != U256::zero(),
191+
TransactionSigned::Transaction4844Signed(tx) =>
192+
tx.transaction_4844_unsigned.chain_id != U256::zero(),
193+
TransactionSigned::Transaction1559Signed(tx) =>
194+
tx.transaction_1559_unsigned.chain_id != U256::zero(),
195+
TransactionSigned::Transaction2930Signed(tx) =>
196+
tx.transaction_2930_unsigned.chain_id != U256::zero(),
197+
TransactionSigned::TransactionLegacySigned(tx) =>
198+
tx.transaction_legacy_unsigned.chain_id.is_some(),
199+
};
200+
201+
if !is_chain_id_provided {
202+
log::trace!(target: LOG_TARGET, "Invalid Transaction: transaction doesn't include a chain-id. ethereum_hash: {hash:?}");
203+
Err(EthRpcError::InvalidTransaction)?;
204+
}
205+
}
206+
171207
let call = subxt_client::tx().revive().eth_transact(transaction.0);
172208

173209
// Subscribe to new block only when automine is enabled.

substrate/frame/revive/src/evm/call.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::{
2121
evm::{
2222
fees::{compute_max_integer_quotient, InfoT},
2323
runtime::SetWeightLimit,
24+
TYPE_LEGACY,
2425
},
2526
extract_code_and_data, BalanceOf, CallOf, Config, GenericTransaction, Pallet, Weight, Zero,
2627
LOG_TARGET, RUNTIME_PALLETS_ADDR,
@@ -69,6 +70,27 @@ impl GenericTransaction {
6970
let is_dry_run = matches!(mode, CreateCallMode::DryRun);
7071
let base_fee = <Pallet<T>>::evm_base_fee();
7172

73+
// We would like to allow for transactions without a chain id to be executed through pallet
74+
// revive. These are called unprotected transactions and they are transactions that predate
75+
// EIP-155 which do not include a Chain ID. These transactions are still useful today in
76+
// certain patterns in Ethereum such as "Nick's Method" for contract deployment which
77+
// allows a contract to be deployed on all chains with the same address. This is only
78+
// allowed for legacy transactions and isn't allowed for any other transaction type.
79+
// * Here's a relevant EIP: https://eips.ethereum.org/EIPS/eip-2470
80+
// * Here's Nick's article: https://weka.medium.com/how-to-send-ether-to-11-440-people-187e332566b7
81+
match (self.chain_id, self.r#type.as_ref()) {
82+
(None, Some(super::Byte(TYPE_LEGACY))) => {},
83+
(Some(chain_id), ..) =>
84+
if chain_id != <T as Config>::ChainId::get().into() {
85+
log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}");
86+
return Err(InvalidTransaction::Call);
87+
},
88+
(None, ..) => {
89+
log::debug!(target: LOG_TARGET, "Invalid chain_id None");
90+
return Err(InvalidTransaction::Call);
91+
},
92+
}
93+
7294
let Some(gas) = self.gas else {
7395
log::debug!(target: LOG_TARGET, "No gas provided");
7496
return Err(InvalidTransaction::Call);
@@ -82,13 +104,6 @@ impl GenericTransaction {
82104
return Err(InvalidTransaction::Payment);
83105
};
84106

85-
let chain_id = self.chain_id.unwrap_or_default();
86-
87-
if chain_id != <T as Config>::ChainId::get().into() {
88-
log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}");
89-
return Err(InvalidTransaction::Call);
90-
}
91-
92107
if effective_gas_price < base_fee {
93108
log::debug!(
94109
target: LOG_TARGET,

substrate/frame/revive/src/evm/runtime.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ where
138138
if !self.0.is_signed() {
139139
if let Some(crate::Call::eth_transact { payload }) = self.0.function.is_sub_type() {
140140
let checked = E::try_into_checked_extrinsic(payload, self.encoded_size())?;
141-
return Ok(checked)
141+
return Ok(checked);
142142
};
143143
}
144144
self.0.check(lookup)
@@ -709,4 +709,56 @@ mod test {
709709
_ => panic!("Expected the RuntimeCall::Contracts variant, got: {:?}", call),
710710
}
711711
}
712+
713+
/// The raw bytes seen in this test is of a deployment transaction from [eip-2470] which publish
714+
/// a contract at a predicable address on any chain that it's run on. We use these bytes to test
715+
/// that if we were to run this transaction on pallet-revive that it would run and also produce
716+
/// a contract at the address described in the EIP.
717+
///
718+
/// Note: the linked EIP is not an EIP for Nick's method, it's just an EIP that makes use of
719+
/// Nick's method.
720+
///
721+
/// [eip-2470]: https://eips.ethereum.org/EIPS/eip-2470
722+
#[test]
723+
fn contract_deployment_with_nick_method_works() {
724+
// Arrange
725+
let raw_transaction_bytes = alloy_core::hex!("0xf9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b83247000822470");
726+
727+
let mut signed_transaction = TransactionSigned::decode(raw_transaction_bytes.as_slice())
728+
.expect("Invalid raw transaction bytes");
729+
if let TransactionSigned::TransactionLegacySigned(ref mut legacy_transaction) =
730+
signed_transaction
731+
{
732+
legacy_transaction.transaction_legacy_unsigned.gas =
733+
U256::from_dec_str("3750815700000").unwrap();
734+
}
735+
let generic_transaction = GenericTransaction::from_signed(
736+
signed_transaction.clone(),
737+
ExtBuilder::default().build().execute_with(|| Pallet::<Test>::evm_base_fee()),
738+
None,
739+
);
740+
741+
let unchecked_extrinsic_builder = UncheckedExtrinsicBuilder {
742+
tx: generic_transaction,
743+
before_validate: None,
744+
dry_run: None,
745+
};
746+
747+
// Act
748+
let eth_transact_result = unchecked_extrinsic_builder.check();
749+
750+
// Assert
751+
let (
752+
_encoded_len,
753+
_function,
754+
_extra,
755+
generic_transaction,
756+
_gas_required,
757+
_signed_transaction,
758+
) = eth_transact_result.expect("eth_transact failed");
759+
assert!(
760+
generic_transaction.chain_id.is_none(),
761+
"Chain Id in the generic transaction is not None"
762+
);
763+
}
712764
}

0 commit comments

Comments
 (0)