Skip to content

Commit

Permalink
Merge pull request #171 from gregdhill/feat/report-double-payment
Browse files Browse the repository at this point in the history
feat(relay): report double payment
  • Loading branch information
nud3l authored Jul 14, 2021
2 parents f77d5f2 + 0bab75b commit c6c4dc6
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 30 deletions.
131 changes: 106 additions & 25 deletions crates/relay/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! # Relay Pallet
//! Based on the [specification](https://interlay.gitlab.io/polkabtc-spec/spec/staked-relayers.html).
//! Based on the [specification](https://spec.interlay.io/spec/relay.html).
#![deny(warnings)]
#![cfg_attr(test, feature(proc_macro_hygiene))]
Expand Down Expand Up @@ -75,6 +75,7 @@ pub mod pallet {
#[pallet::metadata(T::AccountId = "AccountId")]
pub enum Event<T: Config> {
VaultTheft(T::AccountId, H256Le),
VaultDoublePayment(T::AccountId, H256Le, H256Le),
}

#[pallet::error]
Expand All @@ -95,6 +96,10 @@ pub mod pallet {
InvalidTransaction,
/// Unable to convert value
TryIntoIntError,
/// Expected two unique transactions
DuplicateTransaction,
/// Expected duplicate OP_RETURN ids
ExpectedDuplicate,
}

/// Mapping of Bitcoin transaction identifiers (SHA256 hashes) to account
Expand Down Expand Up @@ -201,15 +206,14 @@ pub mod pallet {
Ok(Pays::No.into())
}

/// A Staked Relayer reports misbehavior by a Vault, providing a fraud proof
/// (malicious Bitcoin transaction and the corresponding transaction inclusion proof).
/// Report misbehavior by a Vault, providing a fraud proof (malicious Bitcoin transaction
/// and the corresponding transaction inclusion proof). This fully slashes the Vault.
///
/// # Arguments
///
/// * `origin`: Any signed user.
/// * `vault_id`: The account of the vault to check.
/// * `tx_id`: The hash of the transaction
/// * `merkle_proof`: The proof of tx inclusion.
/// * `raw_merkle_proof`: The proof of tx inclusion.
/// * `raw_tx`: The raw Bitcoin transaction.
#[pallet::weight(<T as Config>::WeightInfo::report_vault_theft())]
#[transactional]
Expand All @@ -226,8 +230,6 @@ pub mod pallet {
let transaction = parse_transaction(raw_tx.as_slice()).map_err(|_| Error::<T>::InvalidTransaction)?;
let tx_id = transaction.tx_id();

// liquidated vaults are removed, so no need for check here

// throw if already reported
if <TheftReports<T>>::contains_key(&tx_id) {
ensure!(
Expand All @@ -245,14 +247,91 @@ pub mod pallet {
reports.insert(vault_id.clone());
});

// reward the participant by increasing their SLA
// reward the reporter by increasing their SLA
ext::sla::event_update_vault_sla::<T>(&signer, ext::sla::Action::TheftReport)?;

Self::deposit_event(<Event<T>>::VaultTheft(vault_id, tx_id));

// don't take tx fees on success
Ok(Pays::No.into())
}

/// Report Vault double payment, providing two fraud proofs (malicious Bitcoin transactions
/// and the corresponding transaction inclusion proofs). This fully slashes the Vault.
///
/// This can be used for any multiple of payments, i.e., a vault making two, three, four, etc. payments
/// by proving just one double payment.
///
/// # Arguments
///
/// * `origin`: Any signed user.
/// * `vault_id`: The account of the vault to check.
/// * `raw_merkle_proofs`: The proofs of tx inclusion.
/// * `raw_txs`: The raw Bitcoin transactions.
#[pallet::weight(<T as Config>::WeightInfo::report_vault_theft())]
#[transactional]
pub fn report_vault_double_payment(
origin: OriginFor<T>,
vault_id: T::AccountId,
raw_merkle_proofs: (Vec<u8>, Vec<u8>),
raw_txs: (Vec<u8>, Vec<u8>),
) -> DispatchResultWithPostInfo {
ext::security::ensure_parachain_status_not_shutdown::<T>()?;
let signer = ensure_signed(origin)?;

// transactions must be unique
ensure!(raw_txs.0 != raw_txs.1, Error::<T>::DuplicateTransaction);

let parse_and_verify = |raw_tx, raw_proof| -> Result<Transaction, DispatchError> {
let merkle_proof = ext::btc_relay::parse_merkle_proof::<T>(raw_proof)?;
let transaction = parse_transaction(raw_tx).map_err(|_| Error::<T>::InvalidTransaction)?;
// ensure transaction is included
ext::btc_relay::verify_transaction_inclusion::<T>(transaction.tx_id(), merkle_proof)?;
Ok(transaction)
};

let left_tx = parse_and_verify(&raw_txs.0, &raw_merkle_proofs.0)?;
let right_tx = parse_and_verify(&raw_txs.1, &raw_merkle_proofs.1)?;

let left_tx_id = left_tx.tx_id();
let right_tx_id = right_tx.tx_id();

let vault = ext::vault_registry::get_active_vault_from_id::<T>(&vault_id)?;
// ensure that the payment is made from one of the registered wallets of the Vault,
// this prevents a transaction with the same OP_RETURN flagging this Vault for theft
ensure!(
Self::has_input_from_wallet(&left_tx, &vault.wallet)
&& Self::has_input_from_wallet(&right_tx, &vault.wallet),
Error::<T>::VaultNoInputToTransaction
);

match (
OpReturnPaymentData::<T>::try_from(left_tx),
OpReturnPaymentData::<T>::try_from(right_tx),
) {
(Ok(left), Ok(right)) => {
// verify that the OP_RETURN matches, amounts are not relevant as Vaults
// might transfer any amount in the theft transaction
ensure!(left.op_return == right.op_return, Error::<T>::ExpectedDuplicate);

ext::vault_registry::liquidate_theft_vault::<T>(&vault_id)?;

<TheftReports<T>>::mutate(&left_tx_id, |reports| {
reports.insert(vault_id.clone());
});
<TheftReports<T>>::mutate(&right_tx_id, |reports| {
reports.insert(vault_id.clone());
});

// reward the reporter by increasing their SLA
ext::sla::event_update_vault_sla::<T>(&signer, ext::sla::Action::TheftReport)?;
Self::deposit_event(<Event<T>>::VaultDoublePayment(vault_id, left_tx_id, right_tx_id));

// don't take tx fees on success
Ok(Pays::No.into())
}
_ => Err(Error::<T>::InvalidTransaction.into()),
}
}
}
}

Expand All @@ -266,6 +345,23 @@ impl<T: Config> Pallet<T> {
Ok(())
}

pub(crate) fn has_input_from_wallet(transaction: &Transaction, wallet: &Wallet) -> bool {
// collect all addresses that feature in the inputs of the transaction
let input_addresses: Vec<Result<BtcAddress, _>> = transaction
.clone()
.inputs
.into_iter()
.map(|input| input.extract_address())
.collect();

// TODO: can a vault steal funds if it registers a P2WPKH-P2SH since we
// would extract the `P2WPKHv0`?
input_addresses.into_iter().any(|address_result| match address_result {
Ok(address) => wallet.has_btc_address(&address),
_ => false,
})
}

/// Checks if the vault is doing a valid merge transaction to move funds between
/// addresses.
///
Expand Down Expand Up @@ -322,24 +418,9 @@ impl<T: Config> Pallet<T> {
pub fn _is_parsed_transaction_invalid(vault_id: &T::AccountId, tx: Transaction) -> DispatchResult {
let vault = ext::vault_registry::get_active_vault_from_id::<T>(vault_id)?;

// collect all addresses that feature in the inputs of the transaction
let input_addresses: Vec<Result<BtcAddress, _>> = tx
.clone()
.inputs
.into_iter()
.map(|input| input.extract_address())
.collect();

// check if vault's btc address features in an input of the transaction
ensure!(
// TODO: can a vault steal funds if it registers a P2WPKH-P2SH since we
// would extract the `P2WPKHv0`?
input_addresses.into_iter().any(|address_result| {
match address_result {
Ok(address) => vault.wallet.has_btc_address(&address),
_ => false,
}
}),
Self::has_input_from_wallet(&tx, &vault.wallet),
// since the transaction does not have any inputs that correspond
// to any of the vault's registered BTC addresses, return Err
Error::<T>::VaultNoInputToTransaction
Expand Down
115 changes: 110 additions & 5 deletions crates/relay/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,26 @@ fn test_report_vault_theft_succeeds() {
})
}

fn build_dummy_transaction_with(output_addresses: Vec<BtcAddress>) -> Transaction {
fn build_dummy_transaction_from_input_with_output_and_op_return(
prev_hash: H256Le,
public_key: &BtcPublicKey,
output: BtcAddress,
op_return: &[u8],
) -> Transaction {
let mut builder = TransactionBuilder::new();
builder.with_version(1).add_input(
TransactionInputBuilder::new()
.with_source(TransactionInputSource::FromOutput(prev_hash, 1))
.with_sequence(4294967295)
.with_p2pkh(public_key, vec![1u8; 32])
.build(),
);
builder.add_output(TransactionOutput::payment(100, &output));
builder.add_output(TransactionOutput::op_return(0, &op_return));
builder.build()
}

fn build_dummy_transaction_with_output(addresses: Vec<BtcAddress>) -> Transaction {
let mut builder = TransactionBuilder::new();
builder.with_version(1).add_input(
TransactionInputBuilder::new()
Expand All @@ -166,7 +185,7 @@ fn build_dummy_transaction_with(output_addresses: Vec<BtcAddress>) -> Transactio
])
.build(),
);
for address in output_addresses {
for address in addresses {
builder.add_output(TransactionOutput::payment(100, &address));
}
builder.build()
Expand All @@ -180,15 +199,15 @@ fn test_is_valid_merge_transaction_fails() {
.mock_safe(move |_| MockResult::Return(Ok(init_zero_vault(vault, None))));

let address1 = BtcAddress::P2PKH(H160::from_str(&"66c7060feb882664ae62ffad0051fe843e318e85").unwrap());
let transaction1 = build_dummy_transaction_with(vec![address1]);
let transaction1 = build_dummy_transaction_with_output(vec![address1]);
assert_eq!(
Relay::is_valid_merge_transaction(&transaction1, &Wallet::new(dummy_public_key())),
false,
"payment to unknown recipient"
);

let address2 = BtcAddress::P2PKH(H160::from_str(&"5f69790b72c98041330644bbd50f2ebb5d073c36").unwrap());
let transaction2 = build_dummy_transaction_with(vec![address2]);
let transaction2 = build_dummy_transaction_with_output(vec![address2]);
assert_eq!(
Relay::is_valid_merge_transaction(&transaction2, &Wallet::new(dummy_public_key())),
false,
Expand All @@ -205,7 +224,7 @@ fn test_is_valid_merge_transaction_succeeds() {
.mock_safe(move |_| MockResult::Return(Ok(init_zero_vault(vault, None))));

let address = BtcAddress::P2PKH(H160::from_str(&"66c7060feb882664ae62ffad0051fe843e318e85").unwrap());
let transaction = build_dummy_transaction_with(vec![address]);
let transaction = build_dummy_transaction_with_output(vec![address]);

let mut wallet = Wallet::new(dummy_public_key());
wallet.add_btc_address(address);
Expand Down Expand Up @@ -616,3 +635,89 @@ fn test_store_block_header_and_update_sla_fails_with_invalid() {
);
})
}

#[test]
fn should_not_report_double_payment_with_identical_tx() {
run_test(|| {
assert_err!(
Relay::report_vault_double_payment(
Origin::signed(ALICE),
CAROL,
(vec![0u8; 32], vec![0u8; 32]),
(vec![0u8; 32], vec![0u8; 32]),
),
TestError::DuplicateTransaction
);
})
}

#[test]
fn should_report_double_payment() {
run_test(|| {
let public_key = dummy_public_key();
let input_address = BtcAddress::P2PKH(public_key.to_hash());
let output_address = BtcAddress::P2PKH(H160::random());
let left_tx = build_dummy_transaction_from_input_with_output_and_op_return(
H256Le::from_bytes_le(&vec![1u8; 32]),
&public_key,
output_address,
&[1; 32],
);
let right_tx = build_dummy_transaction_from_input_with_output_and_op_return(
H256Le::from_bytes_le(&vec![2u8; 32]),
&public_key,
output_address,
&[1; 32],
);

ext::vault_registry::get_active_vault_from_id::<Test>
.mock_safe(move |_| MockResult::Return(Ok(init_zero_vault(CAROL, Some(input_address)))));
ext::btc_relay::parse_merkle_proof::<Test>.mock_safe(|_| MockResult::Return(Ok(dummy_merkle_proof())));
ext::btc_relay::verify_transaction_inclusion::<Test>.mock_safe(move |_, _| MockResult::Return(Ok(())));
ext::vault_registry::liquidate_theft_vault::<Test>.mock_safe(|_| MockResult::Return(Ok(())));

assert_ok!(Relay::report_vault_double_payment(
Origin::signed(ALICE),
CAROL,
(vec![0u8; 32], vec![1u8; 32]),
(left_tx.format(), right_tx.format()),
));
})
}

#[test]
fn should_not_report_double_payment_with_vault_no_input() {
run_test(|| {
let public_key = dummy_public_key();
let input_address = BtcAddress::P2PKH(H160::random());
let output_address = BtcAddress::P2PKH(H160::random());
let left_tx = build_dummy_transaction_from_input_with_output_and_op_return(
H256Le::from_bytes_le(&vec![1u8; 32]),
&public_key,
output_address,
&[1; 32],
);
let right_tx = build_dummy_transaction_from_input_with_output_and_op_return(
H256Le::from_bytes_le(&vec![2u8; 32]),
&public_key,
output_address,
&[1; 32],
);

ext::vault_registry::get_active_vault_from_id::<Test>
.mock_safe(move |_| MockResult::Return(Ok(init_zero_vault(CAROL, Some(input_address)))));
ext::btc_relay::parse_merkle_proof::<Test>.mock_safe(|_| MockResult::Return(Ok(dummy_merkle_proof())));
ext::btc_relay::verify_transaction_inclusion::<Test>.mock_safe(move |_, _| MockResult::Return(Ok(())));
ext::vault_registry::liquidate_theft_vault::<Test>.mock_safe(|_| MockResult::Return(Ok(())));

assert_err!(
Relay::report_vault_double_payment(
Origin::signed(ALICE),
CAROL,
(vec![0u8; 32], vec![1u8; 32]),
(left_tx.format(), right_tx.format()),
),
TestError::VaultNoInputToTransaction
);
})
}

0 comments on commit c6c4dc6

Please sign in to comment.