Skip to content

Commit 03aba4d

Browse files
authored
Fix security issue: transaction validity controls must be executed in STF (polkadot-evm#495)
* add security tests * add pre_dispatch phase to fiy security issues * Revert frame-evm changes * keeping *_self_contained aligned * remove unused default impl
1 parent 232cd44 commit 03aba4d

File tree

6 files changed

+287
-81
lines changed

6 files changed

+287
-81
lines changed

frame/ethereum/src/lib.rs

Lines changed: 138 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -118,78 +118,26 @@ where
118118
}
119119
}
120120

121-
pub fn validate_self_contained(&self, origin: &H160) -> Option<TransactionValidity> {
121+
pub fn pre_dispatch_self_contained(
122+
&self,
123+
origin: &H160,
124+
) -> Option<Result<(), TransactionValidityError>> {
122125
if let Call::transact(transaction) = self {
123-
let validate = || {
124-
// We must ensure a transaction can pay the cost of its data bytes.
125-
// If it can't it should not be included in a block.
126-
let mut gasometer = evm::gasometer::Gasometer::new(
127-
transaction.gas_limit.low_u64(),
128-
<T as pallet_evm::Config>::config(),
129-
);
130-
let transaction_cost = match transaction.action {
131-
TransactionAction::Call(_) => {
132-
evm::gasometer::call_transaction_cost(&transaction.input)
133-
}
134-
TransactionAction::Create => {
135-
evm::gasometer::create_transaction_cost(&transaction.input)
136-
}
137-
};
138-
if gasometer.record_transaction(transaction_cost).is_err() {
139-
return InvalidTransaction::Custom(
140-
TransactionValidationError::InvalidGasLimit as u8,
141-
)
142-
.into();
143-
}
144-
145-
if let Some(chain_id) = transaction.signature.chain_id() {
146-
if chain_id != T::ChainId::get() {
147-
return InvalidTransaction::Custom(
148-
TransactionValidationError::InvalidChainId as u8,
149-
)
150-
.into();
151-
}
152-
}
153-
154-
if transaction.gas_limit >= T::BlockGasLimit::get() {
155-
return InvalidTransaction::Custom(
156-
TransactionValidationError::InvalidGasLimit as u8,
157-
)
158-
.into();
159-
}
160-
161-
let account_data = pallet_evm::Pallet::<T>::account_basic(&origin);
162-
163-
if transaction.nonce < account_data.nonce {
164-
return InvalidTransaction::Stale.into();
165-
}
166-
167-
let fee = transaction.gas_price.saturating_mul(transaction.gas_limit);
168-
let total_payment = transaction.value.saturating_add(fee);
169-
if account_data.balance < total_payment {
170-
return InvalidTransaction::Payment.into();
171-
}
172-
173-
let min_gas_price = T::FeeCalculator::min_gas_price();
174-
175-
if transaction.gas_price < min_gas_price {
176-
return InvalidTransaction::Payment.into();
177-
}
178-
179-
let mut builder = ValidTransactionBuilder::default()
180-
.and_provides((origin, transaction.nonce))
181-
.priority(transaction.gas_price.unique_saturated_into());
182-
183-
if transaction.nonce > account_data.nonce {
184-
if let Some(prev_nonce) = transaction.nonce.checked_sub(1.into()) {
185-
builder = builder.and_requires((origin, prev_nonce))
186-
}
187-
}
188-
189-
builder.build()
190-
};
126+
Some(Pallet::<T>::validate_transaction_in_block(
127+
*origin,
128+
&transaction,
129+
))
130+
} else {
131+
None
132+
}
133+
}
191134

192-
Some(validate())
135+
pub fn validate_self_contained(&self, origin: &H160) -> Option<TransactionValidity> {
136+
if let Call::transact(transaction) = self {
137+
Some(Pallet::<T>::validate_transaction_in_pool(
138+
*origin,
139+
transaction,
140+
))
193141
} else {
194142
None
195143
}
@@ -259,9 +207,12 @@ pub mod pallet {
259207
"pre-block transaction signature invalid; the block cannot be built",
260208
);
261209

262-
Self::do_transact(source, transaction).expect(
210+
Self::validate_transaction_in_block(source, &transaction).expect(
263211
"pre-block transaction verification failed; the block cannot be built",
264212
);
213+
Self::apply_validated_transaction(source, transaction).expect(
214+
"pre-block transaction execution failed; the block cannot be built",
215+
);
265216
}
266217
}
267218

@@ -287,7 +238,7 @@ pub mod pallet {
287238
Error::<T>::PreLogExists,
288239
);
289240

290-
Self::do_transact(source, transaction)
241+
Self::apply_validated_transaction(source, transaction)
291242
}
292243
}
293244

@@ -418,7 +369,98 @@ impl<T: Config> Pallet<T> {
418369
}
419370
}
420371

421-
fn do_transact(source: H160, transaction: Transaction) -> DispatchResultWithPostInfo {
372+
// Common controls to be performed in the same way by the pool and the
373+
// State Transition Function (STF).
374+
// This is the case for all controls except those concerning the nonce.
375+
fn validate_transaction_common(
376+
origin: H160,
377+
transaction: &Transaction,
378+
) -> Result<U256, TransactionValidityError> {
379+
// We must ensure a transaction can pay the cost of its data bytes.
380+
// If it can't it should not be included in a block.
381+
let mut gasometer = evm::gasometer::Gasometer::new(
382+
transaction.gas_limit.low_u64(),
383+
<T as pallet_evm::Config>::config(),
384+
);
385+
let transaction_cost = match transaction.action {
386+
TransactionAction::Call(_) => evm::gasometer::call_transaction_cost(&transaction.input),
387+
TransactionAction::Create => {
388+
evm::gasometer::create_transaction_cost(&transaction.input)
389+
}
390+
};
391+
if gasometer.record_transaction(transaction_cost).is_err() {
392+
return Err(InvalidTransaction::Custom(
393+
TransactionValidationError::InvalidGasLimit as u8,
394+
)
395+
.into());
396+
}
397+
398+
if let Some(chain_id) = transaction.signature.chain_id() {
399+
if chain_id != T::ChainId::get() {
400+
return Err(InvalidTransaction::Custom(
401+
TransactionValidationError::InvalidChainId as u8,
402+
)
403+
.into());
404+
}
405+
}
406+
407+
if transaction.gas_limit >= T::BlockGasLimit::get() {
408+
return Err(InvalidTransaction::Custom(
409+
TransactionValidationError::InvalidGasLimit as u8,
410+
)
411+
.into());
412+
}
413+
414+
let account_data = pallet_evm::Pallet::<T>::account_basic(&origin);
415+
416+
let fee = transaction.gas_price.saturating_mul(transaction.gas_limit);
417+
let total_payment = transaction.value.saturating_add(fee);
418+
if account_data.balance < total_payment {
419+
return Err(InvalidTransaction::Payment.into());
420+
}
421+
422+
let min_gas_price = T::FeeCalculator::min_gas_price();
423+
424+
if transaction.gas_price < min_gas_price {
425+
return Err(InvalidTransaction::Payment.into());
426+
}
427+
428+
Ok(account_data.nonce)
429+
}
430+
431+
// Controls that must be performed by the pool.
432+
// The controls common with the State Transition Function (STF) are in
433+
// the function `validate_transaction_common`.
434+
fn validate_transaction_in_pool(
435+
origin: H160,
436+
transaction: &Transaction,
437+
) -> TransactionValidity {
438+
let account_nonce = Self::validate_transaction_common(origin, transaction)?;
439+
440+
if transaction.nonce < account_nonce {
441+
return Err(InvalidTransaction::Stale.into());
442+
}
443+
444+
// The tag provides and requires must be filled correctly according to the nonce.
445+
let mut builder = ValidTransactionBuilder::default()
446+
.and_provides((origin, transaction.nonce))
447+
.priority(transaction.gas_price.unique_saturated_into());
448+
449+
// In the context of the pool, a transaction with
450+
// too high a nonce is still considered valid
451+
if transaction.nonce > account_nonce {
452+
if let Some(prev_nonce) = transaction.nonce.checked_sub(1.into()) {
453+
builder = builder.and_requires((origin, prev_nonce))
454+
}
455+
}
456+
457+
builder.build()
458+
}
459+
460+
fn apply_validated_transaction(
461+
source: H160,
462+
transaction: Transaction,
463+
) -> DispatchResultWithPostInfo {
422464
let transaction_hash =
423465
H256::from_slice(Keccak256::digest(&rlp::encode(&transaction)).as_slice());
424466
let transaction_index = Pending::<T>::get().len() as u32;
@@ -565,6 +607,29 @@ impl<T: Config> Pallet<T> {
565607
}
566608
}
567609
}
610+
611+
/// Validate an Ethereum transaction already in block
612+
///
613+
/// This function must be called during the pre-dispatch phase
614+
/// (just before applying the extrinsic).
615+
pub fn validate_transaction_in_block(
616+
origin: H160,
617+
transaction: &ethereum::TransactionV0,
618+
) -> Result<(), TransactionValidityError> {
619+
let account_nonce = Self::validate_transaction_common(origin, transaction)?;
620+
621+
// In the context of the block, a transaction with a nonce that is
622+
// too high should be considered invalid and make the whole block invalid.
623+
if transaction.nonce > account_nonce {
624+
Err(TransactionValidityError::Invalid(
625+
InvalidTransaction::Future,
626+
))
627+
} else if transaction.nonce < account_nonce {
628+
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))
629+
} else {
630+
Ok(())
631+
}
632+
}
568633
}
569634

570635
#[derive(Eq, PartialEq, Clone, sp_runtime::RuntimeDebug)]

frame/ethereum/src/mock.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
2020
use super::*;
2121
use crate::IntermediateStateRoot;
22+
use codec::{WrapperTypeDecode, WrapperTypeEncode};
2223
use ethereum::{TransactionAction, TransactionSignature};
2324
use frame_support::{parameter_types, traits::FindAuthor, ConsensusEngineId, PalletId};
2425
use pallet_evm::{AddressMapping, EnsureAddressTruncated, FeeCalculator};
@@ -27,11 +28,13 @@ use sha3::Digest;
2728
use sp_core::{H160, H256, U256};
2829
use sp_runtime::{
2930
testing::Header,
30-
traits::{BlakeTwo256, IdentityLookup},
31+
traits::{BlakeTwo256, IdentityLookup, SignedExtension},
3132
AccountId32,
3233
};
3334

34-
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
35+
pub type SignedExtra = (frame_system::CheckSpecVersion<Test>,);
36+
37+
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test, (), SignedExtra>;
3538
type Block = frame_system::mocking::MockBlock<Test>;
3639

3740
frame_support::construct_runtime! {
@@ -166,6 +169,54 @@ impl crate::Config for Test {
166169
type StateRoot = IntermediateStateRoot;
167170
}
168171

172+
impl fp_self_contained::SelfContainedCall for Call {
173+
type SignedInfo = H160;
174+
175+
fn is_self_contained(&self) -> bool {
176+
match self {
177+
Call::Ethereum(call) => call.is_self_contained(),
178+
_ => false,
179+
}
180+
}
181+
182+
fn check_self_contained(&self) -> Option<Result<Self::SignedInfo, TransactionValidityError>> {
183+
match self {
184+
Call::Ethereum(call) => call.check_self_contained(),
185+
_ => None,
186+
}
187+
}
188+
189+
fn validate_self_contained(&self, info: &Self::SignedInfo) -> Option<TransactionValidity> {
190+
match self {
191+
Call::Ethereum(call) => call.validate_self_contained(info),
192+
_ => None,
193+
}
194+
}
195+
196+
fn pre_dispatch_self_contained(
197+
&self,
198+
info: &Self::SignedInfo,
199+
) -> Option<Result<(), TransactionValidityError>> {
200+
match self {
201+
Call::Ethereum(call) => call.pre_dispatch_self_contained(info),
202+
_ => None,
203+
}
204+
}
205+
206+
fn apply_self_contained(
207+
self,
208+
info: Self::SignedInfo,
209+
) -> Option<sp_runtime::DispatchResultWithInfo<sp_runtime::traits::PostDispatchInfoOf<Self>>> {
210+
use sp_runtime::traits::Dispatchable as _;
211+
match self {
212+
call @ Call::Ethereum(crate::Call::transact(_)) => {
213+
Some(call.dispatch(Origin::from(crate::RawOrigin::EthereumTransaction(info))))
214+
}
215+
_ => None,
216+
}
217+
}
218+
}
219+
169220
pub struct AccountInfo {
170221
pub address: H160,
171222
pub account_id: AccountId32,
@@ -255,6 +306,10 @@ impl UnsignedTransaction {
255306
}
256307

257308
pub fn sign(&self, key: &H256) -> Transaction {
309+
self.sign_with_chain_id(key, ChainId::get())
310+
}
311+
312+
pub fn sign_with_chain_id(&self, key: &H256, chain_id: u64) -> Transaction {
258313
let hash = self.signing_hash();
259314
let msg = libsecp256k1::Message::parse(hash.as_fixed_bytes());
260315
let s = libsecp256k1::sign(
@@ -264,7 +319,7 @@ impl UnsignedTransaction {
264319
let sig = s.0.serialize();
265320

266321
let sig = TransactionSignature::new(
267-
s.1.serialize() as u64 % 2 + ChainId::get() * 2 + 35,
322+
s.1.serialize() as u64 % 2 + chain_id * 2 + 35,
268323
H256::from_slice(&sig[0..32]),
269324
H256::from_slice(&sig[32..64]),
270325
)

0 commit comments

Comments
 (0)