Skip to content

Commit

Permalink
Make transactions receipts part of transaction inclusion proof (parit…
Browse files Browse the repository at this point in the history
…ytech#236)

* make receipts part of tx proof

* is_successful_raw_receipt_with_empty_data

* cargo fmt --all

* clippy

* fix everything
  • Loading branch information
svyatonik authored and serban300 committed Apr 9, 2024
1 parent 1f61d4e commit e2045bf
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 75 deletions.
58 changes: 39 additions & 19 deletions bridges/bin/node/runtime/src/exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
use codec::{Decode, Encode};
use frame_support::RuntimeDebug;
use hex_literal::hex;
use sp_bridge_eth_poa::{transaction_decode, RawTransaction};
use sp_bridge_eth_poa::{transaction_decode_rlp, RawTransaction, RawTransactionReceipt};
use sp_currency_exchange::{
Error as ExchangeError, LockFundsTransaction, MaybeLockFundsTransaction, Result as ExchangeResult,
};
Expand All @@ -46,8 +46,9 @@ pub struct EthereumTransactionInclusionProof {
pub block: sp_core::H256,
/// Index of the transaction within the block.
pub index: u64,
/// The proof itself (right now it is all RLP-encoded transactions of the block).
pub proof: Vec<RawTransaction>,
/// The proof itself (right now it is all RLP-encoded transactions of the block +
/// RLP-encoded receipts of all transactions of the block).
pub proof: Vec<(RawTransaction, RawTransactionReceipt)>,
}

/// We uniquely identify transfer by the pair (sender, nonce).
Expand Down Expand Up @@ -76,7 +77,7 @@ impl MaybeLockFundsTransaction for EthTransaction {
fn parse(
raw_tx: &Self::Transaction,
) -> ExchangeResult<LockFundsTransaction<Self::Id, Self::Recipient, Self::Amount>> {
let tx = transaction_decode(raw_tx).map_err(|_| ExchangeError::InvalidTransaction)?;
let tx = transaction_decode_rlp(raw_tx).map_err(|_| ExchangeError::InvalidTransaction)?;

// we only accept transactions sending funds directly to the pre-configured address
if tx.unsigned.to != Some(LOCK_FUNDS_ADDRESS.into()) {
Expand Down Expand Up @@ -128,7 +129,7 @@ impl MaybeLockFundsTransaction for EthTransaction {
/// Prepares everything required to bench claim of funds locked by given transaction.
#[cfg(feature = "runtime-benchmarks")]
pub(crate) fn prepare_environment_for_claim<T: pallet_bridge_eth_poa::Trait<I>, I: pallet_bridge_eth_poa::Instance>(
transactions: &[RawTransaction],
transactions: &[(RawTransaction, RawTransactionReceipt)],
) -> sp_bridge_eth_poa::H256 {
use pallet_bridge_eth_poa::{
test_utils::{insert_header, validator_utils::validator, HeaderBuilder},
Expand All @@ -138,7 +139,8 @@ pub(crate) fn prepare_environment_for_claim<T: pallet_bridge_eth_poa::Trait<I>,

let mut storage = BridgeStorage::<T, I>::new();
let header = HeaderBuilder::with_parent_number_on_runtime::<T, I>(0)
.with_transactions_root(compute_merkle_root(transactions.iter()))
.transactions_root(compute_merkle_root(transactions.iter().map(|(tx, _)| tx)))
.receipts_root(compute_merkle_root(transactions.iter().map(|(_, receipt)| receipt)))
.sign_by(&validator(0));
let header_id = header.compute_id();
insert_header(&mut storage, header);
Expand All @@ -152,8 +154,8 @@ pub(crate) fn prepare_environment_for_claim<T: pallet_bridge_eth_poa::Trait<I>,
pub(crate) fn prepare_ethereum_transaction(
recipient: &crate::AccountId,
editor: impl Fn(&mut sp_bridge_eth_poa::UnsignedTransaction),
) -> Vec<u8> {
use sp_bridge_eth_poa::signatures::SignTransaction;
) -> (RawTransaction, RawTransactionReceipt) {
use sp_bridge_eth_poa::{signatures::SignTransaction, Receipt, TransactionOutcome};

// prepare tx for OpenEthereum private dev chain:
// chain id is 0x11
Expand All @@ -173,7 +175,16 @@ pub(crate) fn prepare_ethereum_transaction(
payload: recipient_raw.to_vec(),
};
editor(&mut eth_tx);
eth_tx.sign_by(&signer, Some(chain_id))
(
eth_tx.sign_by(&signer, Some(chain_id)),
Receipt {
outcome: TransactionOutcome::StatusCode(1),
gas_used: Default::default(),
log_bloom: Default::default(),
logs: Vec::new(),
}
.rlp(),
)
}

#[cfg(test)]
Expand All @@ -188,7 +199,7 @@ mod tests {
#[test]
fn valid_transaction_accepted() {
assert_eq!(
EthTransaction::parse(&prepare_ethereum_transaction(&ferdie(), |_| {})),
EthTransaction::parse(&prepare_ethereum_transaction(&ferdie(), |_| {}).0),
Ok(LockFundsTransaction {
id: EthereumTransactionTag {
account: hex!("00a329c0648769a73afac7f9381e08fb43dbea72"),
Expand All @@ -211,29 +222,38 @@ mod tests {
#[test]
fn transaction_with_invalid_peer_recipient_rejected() {
assert_eq!(
EthTransaction::parse(&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.to = None;
})),
EthTransaction::parse(
&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.to = None;
})
.0
),
Err(ExchangeError::InvalidTransaction),
);
}

#[test]
fn transaction_with_invalid_recipient_rejected() {
assert_eq!(
EthTransaction::parse(&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.payload.clear();
})),
EthTransaction::parse(
&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.payload.clear();
})
.0
),
Err(ExchangeError::InvalidRecipient),
);
}

#[test]
fn transaction_with_invalid_amount_rejected() {
assert_eq!(
EthTransaction::parse(&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.value = sp_core::U256::from(u128::max_value()) + sp_core::U256::from(1);
})),
EthTransaction::parse(
&prepare_ethereum_transaction(&ferdie(), |tx| {
tx.value = sp_core::U256::from(u128::max_value()) + sp_core::U256::from(1);
})
.0
),
Err(ExchangeError::InvalidAmount),
);
}
Expand Down
2 changes: 1 addition & 1 deletion bridges/bin/node/runtime/src/kovan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ impl PeerBlockchain for KovanBlockchain {
return None;
}

proof.proof.get(proof.index as usize).cloned()
proof.proof.get(proof.index as usize).map(|(tx, _)| tx.clone())
}
}

Expand Down
4 changes: 2 additions & 2 deletions bridges/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ impl_runtime_apis! {
).unwrap();
}

let transaction = crate::exchange::prepare_ethereum_transaction(
let (transaction, receipt) = crate::exchange::prepare_ethereum_transaction(
&proof_params.recipient,
|tx| {
// our runtime only supports transactions where data is exactly 32 bytes long
Expand All @@ -725,7 +725,7 @@ impl_runtime_apis! {
tx.value = (ExistentialDeposit::get() * 10).into();
},
);
let transactions = sp_std::iter::repeat(transaction.clone())
let transactions = sp_std::iter::repeat((transaction, receipt))
.take(1 + proof_params.proof_size_factor as usize)
.collect::<Vec<_>>();
let block_hash = crate::exchange::prepare_environment_for_claim::<Runtime, Kovan>(&transactions);
Expand Down
2 changes: 1 addition & 1 deletion bridges/bin/node/runtime/src/rialto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ impl PeerBlockchain for RialtoBlockchain {
return None;
}

proof.proof.get(proof.index as usize).cloned()
proof.proof.get(proof.index as usize).map(|(tx, _)| tx.clone())
}
}

Expand Down
122 changes: 111 additions & 11 deletions bridges/modules/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use crate::finality::{CachedFinalityVotes, FinalityVotes};
use codec::{Decode, Encode};
use frame_support::{decl_module, decl_storage, traits::Get};
use primitives::{Address, Header, HeaderId, RawTransaction, Receipt, H256, U256};
use primitives::{Address, Header, HeaderId, RawTransaction, RawTransactionReceipt, Receipt, H256, U256};
use sp_runtime::{
transaction_validity::{
InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionSource, TransactionValidity,
Expand Down Expand Up @@ -510,7 +510,11 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
}

/// Verify that transaction is included into given finalized block.
pub fn verify_transaction_finalized(block: H256, tx_index: u64, proof: &[RawTransaction]) -> bool {
pub fn verify_transaction_finalized(
block: H256,
tx_index: u64,
proof: &[(RawTransaction, RawTransactionReceipt)],
) -> bool {
crate::verify_transaction_finalized(&BridgeStorage::<T, I>::new(), block, tx_index, proof)
}
}
Expand Down Expand Up @@ -886,7 +890,7 @@ pub fn verify_transaction_finalized<S: Storage>(
storage: &S,
block: H256,
tx_index: u64,
proof: &[RawTransaction],
proof: &[(RawTransaction, RawTransactionReceipt)],
) -> bool {
if tx_index >= proof.len() as _ {
return false;
Expand Down Expand Up @@ -914,7 +918,21 @@ pub fn verify_transaction_finalized<S: Storage>(
return false;
}

header.verify_transactions_root(proof)
// verify that transaction is included in the block
if !header.verify_transactions_root(proof.iter().map(|(tx, _)| tx)) {
return false;
}

// verify that transaction receipt is included in the block
if !header.verify_raw_receipts_root(proof.iter().map(|(_, r)| r)) {
return false;
}

// check that transaction has completed successfully
matches!(
Receipt::is_successful_raw_receipt(&proof[tx_index as usize].1),
Ok(true)
)
}

/// Transaction pool configuration.
Expand Down Expand Up @@ -954,10 +972,31 @@ pub(crate) mod tests {
vec![42]
}

fn example_tx_receipt(success: bool) -> Vec<u8> {
Receipt {
// the only thing that we care of:
outcome: primitives::TransactionOutcome::StatusCode(if success { 1 } else { 0 }),
gas_used: Default::default(),
log_bloom: Default::default(),
logs: Vec::new(),
}
.rlp()
}

fn example_header_with_failed_receipt() -> Header {
let mut header = Header::default();
header.number = 3;
header.transactions_root = compute_merkle_root(vec![example_tx()].into_iter());
header.receipts_root = compute_merkle_root(vec![example_tx_receipt(false)].into_iter());
header.parent_hash = example_header().compute_hash();
header
}

fn example_header() -> Header {
let mut header = Header::default();
header.number = 2;
header.transactions_root = compute_merkle_root(vec![example_tx()].into_iter());
header.receipts_root = compute_merkle_root(vec![example_tx_receipt(true)].into_iter());
header.parent_hash = example_header_parent().compute_hash();
header
}
Expand All @@ -966,6 +1005,7 @@ pub(crate) mod tests {
let mut header = Header::default();
header.number = 1;
header.transactions_root = compute_merkle_root(vec![example_tx()].into_iter());
header.receipts_root = compute_merkle_root(vec![example_tx_receipt(true)].into_iter());
header.parent_hash = genesis().compute_hash();
header
}
Expand Down Expand Up @@ -1265,7 +1305,12 @@ pub(crate) mod tests {
run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| {
let storage = BridgeStorage::<TestRuntime>::new();
assert_eq!(
verify_transaction_finalized(&storage, example_header().compute_hash(), 0, &[example_tx()],),
verify_transaction_finalized(
&storage,
example_header().compute_hash(),
0,
&[(example_tx(), example_tx_receipt(true))],
),
true,
);
});
Expand All @@ -1279,7 +1324,12 @@ pub(crate) mod tests {
insert_header(&mut storage, example_header());
storage.finalize_and_prune_headers(Some(example_header().compute_id()), 0);
assert_eq!(
verify_transaction_finalized(&storage, example_header_parent().compute_hash(), 0, &[example_tx()],),
verify_transaction_finalized(
&storage,
example_header_parent().compute_hash(),
0,
&[(example_tx(), example_tx_receipt(true))],
),
true,
);
});
Expand Down Expand Up @@ -1314,7 +1364,12 @@ pub(crate) mod tests {
insert_header(&mut storage, example_header_parent());
insert_header(&mut storage, example_header());
assert_eq!(
verify_transaction_finalized(&storage, example_header().compute_hash(), 0, &[example_tx()],),
verify_transaction_finalized(
&storage,
example_header().compute_hash(),
0,
&[(example_tx(), example_tx_receipt(true))],
),
false,
);
});
Expand All @@ -1333,7 +1388,12 @@ pub(crate) mod tests {
insert_header(&mut storage, finalized_header_sibling);
storage.finalize_and_prune_headers(Some(example_header().compute_id()), 0);
assert_eq!(
verify_transaction_finalized(&storage, finalized_header_sibling_hash, 0, &[example_tx()],),
verify_transaction_finalized(
&storage,
finalized_header_sibling_hash,
0,
&[(example_tx(), example_tx_receipt(true))],
),
false,
);
});
Expand All @@ -1352,22 +1412,62 @@ pub(crate) mod tests {
insert_header(&mut storage, example_header());
storage.finalize_and_prune_headers(Some(example_header().compute_id()), 0);
assert_eq!(
verify_transaction_finalized(&storage, finalized_header_uncle_hash, 0, &[example_tx()],),
verify_transaction_finalized(
&storage,
finalized_header_uncle_hash,
0,
&[(example_tx(), example_tx_receipt(true))],
),
false,
);
});
}

#[test]
fn verify_transaction_finalized_rejects_invalid_proof() {
fn verify_transaction_finalized_rejects_invalid_transactions_in_proof() {
run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| {
let storage = BridgeStorage::<TestRuntime>::new();
assert_eq!(
verify_transaction_finalized(
&storage,
example_header().compute_hash(),
0,
&[example_tx(), example_tx()],
&[
(example_tx(), example_tx_receipt(true)),
(example_tx(), example_tx_receipt(true))
],
),
false,
);
});
}

#[test]
fn verify_transaction_finalized_rejects_invalid_receipts_in_proof() {
run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| {
let storage = BridgeStorage::<TestRuntime>::new();
assert_eq!(
verify_transaction_finalized(
&storage,
example_header().compute_hash(),
0,
&[(example_tx(), vec![42])],
),
false,
);
});
}

#[test]
fn verify_transaction_finalized_rejects_failed_transaction() {
run_test_with_genesis(example_header_with_failed_receipt(), TOTAL_VALIDATORS, |_| {
let storage = BridgeStorage::<TestRuntime>::new();
assert_eq!(
verify_transaction_finalized(
&storage,
example_header_with_failed_receipt().compute_hash(),
0,
&[(example_tx(), example_tx_receipt(false))],
),
false,
);
Expand Down
2 changes: 1 addition & 1 deletion bridges/modules/ethereum/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ impl HeaderBuilder {
}

/// Update transactions root field of this header.
pub fn with_transactions_root(mut self, transactions_root: H256) -> Self {
pub fn transactions_root(mut self, transactions_root: H256) -> Self {
self.header.transactions_root = transactions_root;
self
}
Expand Down
Loading

0 comments on commit e2045bf

Please sign in to comment.