Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(ckbtc): property tests for event serialization and deserialization #3277

Merged
merged 4 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 32 additions & 42 deletions rs/bitcoin/ckbtc/minter/src/storage/tests.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,38 @@
use crate::state::eventlog::{Event, EventType};
use crate::test_fixtures::{ignored_utxo, ledger_account};

#[test]
fn should_decode_encoded_event() {
let event = Event {
timestamp: Some(123),
payload: event_type(),
};

let encoded = super::encode_event(&event);
let decoded = super::decode_event(&encoded);

assert_eq!(event, decoded);
}

#[test]
fn should_decode_encoded_legacy_event() {
/// Legacy events simply consisted of an event type instance. The
/// encoding logic is the exact same as for new events, only the type
/// being encoded differs.
fn encode_legacy_event(event: &EventType) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::ser::into_writer(event, &mut buf).expect("failed to encode a minter event");
buf
use super::{decode_event, encode_event};
use crate::{
state::eventlog::{Event, EventType},
test_fixtures::arbitrary,
};
use proptest::proptest;

proptest! {
#[test]
fn should_decode_encoded_event(event in arbitrary::event()) {
let encoded = encode_event(&event);
let decoded = decode_event(&encoded);

assert_eq!(event, decoded);
}

let legacy_event = event_type();

let encoded = encode_legacy_event(&legacy_event);
let decoded = super::decode_event(&encoded);

assert_eq!(
decoded,
Event {
timestamp: None,
payload: legacy_event,
#[test]
fn should_decode_encoded_legacy_event(legacy_event in arbitrary::event_type()) {
/// Legacy events just consist of an event type instance. The encoding logic
/// is the exact same as for new events. Only the type being encoded differs.
fn encode_legacy_event(event: &EventType) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::ser::into_writer(event, &mut buf).expect("failed to encode a minter event");
buf
}
);
}

fn event_type() -> EventType {
EventType::ReceivedUtxos {
mint_txid: Some(1),
to_account: ledger_account(),
utxos: vec![ignored_utxo()],
let encoded = encode_legacy_event(&legacy_event);
let decoded = decode_event(&encoded);

assert_eq!(
decoded,
Event {
timestamp: None,
payload: legacy_event,
}
);
}
}
303 changes: 303 additions & 0 deletions rs/bitcoin/ckbtc/minter/src/test_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,306 @@ pub mod mock {
}
}
}

pub mod arbitrary {
use crate::{
address::BitcoinAddress,
signature::EncodedSignature,
state::{
eventlog::{Event, EventType},
ChangeOutput, Mode, ReimbursementReason, RetrieveBtcRequest, SuspendedReason,
},
tx,
tx::{SignedInput, TxOut, UnsignedInput},
};
use candid::Principal;
pub use event::event_type;
use ic_base_types::CanisterId;
use ic_btc_interface::{OutPoint, Satoshi, Txid, Utxo};
use icrc_ledger_types::icrc1::account::Account;
use proptest::{
array::uniform20,
array::uniform32,
collection::{vec as pvec, SizeRange},
option,
prelude::{any, Just, Strategy},
prop_oneof,
};
use serde_bytes::ByteBuf;

// Macro to simplify writing strategies that generate structs.
macro_rules! prop_struct {
($struct_path:path { $($field_name:ident: $strategy:expr),* $(,)? }) => {
#[allow(unused_parens)]
lpahlavi marked this conversation as resolved.
Show resolved Hide resolved
($($strategy),*).prop_map(|($($field_name),*)| {
$struct_path {
$($field_name),*
}
})
};
}

fn amount() -> impl Strategy<Value = Satoshi> {
1..10_000_000_000u64
}

fn txid() -> impl Strategy<Value = Txid> {
uniform32(any::<u8>()).prop_map(Txid::from)
}

fn outpoint() -> impl Strategy<Value = OutPoint> {
prop_struct!(OutPoint {
txid: txid(),
vout: any::<u32>(),
})
}

fn canister_id() -> impl Strategy<Value = CanisterId> {
any::<u64>().prop_map(CanisterId::from_u64)
}

pub fn retrieve_btc_requests(
amount: impl Strategy<Value = Satoshi>,
num: impl Into<SizeRange>,
) -> impl Strategy<Value = Vec<RetrieveBtcRequest>> {
pvec(retrieve_btc_request(amount), num).prop_map(|mut reqs| {
reqs.sort_by_key(|req| req.received_at);
for (i, req) in reqs.iter_mut().enumerate() {
req.block_index = i as u64;
}
reqs
})
}

fn principal() -> impl Strategy<Value = Principal> {
pvec(any::<u8>(), 1..=Principal::MAX_LENGTH_IN_BYTES)
.prop_map(|bytes| Principal::from_slice(bytes.as_slice()))
}

fn retrieve_btc_request(
amount: impl Strategy<Value = Satoshi>,
) -> impl Strategy<Value = RetrieveBtcRequest> {
prop_struct!(RetrieveBtcRequest {
amount: amount,
address: address(),
block_index: any::<u64>(),
received_at: 1569975147000..2069975147000u64,
kyt_provider: option::of(principal()),
reimbursement_account: option::of(account()),
})
}

fn reimbursement_reason() -> impl Strategy<Value = ReimbursementReason> {
prop_oneof![
(principal(), any::<u64>()).prop_map(|(kyt_provider, kyt_fee)| {
ReimbursementReason::TaintedDestination {
kyt_provider,
kyt_fee,
}
}),
Just(ReimbursementReason::CallFailed),
]
}

fn suspended_reason() -> impl Strategy<Value = SuspendedReason> {
prop_oneof![
Just(SuspendedReason::ValueTooSmall),
Just(SuspendedReason::Quarantined),
]
}

fn change_output() -> impl Strategy<Value = ChangeOutput> {
(any::<u32>(), any::<u64>()).prop_map(|(vout, value)| ChangeOutput { vout, value })
}

fn mode() -> impl Strategy<Value = Mode> {
prop_oneof![
Just(Mode::ReadOnly),
pvec(principal(), 0..10_000).prop_map(Mode::RestrictedTo),
pvec(principal(), 0..10_000).prop_map(Mode::DepositsRestrictedTo),
Just(Mode::GeneralAvailability),
]
}

fn encoded_signature() -> impl Strategy<Value = EncodedSignature> {
pvec(1u8..0xff, 64).prop_map(|bytes| EncodedSignature::from_sec1(bytes.as_slice()))
}

pub fn unsigned_input(
value: impl Strategy<Value = Satoshi>,
) -> impl Strategy<Value = UnsignedInput> {
prop_struct!(UnsignedInput {
previous_output: outpoint(),
value: value,
sequence: any::<u32>(),
})
}

pub fn signed_input() -> impl Strategy<Value = SignedInput> {
prop_struct!(SignedInput {
previous_output: outpoint(),
sequence: any::<u32>(),
signature: encoded_signature(),
pubkey: pvec(any::<u8>(), tx::PUBKEY_LEN).prop_map(ByteBuf::from),
})
}

pub fn address() -> impl Strategy<Value = BitcoinAddress> {
prop_oneof![
uniform20(any::<u8>()).prop_map(BitcoinAddress::P2wpkhV0),
uniform32(any::<u8>()).prop_map(BitcoinAddress::P2wshV0),
uniform32(any::<u8>()).prop_map(BitcoinAddress::P2trV1),
uniform20(any::<u8>()).prop_map(BitcoinAddress::P2pkh),
uniform20(any::<u8>()).prop_map(BitcoinAddress::P2sh),
]
}

pub fn tx_out() -> impl Strategy<Value = TxOut> {
prop_struct!(TxOut {
value: amount(),
address: address(),
})
}

pub fn utxo(amount: impl Strategy<Value = Satoshi>) -> impl Strategy<Value = Utxo> {
prop_struct!(Utxo {
outpoint: outpoint(),
value: amount,
height: any::<u32>(),
})
}

pub fn account() -> impl Strategy<Value = Account> {
prop_struct!(Account {
owner: principal(),
subaccount: option::of(uniform32(any::<u8>())),
})
}

pub fn event() -> impl Strategy<Value = Event> {
(any::<Option<u64>>(), event_type())
.prop_map(|(timestamp, payload)| Event { timestamp, payload })
}

// Some event types are deprecated, however we still want to use them in prop tests as we want
// to make sure they can still be deserialized.
// For convenience, the module is not visible to the outside.
#[allow(deprecated)]
mod event {
use super::*;
use crate::lifecycle::{
init::{BtcNetwork, InitArgs},
upgrade::UpgradeArgs,
};

fn btc_network() -> impl Strategy<Value = BtcNetwork> {
prop_oneof![
Just(BtcNetwork::Mainnet),
Just(BtcNetwork::Testnet),
Just(BtcNetwork::Regtest),
]
}

fn init_args() -> impl Strategy<Value = InitArgs> {
prop_struct!(InitArgs {
btc_network: btc_network(),
ecdsa_key_name: ".*",
retrieve_btc_min_amount: any::<u64>(),
ledger_id: canister_id(),
max_time_in_queue_nanos: any::<u64>(),
min_confirmations: option::of(any::<u32>()),
mode: mode(),
check_fee: option::of(any::<u64>()),
kyt_fee: option::of(any::<u64>()),
btc_checker_principal: option::of(canister_id()),
kyt_principal: option::of(canister_id()),
})
}

fn upgrade_args() -> impl Strategy<Value = UpgradeArgs> {
prop_struct!(UpgradeArgs {
retrieve_btc_min_amount: option::of(any::<u64>()),
min_confirmations: option::of(any::<u32>()),
max_time_in_queue_nanos: option::of(any::<u64>()),
mode: option::of(mode()),
check_fee: option::of(any::<u64>()),
kyt_fee: option::of(any::<u64>()),
btc_checker_principal: option::of(canister_id()),
kyt_principal: option::of(canister_id()),
})
}

pub fn event_type() -> impl Strategy<Value = EventType> {
prop_oneof![
init_args().prop_map(EventType::Init),
upgrade_args().prop_map(EventType::Upgrade),
retrieve_btc_request(amount()).prop_map(EventType::AcceptedRetrieveBtcRequest),
prop_struct!(EventType::ReceivedUtxos {
mint_txid: option::of(any::<u64>()),
to_account: account(),
utxos: pvec(utxo(amount()), 0..10_000),
}),
prop_struct!(EventType::RemovedRetrieveBtcRequest {
block_index: any::<u64>()
}),
prop_struct!(EventType::SentBtcTransaction {
request_block_indices: pvec(any::<u64>(), 0..10_000),
txid: txid(),
utxos: pvec(utxo(amount()), 0..10_000),
change_output: option::of(change_output()),
submitted_at: any::<u64>(),
fee_per_vbyte: option::of(any::<u64>()),
}),
prop_struct!(EventType::ReplacedBtcTransaction {
old_txid: txid(),
new_txid: txid(),
change_output: change_output(),
submitted_at: any::<u64>(),
fee_per_vbyte: any::<u64>(),
}),
prop_struct!(EventType::ConfirmedBtcTransaction { txid: txid() }),
prop_struct!(EventType::CheckedUtxo {
utxo: utxo(amount()),
uuid: any::<String>(),
clean: any::<bool>(),
kyt_provider: option::of(principal())
}),
prop_struct!(EventType::CheckedUtxoV2 {
utxo: utxo(amount()),
account: account(),
}),
prop_struct!(EventType::IgnoredUtxo {
utxo: utxo(amount())
}),
prop_struct!(EventType::SuspendedUtxo {
utxo: utxo(amount()),
account: account(),
reason: suspended_reason(),
}),
prop_struct!(EventType::DistributedKytFee {
kyt_provider: principal(),
amount: any::<u64>(),
block_index: any::<u64>(),
}),
prop_struct!(EventType::RetrieveBtcKytFailed {
owner: principal(),
address: ".*",
amount: any::<u64>(),
uuid: ".*",
kyt_provider: principal(),
block_index: any::<u64>(),
}),
prop_struct!(EventType::ScheduleDepositReimbursement {
account: account(),
amount: any::<u64>(),
reason: reimbursement_reason(),
burn_block_index: any::<u64>(),
}),
prop_struct!(EventType::ReimbursedFailedDeposit {
burn_block_index: any::<u64>(),
mint_block_index: any::<u64>(),
}),
]
}
}
}
Loading
Loading