diff --git a/bridges/bin/node/runtime/Cargo.toml b/bridges/bin/node/runtime/Cargo.toml index d8e5a8a92a..e3cc405ee9 100644 --- a/bridges/bin/node/runtime/Cargo.toml +++ b/bridges/bin/node/runtime/Cargo.toml @@ -9,6 +9,9 @@ repository = "https://github.com/paritytech/parity-bridges-common/" [dependencies] hex-literal = "0.2" +[dev-dependencies] +ethereum-tx-sign = "3.0" + [dependencies.codec] package = "parity-scale-codec" version = "1.0.0" @@ -38,6 +41,11 @@ version = "0.1.0" default-features = false path = "../../../modules/ethereum" +[dependencies.pallet-bridge-currency-exchange] +version = "0.1.0" +default-features = false +path = "../../../modules/currency-exchange" + [dependencies.frame-support] version = "2.0.0-rc1" default-features = false @@ -116,6 +124,11 @@ version = "0.1.0" default-features = false path = "../../../primitives/ethereum-poa" +[dependencies.sp-currency-exchange] +version = "0.1.0" +default-features = false +path = "../../../primitives/currency-exchange" + [dependencies.sp-consensus-aura] version = "0.8.0-rc1" default-features = false @@ -194,6 +207,7 @@ std = [ "pallet-aura/std", "pallet-balances/std", "pallet-bridge-eth-poa/std", + "pallet-bridge-currency-exchange/std", "codec/std", "frame-executive/std", "frame-support/std", @@ -205,6 +219,7 @@ std = [ "sp-api/std", "sp-block-builder/std", "sp-bridge-eth-poa/std", + "sp-currency-exchange/std", "sp-consensus-aura/std", "sp-core/std", "sp-inherents/std", diff --git a/bridges/bin/node/runtime/src/exchange.rs b/bridges/bin/node/runtime/src/exchange.rs new file mode 100644 index 0000000000..e269882a50 --- /dev/null +++ b/bridges/bin/node/runtime/src/exchange.rs @@ -0,0 +1,229 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Support for PoA -> Substrate native tokens exchange. +//! +//! If you want to exchange native PoA tokens for native Substrate +//! chain tokens, you need to: +//! 1) send some PoA tokens to `LOCK_FUNDS_ADDRESS` address on PoA chain. Data field of +//! the transaction must be SCALE-encoded id of Substrate account that will receive +//! funds on Substrate chain; +//! 2) wait until the 'lock funds' transaction is mined on PoA chain; +//! 3) wait until the block containing the 'lock funds' transaction is finalized on PoA chain; +//! 4) wait until the required PoA header and its finality are provided +//! to the PoA -> Substrate bridge module (it can be provided by you); +//! 5) receive tokens by providing proof-of-inclusion of PoA transaction. + +use codec::{Decode, Encode}; +use frame_support::RuntimeDebug; +use hex_literal::hex; +use pallet_bridge_currency_exchange::Blockchain; +use sp_bridge_eth_poa::transaction_decode; +use sp_currency_exchange::{ + Error as ExchangeError, LockFundsTransaction, MaybeLockFundsTransaction, Result as ExchangeResult, +}; +use sp_std::vec::Vec; + +/// Ethereum address where locked PoA funds must be sent to. +const LOCK_FUNDS_ADDRESS: [u8; 20] = hex!("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"); + +/// Ethereum transaction inclusion proof. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug)] +pub struct EthereumTransactionInclusionProof { + /// Hash of the block with transaction. + 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>, +} + +/// We uniquely identify transfer by the pair (sender, nonce). +/// +/// The assumption is that this pair will never appear more than once in +/// transactions included into finalized blocks. This is obviously true +/// for any existing eth-like chain (that keep current tx format), because +/// otherwise transaction can be replayed over and over. +#[derive(Encode, Decode, PartialEq, RuntimeDebug)] +pub struct EthereumTransactionTag { + /// Account that has locked funds. + pub account: [u8; 20], + /// Lock transaction nonce. + pub nonce: sp_core::U256, +} + +/// Eth blockchain from runtime perspective. +pub struct EthBlockchain; + +impl Blockchain for EthBlockchain { + type Transaction = Vec; + type TransactionInclusionProof = EthereumTransactionInclusionProof; + + fn verify_transaction_inclusion_proof(proof: &Self::TransactionInclusionProof) -> Option { + let is_transaction_finalized = + crate::BridgeEthPoA::verify_transaction_finalized(proof.block, proof.index, &proof.proof); + + if !is_transaction_finalized { + return None; + } + + proof.proof.get(proof.index as usize).cloned() + } +} + +/// Eth transaction from runtime perspective. +pub struct EthTransaction; + +impl MaybeLockFundsTransaction for EthTransaction { + type Transaction = Vec; + type Id = EthereumTransactionTag; + type Recipient = crate::AccountId; + type Amount = crate::Balance; + + fn parse( + raw_tx: &Self::Transaction, + ) -> ExchangeResult> { + let tx = transaction_decode(raw_tx).map_err(|_| ExchangeError::InvalidTransaction)?; + + // we only accept transactions sending funds directly to the pre-configured address + if tx.to != Some(LOCK_FUNDS_ADDRESS.into()) { + frame_support::debug::error!( + target: "runtime", + "Failed to parse fund locks transaction. Invalid peer recipient: {:?}", + tx.to, + ); + + return Err(ExchangeError::InvalidTransaction); + } + + let mut recipient_raw = sp_core::H256::default(); + match tx.payload.len() { + 32 => recipient_raw.as_fixed_bytes_mut().copy_from_slice(&tx.payload), + len => { + frame_support::debug::error!( + target: "runtime", + "Failed to parse fund locks transaction. Invalid recipient length: {}", + len, + ); + + return Err(ExchangeError::InvalidRecipient); + } + } + let amount = tx.value.low_u128(); + + if tx.value != amount.into() { + frame_support::debug::error!( + target: "runtime", + "Failed to parse fund locks transaction. Invalid amount: {}", + tx.value, + ); + + return Err(ExchangeError::InvalidAmount); + } + + Ok(LockFundsTransaction { + id: EthereumTransactionTag { + account: *tx.sender.as_fixed_bytes(), + nonce: tx.nonce, + }, + recipient: crate::AccountId::from(*recipient_raw.as_fixed_bytes()), + amount, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + fn ferdie() -> crate::AccountId { + hex!("1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c").into() + } + + fn prepare_ethereum_transaction(editor: impl Fn(&mut ethereum_tx_sign::RawTransaction)) -> Vec { + // prepare tx for OpenEthereum private dev chain: + // chain id is 0x11 + // sender secret is 0x4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7 + let chain_id = 0x11_u64; + let signer = hex!("4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7"); + let ferdie_id = ferdie(); + let ferdie_raw: &[u8; 32] = ferdie_id.as_ref(); + let mut eth_tx = ethereum_tx_sign::RawTransaction { + nonce: 0.into(), + to: Some(LOCK_FUNDS_ADDRESS.into()), + value: 100.into(), + gas: 100_000.into(), + gas_price: 100_000.into(), + data: ferdie_raw.to_vec(), + }; + editor(&mut eth_tx); + eth_tx.sign(&signer.into(), &chain_id) + } + + #[test] + fn valid_transaction_accepted() { + assert_eq!( + EthTransaction::parse(&prepare_ethereum_transaction(|_| {})), + Ok(LockFundsTransaction { + id: EthereumTransactionTag { + account: hex!("00a329c0648769a73afac7f9381e08fb43dbea72"), + nonce: 0.into(), + }, + recipient: ferdie(), + amount: 100, + }), + ); + } + + #[test] + fn invalid_transaction_rejected() { + assert_eq!( + EthTransaction::parse(&Vec::new()), + Err(ExchangeError::InvalidTransaction), + ); + } + + #[test] + fn transaction_with_invalid_peer_recipient_rejected() { + assert_eq!( + EthTransaction::parse(&prepare_ethereum_transaction(|tx| { + tx.to = None; + })), + Err(ExchangeError::InvalidTransaction), + ); + } + + #[test] + fn transaction_with_invalid_recipient_rejected() { + assert_eq!( + EthTransaction::parse(&prepare_ethereum_transaction(|tx| { + tx.data.clear(); + })), + Err(ExchangeError::InvalidRecipient), + ); + } + + #[test] + fn transaction_with_invalid_amount_rejected() { + assert_eq!( + EthTransaction::parse(&prepare_ethereum_transaction(|tx| { + tx.value = sp_core::U256::from(u128::max_value()) + sp_core::U256::from(1); + })), + Err(ExchangeError::InvalidAmount), + ); + } +} diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs index d02d108d51..eef870a3f5 100644 --- a/bridges/bin/node/runtime/src/lib.rs +++ b/bridges/bin/node/runtime/src/lib.rs @@ -24,6 +24,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +mod exchange; + pub mod kovan; use codec::{Decode, Encode}; @@ -47,11 +49,12 @@ use sp_version::RuntimeVersion; // A few exports that help ease life for downstream crates. pub use frame_support::{ construct_runtime, parameter_types, - traits::{KeyOwnerProofSystem, Randomness}, + traits::{Currency, ExistenceRequirement, KeyOwnerProofSystem, Randomness}, weights::{IdentityFee, RuntimeDbWeight, Weight}, StorageValue, }; pub use pallet_balances::Call as BalancesCall; +pub use pallet_bridge_currency_exchange::Call as BridgeCurrencyExchangeCall; pub use pallet_bridge_eth_poa::Call as BridgeEthPoACall; pub use pallet_timestamp::Call as TimestampCall; #[cfg(any(feature = "std", test))] @@ -223,6 +226,46 @@ impl pallet_bridge_eth_poa::Trait for Runtime { type OnHeadersSubmitted = (); } +impl pallet_bridge_currency_exchange::Trait for Runtime { + type OnTransactionSubmitted = (); + type PeerBlockchain = exchange::EthBlockchain; + type PeerMaybeLockFundsTransaction = exchange::EthTransaction; + type RecipientsMap = sp_currency_exchange::IdentityRecipients; + type Amount = Balance; + type CurrencyConverter = sp_currency_exchange::IdentityCurrencyConverter; + type DepositInto = DepositInto; +} + +pub struct DepositInto; + +impl sp_currency_exchange::DepositInto for DepositInto { + type Recipient = AccountId; + type Amount = Balance; + + fn deposit_into(recipient: Self::Recipient, amount: Self::Amount) -> sp_currency_exchange::Result<()> { + as Currency>::deposit_into_existing(&recipient, amount) + .map(|_| { + frame_support::debug::trace!( + target: "runtime", + "Deposited {} to {:?}", + amount, + recipient, + ); + }) + .map_err(|e| { + frame_support::debug::error!( + target: "runtime", + "Deposit of {} to {:?} has failed with: {:?}", + amount, + recipient, + e + ); + + sp_currency_exchange::Error::DepositFailed + }) + } +} + impl pallet_grandpa::Trait for Runtime { type Event = Event; type Call = Call; @@ -364,6 +407,7 @@ construct_runtime!( Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, Session: pallet_session::{Module, Call, Storage, Event, Config}, BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage, ValidateUnsigned}, + BridgeCurrencyExchange: pallet_bridge_currency_exchange::{Module, Call}, } ); diff --git a/bridges/modules/currency-exchange/Cargo.toml b/bridges/modules/currency-exchange/Cargo.toml new file mode 100644 index 0000000000..cd3d112119 --- /dev/null +++ b/bridges/modules/currency-exchange/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "pallet-bridge-currency-exchange" +description = "A Substrate Runtime module that accepts 'lock funds' transactions from a peer chain and grants an equivalent amount to a the appropriate Substrate account." +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +serde = { version = "1.0", optional = true } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } +sp-currency-exchange = { path = "../../primitives/currency-exchange", default-features = false } + +# Substrate Based Dependencies +[dependencies.frame-support] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[dependencies.frame-system] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-std] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[dependencies.sp-runtime] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[dev-dependencies.sp-core] +version = "2.0.0-rc1" +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[dev-dependencies.sp-io] +version = "2.0.0-rc1" +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate/" + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "sp-currency-exchange/std", +] diff --git a/bridges/modules/currency-exchange/src/lib.rs b/bridges/modules/currency-exchange/src/lib.rs new file mode 100644 index 0000000000..a4ef7360f9 --- /dev/null +++ b/bridges/modules/currency-exchange/src/lib.rs @@ -0,0 +1,418 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! Runtime module that allows tokens exchange between two bridged chains. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{decl_error, decl_module, decl_storage, ensure, Parameter}; +use sp_currency_exchange::{ + CurrencyConverter, DepositInto, Error as ExchangeError, MaybeLockFundsTransaction, RecipientsMap, +}; +use sp_runtime::DispatchResult; + +/// Called when transaction is submitted to the exchange module. +pub trait OnTransactionSubmitted { + /// Called when valid transaction is submitted and accepted by the module. + fn on_valid_transaction_submitted(submitter: AccountId); +} + +/// Peer blockhain interface. +pub trait Blockchain { + /// Transaction type. + type Transaction: Parameter; + /// Transaction inclusion proof type. + type TransactionInclusionProof: Parameter; + + /// Verify that transaction is a part of given block. + /// + /// Returns Some(transaction) if proof is valid and None otherwise. + fn verify_transaction_inclusion_proof(proof: &Self::TransactionInclusionProof) -> Option; +} + +/// The module configuration trait +pub trait Trait: frame_system::Trait { + /// Handler for transaction submission result. + type OnTransactionSubmitted: OnTransactionSubmitted; + /// Peer blockchain type. + type PeerBlockchain: Blockchain; + /// Peer blockchain transaction parser. + type PeerMaybeLockFundsTransaction: MaybeLockFundsTransaction< + Transaction = ::Transaction, + >; + /// Map between blockchains recipients. + type RecipientsMap: RecipientsMap< + PeerRecipient = ::Recipient, + Recipient = Self::AccountId, + >; + /// This blockchain currency amount type. + type Amount; + /// Converter from peer blockchain currency type into current blockchain currency type. + type CurrencyConverter: CurrencyConverter< + SourceAmount = ::Amount, + TargetAmount = Self::Amount, + >; + /// Something that could grant money. + type DepositInto: DepositInto; +} + +decl_error! { + pub enum Error for Module { + /// Invalid peer blockchain transaction provided. + InvalidTransaction, + /// Peer transaction has invalid amount. + InvalidAmount, + /// Peer transaction has invalid recipient. + InvalidRecipient, + /// Cannot map from peer recipient to this blockchain recipient. + FailedToMapRecipients, + /// Failed to convert from peer blockchain currency to this blockhain currency. + FailedToConvertCurrency, + /// Deposit has failed. + DepositFailed, + /// Transaction is not finalized. + UnfinalizedTransaction, + /// Transaction funds are already claimed. + AlreadyClaimed, + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + /// Imports lock fund transaction of the peer blockchain. + #[weight = 0] // TODO: update me (https://github.com/paritytech/parity-bridges-common/issues/78) + pub fn import_peer_transaction( + origin, + proof: <::PeerBlockchain as Blockchain>::TransactionInclusionProof, + ) -> DispatchResult { + let submitter = frame_system::ensure_signed(origin)?; + + // ensure that transaction is included in finalized block that we know of + let transaction = ::PeerBlockchain::verify_transaction_inclusion_proof( + &proof, + ).ok_or_else(|| Error::::UnfinalizedTransaction)?; + + // parse transaction + let transaction = ::PeerMaybeLockFundsTransaction::parse(&transaction) + .map_err(Error::::from)?; + let transfer_id = transaction.id; + ensure!( + !Transfers::::contains_key(&transfer_id), + Error::::AlreadyClaimed + ); + + // grant recipient + let recipient = T::RecipientsMap::map(transaction.recipient).map_err(Error::::from)?; + let amount = T::CurrencyConverter::convert(transaction.amount).map_err(Error::::from)?; + + // make sure to update the mapping if we deposit successfully to avoid double spending, + // i.e. whenever `deposit_into` is successful we MUST update `Transfers`. + { + T::DepositInto::deposit_into(recipient, amount).map_err(Error::::from)?; + Transfers::::insert(transfer_id, ()) + } + + // reward submitter for providing valid message + T::OnTransactionSubmitted::on_valid_transaction_submitted(submitter); + + Ok(()) + } + } +} + +decl_storage! { + trait Store for Module as Bridge { + /// All transfers that have already been claimed. + Transfers: map hasher(blake2_128_concat) ::Id => (); + } +} + +impl From for Error { + fn from(error: ExchangeError) -> Self { + match error { + ExchangeError::InvalidTransaction => Error::InvalidTransaction, + ExchangeError::InvalidAmount => Error::InvalidAmount, + ExchangeError::InvalidRecipient => Error::InvalidRecipient, + ExchangeError::FailedToMapRecipients => Error::FailedToMapRecipients, + ExchangeError::FailedToConvertCurrency => Error::FailedToConvertCurrency, + ExchangeError::DepositFailed => Error::DepositFailed, + } + } +} + +impl OnTransactionSubmitted for () { + fn on_valid_transaction_submitted(_: AccountId) {} +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{assert_noop, assert_ok, impl_outer_origin, parameter_types, weights::Weight}; + use sp_core::H256; + use sp_currency_exchange::LockFundsTransaction; + use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, + }; + + type AccountId = u64; + + const INVALID_TRANSACTION_ID: u64 = 100; + const ALREADY_CLAIMED_TRANSACTION_ID: u64 = 101; + const UNKNOWN_RECIPIENT_ID: u64 = 0; + const INVALID_AMOUNT: u64 = 0; + const MAX_DEPOSIT_AMOUNT: u64 = 1000; + const SUBMITTER: u64 = 2000; + + type RawTransaction = LockFundsTransaction; + + pub struct DummyTransactionSubmissionHandler; + + impl OnTransactionSubmitted for DummyTransactionSubmissionHandler { + fn on_valid_transaction_submitted(submitter: AccountId) { + Transfers::::insert(submitter, ()); + } + } + + pub struct DummyBlockchain; + + impl Blockchain for DummyBlockchain { + type Transaction = RawTransaction; + type TransactionInclusionProof = (bool, RawTransaction); + + fn verify_transaction_inclusion_proof(proof: &Self::TransactionInclusionProof) -> Option { + if proof.0 { + Some(proof.1.clone()) + } else { + None + } + } + } + + pub struct DummyTransaction; + + impl MaybeLockFundsTransaction for DummyTransaction { + type Transaction = RawTransaction; + type Id = u64; + type Recipient = AccountId; + type Amount = u64; + + fn parse(tx: &Self::Transaction) -> sp_currency_exchange::Result { + match tx.id { + INVALID_TRANSACTION_ID => Err(sp_currency_exchange::Error::InvalidTransaction), + _ => Ok(tx.clone()), + } + } + } + + pub struct DummyRecipientsMap; + + impl RecipientsMap for DummyRecipientsMap { + type PeerRecipient = AccountId; + type Recipient = AccountId; + + fn map(peer_recipient: Self::PeerRecipient) -> sp_currency_exchange::Result { + match peer_recipient { + UNKNOWN_RECIPIENT_ID => Err(sp_currency_exchange::Error::FailedToMapRecipients), + _ => Ok(peer_recipient * 10), + } + } + } + + pub struct DummyCurrencyConverter; + + impl CurrencyConverter for DummyCurrencyConverter { + type SourceAmount = u64; + type TargetAmount = u64; + + fn convert(amount: Self::SourceAmount) -> sp_currency_exchange::Result { + match amount { + INVALID_AMOUNT => Err(sp_currency_exchange::Error::FailedToConvertCurrency), + _ => Ok(amount * 10), + } + } + } + + pub struct DummyDepositInto; + + impl DepositInto for DummyDepositInto { + type Recipient = AccountId; + type Amount = u64; + + fn deposit_into(_recipient: Self::Recipient, amount: Self::Amount) -> sp_currency_exchange::Result<()> { + match amount > MAX_DEPOSIT_AMOUNT { + true => Err(sp_currency_exchange::Error::DepositFailed), + _ => Ok(()), + } + } + } + + #[derive(Clone, Eq, PartialEq)] + pub struct TestRuntime; + + impl_outer_origin! { + pub enum Origin for TestRuntime where system = frame_system {} + } + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + } + + impl frame_system::Trait for TestRuntime { + type Origin = Origin; + type Index = u64; + type Call = (); + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type DbWeight = (); + type BlockExecutionWeight = (); + type ExtrinsicBaseWeight = (); + type MaximumExtrinsicWeight = (); + type AvailableBlockRatio = AvailableBlockRatio; + type MaximumBlockLength = MaximumBlockLength; + type Version = (); + type ModuleToIndex = (); + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + } + + impl Trait for TestRuntime { + type OnTransactionSubmitted = DummyTransactionSubmissionHandler; + type PeerBlockchain = DummyBlockchain; + type PeerMaybeLockFundsTransaction = DummyTransaction; + type RecipientsMap = DummyRecipientsMap; + type Amount = u64; + type CurrencyConverter = DummyCurrencyConverter; + type DepositInto = DummyDepositInto; + } + + type Exchange = Module; + + fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + sp_io::TestExternalities::new(t) + } + + fn transaction(id: u64) -> RawTransaction { + RawTransaction { + id, + recipient: 1, + amount: 2, + } + } + + #[test] + fn unfinalized_transaction_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (false, transaction(0))), + Error::::UnfinalizedTransaction, + ); + }); + } + + #[test] + fn invalid_transaction_rejected() { + new_test_ext().execute_with(|| { + assert_noop!( + Exchange::import_peer_transaction( + Origin::signed(SUBMITTER), + (true, transaction(INVALID_TRANSACTION_ID)), + ), + Error::::InvalidTransaction, + ); + }); + } + + #[test] + fn claimed_transaction_rejected() { + new_test_ext().execute_with(|| { + ::Transfers::insert(ALREADY_CLAIMED_TRANSACTION_ID, ()); + assert_noop!( + Exchange::import_peer_transaction( + Origin::signed(SUBMITTER), + (true, transaction(ALREADY_CLAIMED_TRANSACTION_ID)), + ), + Error::::AlreadyClaimed, + ); + }); + } + + #[test] + fn transaction_with_unknown_recipient_rejected() { + new_test_ext().execute_with(|| { + let mut transaction = transaction(0); + transaction.recipient = UNKNOWN_RECIPIENT_ID; + assert_noop!( + Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)), + Error::::FailedToMapRecipients, + ); + }); + } + + #[test] + fn transaction_with_invalid_amount_rejected() { + new_test_ext().execute_with(|| { + let mut transaction = transaction(0); + transaction.amount = INVALID_AMOUNT; + assert_noop!( + Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)), + Error::::FailedToConvertCurrency, + ); + }); + } + + #[test] + fn transaction_with_invalid_deposit_rejected() { + new_test_ext().execute_with(|| { + let mut transaction = transaction(0); + transaction.amount = MAX_DEPOSIT_AMOUNT; + assert_noop!( + Exchange::import_peer_transaction(Origin::signed(SUBMITTER), (true, transaction)), + Error::::DepositFailed, + ); + }); + } + + #[test] + fn valid_transaction_accepted() { + new_test_ext().execute_with(|| { + assert_ok!(Exchange::import_peer_transaction( + Origin::signed(SUBMITTER), + (true, transaction(0)), + ),); + + // ensure that the transfer has been marked as completed + assert!(::Transfers::contains_key(0u64)); + // ensure that submitter has been rewarded + assert!(::Transfers::contains_key(SUBMITTER)); + }); + } +} diff --git a/bridges/modules/ethereum/src/finality.rs b/bridges/modules/ethereum/src/finality.rs index 676108a30d..70d79fb20d 100644 --- a/bridges/modules/ethereum/src/finality.rs +++ b/bridges/modules/ethereum/src/finality.rs @@ -103,7 +103,7 @@ fn prepare_votes( // we only take ancestors that are not yet pruned and those signed by // the same set of validators let mut parent_empty_step_signers = empty_steps_signers(header); - let ancestry = ancestry(storage, header) + let ancestry = ancestry(storage, header.parent_hash) .map(|(hash, header, submitter)| { let mut signers = BTreeSet::new(); sp_std::mem::swap(&mut signers, &mut parent_empty_step_signers); @@ -196,9 +196,8 @@ fn empty_step_signer(empty_step: &SealedEmptyStep, parent_hash: &H256) -> Option /// Return iterator of given header ancestors. pub(crate) fn ancestry<'a, S: Storage>( storage: &'a S, - header: &Header, + mut parent_hash: H256, ) -> impl Iterator)> + 'a { - let mut parent_hash = header.parent_hash.clone(); from_fn(move || { let (header, submitter) = storage.header(&parent_hash)?; if header.number == 0 { diff --git a/bridges/modules/ethereum/src/lib.rs b/bridges/modules/ethereum/src/lib.rs index 232f89d9c3..3f662547d1 100644 --- a/bridges/modules/ethereum/src/lib.rs +++ b/bridges/modules/ethereum/src/lib.rs @@ -18,7 +18,7 @@ use codec::{Decode, Encode}; use frame_support::{decl_module, decl_storage, traits::Get}; -use primitives::{Address, Header, Receipt, H256, U256}; +use primitives::{Address, Header, RawTransaction, Receipt, H256, U256}; use sp_runtime::{ transaction_validity::{ InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionSource, TransactionValidity, @@ -456,6 +456,11 @@ impl Module { pub fn is_known_block(hash: H256) -> bool { BridgeStorage::::new().header(&hash).is_some() } + + /// Verify that transaction is included into given finalized block. + pub fn verify_transaction_finalized(block: H256, tx_index: u64, proof: &Vec) -> bool { + crate::verify_transaction_finalized(&BridgeStorage::::new(), block, tx_index, proof) + } } impl frame_support::unsigned::ValidateUnsigned for Module { @@ -672,6 +677,13 @@ impl Storage for BridgeStorage { } }; + frame_support::debug::trace!( + target: "runtime", + "Inserting PoA header: ({}, {})", + header.header.number, + header.hash, + ); + let last_signal_block = header.context.last_signal_block().cloned(); HeadersByNumber::append(header.header.number, header.hash); Headers::::insert( @@ -693,6 +705,13 @@ impl Storage for BridgeStorage { .map(|f| f.0) .unwrap_or_else(|| FinalizedBlock::get().0); if let Some(finalized) = finalized { + frame_support::debug::trace!( + target: "runtime", + "Finalizing PoA header: ({}, {})", + finalized.0, + finalized.1, + ); + FinalizedBlock::put(finalized); } @@ -701,6 +720,44 @@ impl Storage for BridgeStorage { } } +/// Verify that transaction is included into given finalized block. +pub fn verify_transaction_finalized( + storage: &S, + block: H256, + tx_index: u64, + proof: &Vec, +) -> bool { + if tx_index >= proof.len() as _ { + return false; + } + + let header = match storage.header(&block) { + Some((header, _)) => header, + None => return false, + }; + let (finalized_number, finalized_hash) = storage.finalized_block(); + + // if header is not yet finalized => return + if header.number > finalized_number { + return false; + } + + // check if header is actually finalized + let is_finalized = match header.number < finalized_number { + true => finality::ancestry(storage, finalized_hash) + .skip_while(|(_, ancestor, _)| ancestor.number > header.number) + .filter(|&(ancestor_hash, _, _)| ancestor_hash == block) + .next() + .is_some(), + false => block == finalized_hash, + }; + if !is_finalized { + return false; + } + + header.verify_transactions_root(proof) +} + /// Transaction pool configuration. fn pool_configuration() -> PoolConfiguration { PoolConfiguration { @@ -709,9 +766,12 @@ fn pool_configuration() -> PoolConfiguration { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; - use crate::mock::{custom_block_i, custom_test_ext, genesis, validators, validators_addresses, TestRuntime}; + use crate::mock::{ + custom_block_i, custom_test_ext, genesis, insert_header, validators, validators_addresses, TestRuntime, + }; + use primitives::compute_merkle_root; fn with_headers_to_prune(f: impl Fn(BridgeStorage) -> T) -> T { custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { @@ -874,4 +934,133 @@ mod tests { ); }); } + + fn example_tx() -> Vec { + vec![42] + } + + fn example_header() -> Header { + let mut header = Header::default(); + header.number = 2; + header.transactions_root = compute_merkle_root(vec![example_tx()].into_iter()); + header.parent_hash = example_header_parent().hash(); + header + } + + fn example_header_parent() -> Header { + let mut header = Header::default(); + header.number = 1; + header.transactions_root = compute_merkle_root(vec![example_tx()].into_iter()); + header.parent_hash = genesis().hash(); + header + } + + #[test] + fn verify_transaction_finalized_works_for_best_finalized_header() { + custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + let storage = BridgeStorage::::new(); + assert_eq!( + verify_transaction_finalized(&storage, example_header().hash(), 0, &vec![example_tx()],), + true, + ); + }); + } + + #[test] + fn verify_transaction_finalized_works_for_best_finalized_header_ancestor() { + custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + let mut storage = BridgeStorage::::new(); + insert_header(&mut storage, example_header_parent()); + insert_header(&mut storage, example_header()); + storage.finalize_headers(Some((example_header().number, example_header().hash())), None); + assert_eq!( + verify_transaction_finalized(&storage, example_header_parent().hash(), 0, &vec![example_tx()],), + true, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_proof_with_missing_tx() { + custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + let storage = BridgeStorage::::new(); + assert_eq!( + verify_transaction_finalized(&storage, example_header().hash(), 1, &vec![],), + false, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_unknown_header() { + custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + let storage = BridgeStorage::::new(); + assert_eq!( + verify_transaction_finalized(&storage, example_header().hash(), 1, &vec![],), + false, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_unfinalized_header() { + custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + let mut storage = BridgeStorage::::new(); + insert_header(&mut storage, example_header_parent()); + insert_header(&mut storage, example_header()); + assert_eq!( + verify_transaction_finalized(&storage, example_header().hash(), 0, &vec![example_tx()],), + false, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_finalized_header_sibling() { + custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + let mut finalized_header_sibling = example_header(); + finalized_header_sibling.timestamp = 1; + let finalized_header_sibling_hash = finalized_header_sibling.hash(); + + let mut storage = BridgeStorage::::new(); + insert_header(&mut storage, example_header_parent()); + insert_header(&mut storage, example_header()); + insert_header(&mut storage, finalized_header_sibling); + storage.finalize_headers(Some((example_header().number, example_header().hash())), None); + assert_eq!( + verify_transaction_finalized(&storage, finalized_header_sibling_hash, 0, &vec![example_tx()],), + false, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_finalized_header_uncle() { + custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + let mut finalized_header_uncle = example_header_parent(); + finalized_header_uncle.timestamp = 1; + let finalized_header_uncle_hash = finalized_header_uncle.hash(); + + let mut storage = BridgeStorage::::new(); + insert_header(&mut storage, example_header_parent()); + insert_header(&mut storage, finalized_header_uncle); + insert_header(&mut storage, example_header()); + storage.finalize_headers(Some((example_header().number, example_header().hash())), None); + assert_eq!( + verify_transaction_finalized(&storage, finalized_header_uncle_hash, 0, &vec![example_tx()],), + false, + ); + }); + } + + #[test] + fn verify_transaction_finalized_rejects_invalid_proof() { + custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + let storage = BridgeStorage::::new(); + assert_eq!( + verify_transaction_finalized(&storage, example_header().hash(), 0, &vec![example_tx(), example_tx(),],), + false, + ); + }); + } } diff --git a/bridges/modules/ethereum/src/validators.rs b/bridges/modules/ethereum/src/validators.rs index 7a1fbb52f0..8b1c5698fc 100644 --- a/bridges/modules/ethereum/src/validators.rs +++ b/bridges/modules/ethereum/src/validators.rs @@ -129,7 +129,7 @@ impl<'a> Validators<'a> { } let receipts = receipts.ok_or(Error::MissingTransactionsReceipts)?; - if !header.check_transactions_receipts(&receipts) { + if !header.verify_receipts_root(&receipts) { return Err(Error::TransactionsReceiptsMismatch); } diff --git a/bridges/modules/ethereum/src/verification.rs b/bridges/modules/ethereum/src/verification.rs index 567d14f3b5..ca8d9af6d1 100644 --- a/bridges/modules/ethereum/src/verification.rs +++ b/bridges/modules/ethereum/src/verification.rs @@ -134,7 +134,7 @@ pub fn accept_aura_header_into_pool( // the heaviest, but rare operation - we do not want invalid receipts in the pool if let Some(receipts) = receipts { - if !header.check_transactions_receipts(receipts) { + if !header.verify_receipts_root(receipts) { return Err(Error::TransactionsReceiptsMismatch); } } diff --git a/bridges/primitives/currency-exchange/Cargo.toml b/bridges/primitives/currency-exchange/Cargo.toml new file mode 100644 index 0000000000..eec26b3797 --- /dev/null +++ b/bridges/primitives/currency-exchange/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "sp-currency-exchange" +description = "Primitives of currency exchange module." +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } + +# Substrate Based Dependencies + +[dependencies.sp-std] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate.git" + +[dependencies.frame-support] +version = "2.0.0-rc1" +default-features = false +rev = "599ba75bc2b5acd238c21c5c7efe8e2ad8d401ee" +git = "https://github.com/paritytech/substrate.git" + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-std/std", + "frame-support/std", +] diff --git a/bridges/primitives/currency-exchange/src/lib.rs b/bridges/primitives/currency-exchange/src/lib.rs new file mode 100644 index 0000000000..59e411c1bc --- /dev/null +++ b/bridges/primitives/currency-exchange/src/lib.rs @@ -0,0 +1,127 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, EncodeLike}; +use frame_support::RuntimeDebug; +use sp_std::marker::PhantomData; + +/// All errors that may happen during exchange. +#[derive(RuntimeDebug, PartialEq)] +pub enum Error { + /// Invalid peer blockchain transaction provided. + InvalidTransaction, + /// Peer transaction has invalid amount. + InvalidAmount, + /// Peer transaction has invalid recipient. + InvalidRecipient, + /// Cannot map from peer recipient to this blockchain recipient. + FailedToMapRecipients, + /// Failed to convert from peer blockchain currency to this blockhain currency. + FailedToConvertCurrency, + /// Deposit has failed. + DepositFailed, +} + +/// Result of all exchange operations. +pub type Result = sp_std::result::Result; + +/// Peer blockchain lock funds transaction. +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)] +pub struct LockFundsTransaction { + /// Something that uniquely identifies this transfer. + pub id: TransferId, + /// Funds recipient on the peer chain. + pub recipient: Recipient, + /// Amount of the locked funds. + pub amount: Amount, +} + +/// Peer blockchain transaction that may represent lock funds transaction. +pub trait MaybeLockFundsTransaction { + /// Transaction type. + type Transaction; + /// Identifier that uniquely identifies this transfer. + type Id: Decode + Encode + EncodeLike; + /// Peer recipient type. + type Recipient; + /// Peer currency amount type. + type Amount; + + /// Parse lock funds transaction of the peer blockchain. Returns None if + /// transaction format is unknown, or it isn't a lock funds transaction. + fn parse(tx: &Self::Transaction) -> Result>; +} + +/// Map that maps recipients from peer blockchain to this blockchain recipients. +pub trait RecipientsMap { + /// Peer blockchain recipient type. + type PeerRecipient; + /// Current blockchain recipient type. + type Recipient; + + /// Lookup current blockchain recipient by peer blockchain recipient. + fn map(peer_recipient: Self::PeerRecipient) -> Result; +} + +/// Conversion between two currencies. +pub trait CurrencyConverter { + /// Type of the source currency amount. + type SourceAmount; + /// Type of the target currency amount. + type TargetAmount; + + /// Covert from source to target currency. + fn convert(amount: Self::SourceAmount) -> Result; +} + +/// Currency deposit. +pub trait DepositInto { + /// Recipient type. + type Recipient; + /// Currency amount type. + type Amount; + + /// Grant some money to given account. + fn deposit_into(recipient: Self::Recipient, amount: Self::Amount) -> Result<()>; +} + +/// Recipients map which is used when accounts ids are the same on both chains. +#[derive(Debug)] +pub struct IdentityRecipients(PhantomData); + +impl RecipientsMap for IdentityRecipients { + type PeerRecipient = AccountId; + type Recipient = AccountId; + + fn map(peer_recipient: Self::PeerRecipient) -> Result { + Ok(peer_recipient) + } +} + +/// Currency converter which is used when currency is the same on both chains. +#[derive(Debug)] +pub struct IdentityCurrencyConverter(PhantomData); + +impl CurrencyConverter for IdentityCurrencyConverter { + type SourceAmount = Amount; + type TargetAmount = Amount; + + fn convert(currency: Self::SourceAmount) -> Result { + Ok(currency) + } +} diff --git a/bridges/primitives/ethereum-poa/Cargo.toml b/bridges/primitives/ethereum-poa/Cargo.toml index 32ac689bb9..e6c3c6c04a 100644 --- a/bridges/primitives/ethereum-poa/Cargo.toml +++ b/bridges/primitives/ethereum-poa/Cargo.toml @@ -20,6 +20,9 @@ hash-db = { version = "0.15.2", default-features = false } triehash = { version = "0.8.2", default-features = false } plain_hasher = { version = "0.2.2", default-features = false } +[dev-dependencies] +hex-literal = "0.2" + # Substrate Based Dependencies [dependencies.sp-api] version = "2.0.0-rc1" diff --git a/bridges/primitives/ethereum-poa/src/lib.rs b/bridges/primitives/ethereum-poa/src/lib.rs index 662079c301..969af63e15 100644 --- a/bridges/primitives/ethereum-poa/src/lib.rs +++ b/bridges/primitives/ethereum-poa/src/lib.rs @@ -43,6 +43,9 @@ impl_fixed_hash_rlp!(H520, 65); #[cfg(feature = "std")] impl_fixed_hash_serde!(H520, 65); +/// Raw (RLP-encoded) ethereum transaction. +pub type RawTransaction = Vec; + /// An ethereum address. pub type Address = H160; @@ -83,6 +86,21 @@ pub struct Header { pub seal: Vec, } +/// Parsed ethereum transaction. +#[derive(Debug, PartialEq)] +pub struct Transaction { + /// Sender address. + pub sender: Address, + /// Sender nonce. + pub nonce: U256, + /// Transaction destination address. None if it is contract creation transaction. + pub to: Option
, + /// Transaction value. + pub value: U256, + /// Transaction payload. + pub payload: Bytes, +} + /// Information describing execution of a transaction. #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug)] pub struct Receipt { @@ -143,23 +161,14 @@ impl Header { keccak_256(&self.rlp(true)).into() } - /// Check if passed transactions receipts are matching this header. - pub fn check_transactions_receipts(&self, receipts: &Vec) -> bool { - struct Keccak256Hasher; + /// Check if passed transactions receipts are matching receipts root in this header. + pub fn verify_receipts_root(&self, receipts: &[Receipt]) -> bool { + verify_merkle_proof(self.receipts_root, receipts.iter().map(|r| r.rlp())) + } - impl hash_db::Hasher for Keccak256Hasher { - type Out = H256; - type StdHasher = plain_hasher::PlainHasher; - const LENGTH: usize = 32; - fn hash(x: &[u8]) -> Self::Out { - keccak_256(x).into() - } - } - - let receipts = receipts.iter().map(|r| r.rlp()); - let actual_root = triehash::ordered_trie_root::(receipts); - let expected_root = self.receipts_root; - actual_root == expected_root + /// Check if passed transactions are matching transactions root in this header. + pub fn verify_transactions_root(&self, transactions: &[RawTransaction]) -> bool { + verify_merkle_proof(self.transactions_root, transactions.into_iter()) } /// Gets the seal hash of this header. @@ -335,6 +344,65 @@ impl std::fmt::Debug for Bloom { } } +/// Decode Ethereum transaction. +pub fn transaction_decode(raw_tx: &[u8]) -> Result { + // parse transaction fields + let tx_rlp = Rlp::new(raw_tx); + let nonce: U256 = tx_rlp.val_at(0)?; + let gas_price = tx_rlp.at(1)?; + let gas = tx_rlp.at(2)?; + let action = tx_rlp.at(3)?; + let to = match action.is_empty() { + false => Some(action.as_val()?), + true => None, + }; + let value: U256 = tx_rlp.val_at(4)?; + let payload: Bytes = tx_rlp.val_at(5)?; + let v: u64 = tx_rlp.val_at(6)?; + let r: U256 = tx_rlp.val_at(7)?; + let s: U256 = tx_rlp.val_at(8)?; + + // reconstruct signature + let mut signature = [0u8; 65]; + let (chain_id, v) = match v { + v if v == 27u64 => (None, 0), + v if v == 28u64 => (None, 1), + v if v >= 35u64 => (Some((v - 35) / 2), ((v - 1) % 2) as u8), + _ => (None, 4), + }; + r.to_big_endian(&mut signature[0..32]); + s.to_big_endian(&mut signature[32..64]); + signature[64] = v; + + // reconstruct message that has been signed + let mut message = RlpStream::new_list(if chain_id.is_some() { 9 } else { 6 }); + message.append(&nonce); + message.append_raw(gas_price.as_raw(), 1); + message.append_raw(gas.as_raw(), 1); + message.append_raw(action.as_raw(), 1); + message.append(&value); + message.append(&payload); + if let Some(chain_id) = chain_id { + message.append(&chain_id); + message.append(&0u8); + message.append(&0u8); + } + let message = keccak_256(&message.out()); + + // recover tx sender + let sender_public = sp_io::crypto::secp256k1_ecdsa_recover(&signature, &message) + .map_err(|_| rlp::DecoderError::Custom("Failed to recover transaction sender"))?; + let sender_address = public_to_address(&sender_public); + + Ok(Transaction { + sender: sender_address, + nonce, + to, + value, + payload, + }) +} + /// Convert public key into corresponding ethereum address. pub fn public_to_address(public: &[u8; 64]) -> Address { let hash = keccak_256(public); @@ -343,6 +411,27 @@ pub fn public_to_address(public: &[u8; 64]) -> Address { result } +/// Verify ethereum merkle proof. +fn verify_merkle_proof>(expected_root: H256, items: impl Iterator) -> bool { + compute_merkle_root(items) == expected_root +} + +/// Compute ethereum merkle root. +pub fn compute_merkle_root>(items: impl Iterator) -> H256 { + struct Keccak256Hasher; + + impl hash_db::Hasher for Keccak256Hasher { + type Out = H256; + type StdHasher = plain_hasher::PlainHasher; + const LENGTH: usize = 32; + fn hash(x: &[u8]) -> Self::Out { + keccak_256(x).into() + } + } + + triehash::ordered_trie_root::(items) +} + sp_api::decl_runtime_apis! { /// API for headers submitters. pub trait EthereumHeadersApi { @@ -358,3 +447,75 @@ sp_api::decl_runtime_apis! { fn is_known_block(hash: H256) -> bool; } } + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn transfer_transaction_decode_works() { + // value transfer transaction + // https://etherscan.io/tx/0xb9d4ad5408f53eac8627f9ccd840ba8fb3469d55cd9cc2a11c6e049f1eef4edd + // https://etherscan.io/getRawTx?tx=0xb9d4ad5408f53eac8627f9ccd840ba8fb3469d55cd9cc2a11c6e049f1eef4edd + let raw_tx = hex!("f86c0a85046c7cfe0083016dea94d1310c1e038bc12865d3d3997275b3e4737c6302880b503be34d9fe80080269fc7eaaa9c21f59adf8ad43ed66cf5ef9ee1c317bd4d32cd65401e7aaca47cfaa0387d79c65b90be6260d09dcfb780f29dd8133b9b1ceb20b83b7e442b4bfc30cb"); + assert_eq!( + transaction_decode(&raw_tx), + Ok(Transaction { + sender: hex!("67835910d32600471f388a137bbff3eb07993c04").into(), + nonce: 10.into(), + to: Some(hex!("d1310c1e038bc12865d3d3997275b3e4737c6302").into()), + value: 815217380000000000_u64.into(), + payload: Default::default(), + }), + ); + + // Kovan value transfer transaction + // https://kovan.etherscan.io/tx/0x3b4b7bd41c1178045ccb4753aa84c1ef9864b4d712fa308b228917cd837915da + // https://kovan.etherscan.io/getRawTx?tx=0x3b4b7bd41c1178045ccb4753aa84c1ef9864b4d712fa308b228917cd837915da + let raw_tx = hex!("f86a822816808252089470c1ccde719d6f477084f07e4137ab0e55f8369f8930cf46e92063afd8008078a00e4d1f4d8aa992bda3c105ff3d6e9b9acbfd99facea00985e2131029290adbdca028ea29a46a4b66ec65b454f0706228e3768cb0ecf755f67c50ddd472f11d5994"); + assert_eq!( + transaction_decode(&raw_tx), + Ok(Transaction { + sender: hex!("faadface3fbd81ce37b0e19c0b65ff4234148132").into(), + nonce: 10262.into(), + to: Some(hex!("70c1ccde719d6f477084f07e4137ab0e55f8369f").into()), + value: 900379597077600000000_u128.into(), + payload: Default::default(), + }), + ); + } + + #[test] + fn payload_transaction_decode_works() { + // contract call transaction + // https://etherscan.io/tx/0xdc2b996b4d1d6922bf6dba063bfd70913279cb6170967c9bb80252aeb061cf65 + // https://etherscan.io/getRawTx?tx=0xdc2b996b4d1d6922bf6dba063bfd70913279cb6170967c9bb80252aeb061cf65 + let raw_tx = hex!("f8aa76850430e234008301500094dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000e08f35f66867a454835b25118f1e490e7f9e9a7400000000000000000000000000000000000000000000000000000000004c4b4025a0964e023999621dc3d4d831c43c71f7555beb6d1192dee81a3674b3f57e310f21a00f229edd86f841d1ee4dc48cc16667e2283817b1d39bae16ced10cd206ae4fd4"); + assert_eq!( + transaction_decode(&raw_tx), + Ok(Transaction { + sender: hex!("2b9a4d37bdeecdf994c4c9ad7f3cf8dc632f7d70").into(), + nonce: 118.into(), + to: Some(hex!("dac17f958d2ee523a2206206994597c13d831ec7").into()), + value: 0.into(), + payload: hex!("a9059cbb000000000000000000000000e08f35f66867a454835b25118f1e490e7f9e9a7400000000000000000000000000000000000000000000000000000000004c4b40").to_vec().into(), + }), + ); + + // Kovan contract call transaction + // https://kovan.etherscan.io/tx/0x2904b4451d23665492239016b78da052d40d55fdebc7304b38e53cf6a37322cf + // https://kovan.etherscan.io/getRawTx?tx=0x2904b4451d23665492239016b78da052d40d55fdebc7304b38e53cf6a37322cf + let raw_tx = hex!("f8ac8302200b843b9aca00830271009484dd11eb2a29615303d18149c0dbfa24167f896680b844a9059cbb00000000000000000000000001503dfc5ad81bf630d83697e98601871bb211b600000000000000000000000000000000000000000000000000000000000027101ba0ce126d2cca81f5e245f292ff84a0d915c0a4ac52af5c51219db1e5d36aa8da35a0045298b79dac631907403888f9b04c2ab5509fe0cc31785276d30a40b915fcf9"); + assert_eq!( + transaction_decode(&raw_tx), + Ok(Transaction { + sender: hex!("617da121abf03d4c1af572f5a4e313e26bef7bdc").into(), + nonce: 139275.into(), + to: Some(hex!("84dd11eb2a29615303d18149c0dbfa24167f8966").into()), + value: 0.into(), + payload: hex!("a9059cbb00000000000000000000000001503dfc5ad81bf630d83697e98601871bb211b60000000000000000000000000000000000000000000000000000000000002710").to_vec().into(), + }), + ); + } +} diff --git a/bridges/relays/ethereum/Cargo.toml b/bridges/relays/ethereum/Cargo.toml index d3e6770fc5..d1918fe8d7 100644 --- a/bridges/relays/ethereum/Cargo.toml +++ b/bridges/relays/ethereum/Cargo.toml @@ -15,7 +15,7 @@ env_logger = "0.7.0" ethabi = "12.0" ethabi-contract = "11.0" ethabi-derive = "12.0" -ethereum-tx-sign = { git = "https://github.com/svyatonik/ethereum-tx-sign.git", branch = "up-ethereum-types" } +ethereum-tx-sign = "3.0" futures = "0.3.5" hex = "0.4" jsonrpsee = { git = "https://github.com/paritytech/jsonrpsee.git", default-features = false, features = ["http"] }