/
lib.rs
438 lines (382 loc) · 16.8 KB
/
lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! # Relay Pallet
//! Based on the [specification](https://spec.interlay.io/spec/relay.html).
#![deny(warnings)]
#![cfg_attr(test, feature(proc_macro_hygiene))]
#![cfg_attr(not(feature = "std"), no_std)]
mod ext;
pub mod types;
#[cfg(any(feature = "runtime-benchmarks", test))]
mod benchmarking;
mod default_weights;
pub use default_weights::WeightInfo;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(test)]
extern crate mocktopus;
#[cfg(test)]
use mocktopus::macros::mockable;
pub use security;
use crate::types::BalanceOf;
use bitcoin::{parser::parse_transaction, types::*};
use btc_relay::{types::OpReturnPaymentData, BtcAddress};
use frame_support::{dispatch::DispatchResult, ensure, transactional, weights::Pays};
use frame_system::ensure_signed;
use sp_std::{
convert::{TryFrom, TryInto},
vec::Vec,
};
use types::DefaultVaultId;
use vault_registry::Wallet;
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
/// ## Configuration
/// The pallet's configuration trait.
#[pallet::config]
pub trait Config:
frame_system::Config
+ security::Config
+ vault_registry::Config
+ btc_relay::Config
+ redeem::Config
+ replace::Config
+ refund::Config
+ fee::Config
{
/// The overarching event type.
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
/// Weight information for the extrinsics in this module.
type WeightInfo: WeightInfo;
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
VaultTheft {
vault_id: DefaultVaultId<T>,
tx_id: H256Le,
},
VaultDoublePayment {
vault_id: DefaultVaultId<T>,
tx_id_1: H256Le,
tx_id_2: H256Le,
},
}
#[pallet::error]
pub enum Error<T> {
/// Vault already reported
VaultAlreadyReported,
/// Vault BTC address not in transaction input
VaultNoInputToTransaction,
/// Valid redeem transaction
ValidRedeemTransaction,
/// Valid replace transaction
ValidReplaceTransaction,
/// Valid refund transaction
ValidRefundTransaction,
/// Valid merge transaction
ValidMergeTransaction,
/// Failed to parse transaction
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
/// identifiers of Vaults accused of theft.
#[pallet::storage]
#[pallet::getter(fn theft_report)]
pub(super) type TheftReports<T: Config> =
StorageDoubleMap<_, Blake2_128Concat, DefaultVaultId<T>, Blake2_128Concat, H256Le, Option<()>, ValueQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {}
#[pallet::pallet]
pub struct Pallet<T>(_);
// The pallet's dispatchable functions.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// One time function to initialize the BTC-Relay with the first block
///
/// # Arguments
///
/// * `block_header_bytes` - 80 byte raw Bitcoin block header.
/// * `block_height` - starting Bitcoin block height of the submitted block header.
///
/// # <weight>
/// - Storage Reads:
/// - One storage read to check that parachain is not shutdown. O(1)
/// - One storage read to check if relayer authorization is disabled. O(1)
/// - One storage read to check if relayer is authorized. O(1)
/// - Storage Writes:
/// - One storage write to store block hash. O(1)
/// - One storage write to store block header. O(1)
/// - One storage write to initialize main chain. O(1)
/// - One storage write to store best block hash. O(1)
/// - One storage write to store best block height. O(1)
/// - Events:
/// - One event for initialization.
///
/// Total Complexity: O(1)
/// # </weight>
#[pallet::weight(<T as Config>::WeightInfo::initialize())]
#[transactional]
pub fn initialize(
origin: OriginFor<T>,
raw_block_header: RawBlockHeader,
block_height: u32,
) -> DispatchResultWithPostInfo {
let relayer = ensure_signed(origin)?;
let block_header = ext::btc_relay::parse_raw_block_header::<T>(&raw_block_header)?;
ext::btc_relay::initialize::<T>(relayer, block_header, block_height)?;
// don't take tx fees on success
Ok(Pays::No.into())
}
/// Stores a single new block header
///
/// # Arguments
///
/// * `raw_block_header` - 80 byte raw Bitcoin block header.
///
/// # <weight>
/// Key: C (len of chains), P (len of positions)
/// - Storage Reads:
/// - One storage read to check that parachain is not shutdown. O(1)
/// - One storage read to check if relayer authorization is disabled. O(1)
/// - One storage read to check if relayer is authorized. O(1)
/// - One storage read to check if block header is stored. O(1)
/// - One storage read to retrieve parent block hash. O(1)
/// - One storage read to check if difficulty check is disabled. O(1)
/// - One storage read to retrieve last re-target. O(1)
/// - One storage read to retrieve all Chains. O(C)
/// - Storage Writes:
/// - One storage write to store block hash. O(1)
/// - One storage write to store block header. O(1)
/// - One storage mutate to extend main chain. O(1)
/// - One storage write to store best block hash. O(1)
/// - One storage write to store best block height. O(1)
/// - Notable Computation:
/// - O(P) sort to reorg chains.
/// - Events:
/// - One event for block stored (fork or extension).
///
/// Total Complexity: O(C + P)
/// # </weight>
#[pallet::weight(<T as Config>::WeightInfo::store_block_header())]
#[transactional]
pub fn store_block_header(
origin: OriginFor<T>,
raw_block_header: RawBlockHeader,
) -> DispatchResultWithPostInfo {
let relayer = ensure_signed(origin)?;
let block_header = ext::btc_relay::parse_raw_block_header::<T>(&raw_block_header)?;
ext::btc_relay::store_block_header::<T>(&relayer, block_header)?;
// don't take tx fees on success
Ok(Pays::No.into())
}
/// 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.
/// * `raw_merkle_proof`: The proof of tx inclusion.
/// * `raw_tx`: The raw Bitcoin transaction.
#[pallet::weight(<T as Config>::WeightInfo::report_vault_theft())]
#[transactional]
pub fn report_vault_theft(
origin: OriginFor<T>,
vault_id: DefaultVaultId<T>,
raw_merkle_proof: Vec<u8>,
raw_tx: Vec<u8>,
) -> DispatchResultWithPostInfo {
let reporter_id = ensure_signed(origin)?;
let merkle_proof = ext::btc_relay::parse_merkle_proof::<T>(&raw_merkle_proof)?;
let transaction = parse_transaction(raw_tx.as_slice()).map_err(|_| Error::<T>::InvalidTransaction)?;
let tx_id = transaction.tx_id();
// throw if already reported
ensure!(
!<TheftReports<T>>::contains_key(&vault_id, &tx_id),
Error::<T>::VaultAlreadyReported,
);
ext::btc_relay::verify_transaction_inclusion::<T>(tx_id, merkle_proof)?;
Self::_is_parsed_transaction_invalid(&vault_id, transaction)?;
ext::vault_registry::liquidate_theft_vault::<T>(&vault_id, reporter_id)?;
<TheftReports<T>>::mutate(&vault_id, &tx_id, |inner| {
let _ = inner.insert(());
});
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: DefaultVaultId<T>,
raw_merkle_proofs: (Vec<u8>, Vec<u8>),
raw_txs: (Vec<u8>, Vec<u8>),
) -> DispatchResultWithPostInfo {
let reporter_id = 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, reporter_id)?;
<TheftReports<T>>::mutate(&vault_id, &left_tx_id, |inner| {
let _ = inner.insert(());
});
<TheftReports<T>>::mutate(&vault_id, &right_tx_id, |inner| {
let _ = inner.insert(());
});
Self::deposit_event(Event::<T>::VaultDoublePayment {
vault_id,
tx_id_1: left_tx_id,
tx_id_2: right_tx_id,
});
// don't take tx fees on success
Ok(Pays::No.into())
}
_ => Err(Error::<T>::InvalidTransaction.into()),
}
}
}
}
// "Internal" functions, callable by code.
#[cfg_attr(test, mockable)]
impl<T: Config> Pallet<T> {
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 sending a valid request transaction.
///
/// # Arguments
///
/// * `request_value` - amount of btc as specified in the request
/// * `request_address` - recipient btc address
/// * `payment_data` - all payment data extracted from tx
/// * `wallet` - vault btc addresses
pub(crate) fn is_valid_request_transaction(
request_value: BalanceOf<T>,
request_address: BtcAddress,
payment_data: &OpReturnPaymentData<T>,
wallet: &Wallet,
) -> bool {
let request_value = match TryInto::<u64>::try_into(request_value).map_err(|_e| Error::<T>::TryIntoIntError) {
Ok(value) => value as i64,
Err(_) => return false,
};
match payment_data.ensure_valid_payment_to(request_value, request_address, None) {
Ok(None) => true,
Ok(Some(return_to_self)) if wallet.has_btc_address(&return_to_self) => true,
_ => false,
}
}
/// Check if a vault transaction is invalid. Returns `Ok` if invalid or `Err` otherwise.
/// This method should be callable over RPC for a staked-relayer client to check validity.
///
/// # Arguments
///
/// `vault_id`: the vault.
/// `raw_tx`: the BTC transaction by the vault.
pub fn is_transaction_invalid(vault_id: &DefaultVaultId<T>, raw_tx: Vec<u8>) -> DispatchResult {
let tx = parse_transaction(raw_tx.as_slice()).map_err(|_| Error::<T>::InvalidTransaction)?;
Self::_is_parsed_transaction_invalid(vault_id, tx)
}
/// Check if a vault transaction is invalid. Returns `Ok` if invalid or `Err` otherwise.
pub fn _is_parsed_transaction_invalid(vault_id: &DefaultVaultId<T>, tx: Transaction) -> DispatchResult {
let vault = ext::vault_registry::get_active_vault_from_id::<T>(vault_id)?;
// check if vault's btc address features in an input of the transaction
ensure!(
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
);
// Vaults are required to move funds for redeem, replace and refund operations.
// Each transaction MUST feature at least two or three outputs as follows:
// * recipient: the recipient of the redeem / replace
// * op_return: the associated ID encoded in the OP_RETURN
// * vault: any "spare change" the vault is transferring
if let Ok(payment_data) = OpReturnPaymentData::<T>::try_from(tx) {
// redeem requests
if let Ok(req) = ext::redeem::get_open_or_completed_redeem_request_from_id::<T>(&payment_data.op_return) {
ensure!(
!Self::is_valid_request_transaction(req.amount_btc, req.btc_address, &payment_data, &vault.wallet),
Error::<T>::ValidRedeemTransaction
);
};
// replace requests
if let Ok(req) = ext::replace::get_open_or_completed_replace_request::<T>(&payment_data.op_return) {
ensure!(
!Self::is_valid_request_transaction(req.amount, req.btc_address, &payment_data, &vault.wallet),
Error::<T>::ValidReplaceTransaction
);
};
// refund requests
if let Ok(req) = ext::refund::get_open_or_completed_refund_request_from_id::<T>(&payment_data.op_return) {
ensure!(
!Self::is_valid_request_transaction(req.amount_btc, req.btc_address, &payment_data, &vault.wallet),
Error::<T>::ValidRefundTransaction
);
};
}
Ok(())
}
}