Skip to content

Commit d4fce48

Browse files
authored
feat(ckbtc): add check_transaction_str to Bitcoin Checker (#3212)
XC-259: Add a new public method `check_transaction_str` to Bitcoin Checker that takes transaction id as a text argument.
1 parent 2bc6396 commit d4fce48

File tree

4 files changed

+78
-22
lines changed

4 files changed

+78
-22
lines changed

rs/bitcoin/checker/btc_checker_canister.did

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type CheckAddressResponse = variant { Passed; Failed };
99

1010
type CheckTransactionArgs = record { txid : blob };
1111

12+
type CheckTransactionStrArgs = record { txid : text };
13+
1214
// The result of a check_transaction call.
1315
type CheckTransactionResponse = variant {
1416
// When check finishes and all input addresses passed.
@@ -85,7 +87,11 @@ service : (CheckArg) -> {
8587
// of the return result.
8688
check_transaction: (CheckTransactionArgs) -> (CheckTransactionResponse);
8789

90+
// Same as check_transaction, but taking the transaction id argument as a string.
91+
check_transaction_str: (CheckTransactionStrArgs) -> (CheckTransactionResponse);
92+
8893
// Return `Passed` if the given Bitcoin address passes the Bitcoin checker, or `Failed` otherwise.
8994
// May throw error (trap) if the given address is malformed or not a mainnet address.
9095
check_address: (CheckAddressArgs) -> (CheckAddressResponse) query;
96+
9197
}

rs/bitcoin/checker/src/main.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use ic_btc_checker::{
33
blocklist_contains, get_tx_cycle_cost, BtcNetwork, CheckAddressArgs, CheckAddressResponse,
44
CheckArg, CheckMode, CheckTransactionArgs, CheckTransactionIrrecoverableError,
55
CheckTransactionResponse, CheckTransactionRetriable, CheckTransactionStatus,
6-
CHECK_TRANSACTION_CYCLES_REQUIRED, CHECK_TRANSACTION_CYCLES_SERVICE_FEE,
6+
CheckTransactionStrArgs, CHECK_TRANSACTION_CYCLES_REQUIRED,
7+
CHECK_TRANSACTION_CYCLES_SERVICE_FEE,
78
};
89
use ic_btc_interface::Txid;
910
use ic_canister_log::{export as export_logs, log};
@@ -42,10 +43,10 @@ pub fn is_response_too_large(code: &RejectionCode, message: &str) -> bool {
4243
&& (message.contains("size limit") || message.contains("length limit"))
4344
}
4445

45-
#[ic_cdk::query]
4646
/// Return `Passed` if the given bitcion address passed the check, or
4747
/// `Failed` otherwise.
4848
/// May throw error (trap) if the given address is malformed or not a mainnet address.
49+
#[ic_cdk::query]
4950
fn check_address(args: CheckAddressArgs) -> CheckAddressResponse {
5051
let config = get_config();
5152
let btc_network = config.btc_network();
@@ -69,7 +70,6 @@ fn check_address(args: CheckAddressArgs) -> CheckAddressResponse {
6970
}
7071
}
7172

72-
#[ic_cdk::update]
7373
/// Return `Passed` if all input addresses of the transaction of the given
7474
/// transaction id passed the check, or `Failed` if any of them did not.
7575
///
@@ -87,14 +87,29 @@ fn check_address(args: CheckAddressArgs) -> CheckAddressResponse {
8787
/// If a permanent error occurred in the process, e.g, when a transaction data
8888
/// fails to decode or its transaction id does not match, then `Error` is returned
8989
/// together with a text description.
90+
#[ic_cdk::update]
9091
async fn check_transaction(args: CheckTransactionArgs) -> CheckTransactionResponse {
92+
check_transaction_with(|| Txid::try_from(args.txid.as_ref()).map_err(|err| err.to_string()))
93+
.await
94+
}
95+
96+
#[ic_cdk::update]
97+
async fn check_transaction_str(args: CheckTransactionStrArgs) -> CheckTransactionResponse {
98+
use std::str::FromStr;
99+
check_transaction_with(|| Txid::from_str(args.txid.as_ref()).map_err(|err| err.to_string()))
100+
.await
101+
}
102+
103+
async fn check_transaction_with<F: FnOnce() -> Result<Txid, String>>(
104+
get_txid: F,
105+
) -> CheckTransactionResponse {
91106
if ic_cdk::api::call::msg_cycles_accept128(CHECK_TRANSACTION_CYCLES_SERVICE_FEE)
92107
< CHECK_TRANSACTION_CYCLES_SERVICE_FEE
93108
{
94109
return CheckTransactionStatus::NotEnoughCycles.into();
95110
}
96111

97-
match Txid::try_from(args.txid.as_ref()) {
112+
match get_txid() {
98113
Ok(txid) => {
99114
STATS.with(|s| s.borrow_mut().check_transaction_count += 1);
100115
if ic_cdk::api::call::msg_cycles_available128()
@@ -107,9 +122,7 @@ async fn check_transaction(args: CheckTransactionArgs) -> CheckTransactionRespon
107122
check_transaction_inputs(txid).await
108123
}
109124
}
110-
Err(err) => {
111-
CheckTransactionIrrecoverableError::InvalidTransactionId(err.to_string()).into()
112-
}
125+
Err(err) => CheckTransactionIrrecoverableError::InvalidTransactionId(err).into(),
113126
}
114127
}
115128

rs/bitcoin/checker/src/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ pub struct CheckTransactionArgs {
1919
pub txid: Vec<u8>,
2020
}
2121

22+
#[derive(CandidType, Debug, Deserialize, Serialize)]
23+
pub struct CheckTransactionStrArgs {
24+
pub txid: String,
25+
}
26+
2227
#[derive(CandidType, Debug, Clone, Deserialize, Serialize)]
2328
pub enum CheckTransactionResponse {
2429
/// When check finishes and all input addresses passed.

rs/bitcoin/checker/tests/tests.rs

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use ic_base_types::PrincipalId;
33
use ic_btc_checker::{
44
blocklist, get_tx_cycle_cost, BtcNetwork, CheckAddressArgs, CheckAddressResponse, CheckArg,
55
CheckMode, CheckTransactionArgs, CheckTransactionIrrecoverableError, CheckTransactionResponse,
6-
CheckTransactionRetriable, CheckTransactionStatus, InitArg, UpgradeArg,
7-
CHECK_TRANSACTION_CYCLES_REQUIRED, CHECK_TRANSACTION_CYCLES_SERVICE_FEE,
6+
CheckTransactionRetriable, CheckTransactionStatus, CheckTransactionStrArgs, InitArg,
7+
UpgradeArg, CHECK_TRANSACTION_CYCLES_REQUIRED, CHECK_TRANSACTION_CYCLES_SERVICE_FEE,
88
INITIAL_MAX_RESPONSE_BYTES,
99
};
1010
use ic_btc_interface::Txid;
@@ -29,11 +29,11 @@ const TEST_SUBNET_NODES: u16 = 34;
2929
// by a small margin. Namely, the universal_canister itself would consume
3030
// some cycle for decoding args and sending the call.
3131
//
32-
// The number 42_000_000 is obtained empirically by running tests with pocket-ic
32+
// The number 43_000_000 is obtained empirically by running tests with pocket-ic
3333
// and checking the actual consumptions. It is both big enough to allow tests to
3434
// succeed, and small enough not to interfere with the expected cycle cost we
3535
// are testing for.
36-
const UNIVERSAL_CANISTER_CYCLE_MARGIN: u128 = 42_000_000;
36+
const UNIVERSAL_CANISTER_CYCLE_MARGIN: u128 = 43_000_000;
3737

3838
struct Setup {
3939
// Owner of canisters created for the setup.
@@ -274,20 +274,21 @@ fn test_check_transaction_passed() {
274274
let txid =
275275
Txid::from_str("c80763842edc9a697a2114517cf0c138c5403a761ef63cfad1fa6993fa3475ed").unwrap();
276276
let env = &setup.env;
277+
let check_transaction_args = Encode!(&CheckTransactionArgs {
278+
txid: txid.as_ref().to_vec()
279+
})
280+
.unwrap();
281+
let check_transaction_str_args = Encode!(&CheckTransactionStrArgs {
282+
txid: txid.to_string()
283+
})
284+
.unwrap();
277285

278286
// Normal operation requires making http outcalls.
279287
// We'll run this again after testing other CheckMode.
280-
let test_normal_operation = || {
288+
let test_normal_operation = |method, arg| {
281289
let cycles_before = setup.env.cycle_balance(setup.caller);
282290
let call_id = setup
283-
.submit_btc_checker_call(
284-
"check_transaction",
285-
Encode!(&CheckTransactionArgs {
286-
txid: txid.as_ref().to_vec()
287-
})
288-
.unwrap(),
289-
CHECK_TRANSACTION_CYCLES_REQUIRED,
290-
)
291+
.submit_btc_checker_call(method, arg, CHECK_TRANSACTION_CYCLES_REQUIRED)
291292
.expect("submit_call failed to return call id");
292293
// The response body used for testing below is generated from the output of
293294
//
@@ -375,7 +376,7 @@ fn test_check_transaction_passed() {
375376
};
376377

377378
// With default installation
378-
test_normal_operation();
379+
test_normal_operation("check_transaction", check_transaction_args.clone());
379380

380381
// Test CheckMode::RejectAll
381382
env.tick();
@@ -470,7 +471,7 @@ fn test_check_transaction_passed() {
470471
)
471472
.unwrap();
472473

473-
test_normal_operation();
474+
test_normal_operation("check_transaction_str", check_transaction_str_args);
474475
}
475476

476477
#[test]
@@ -669,6 +670,37 @@ fn test_check_transaction_error() {
669670
let actual_cost = cycles_before - cycles_after;
670671
assert!(actual_cost > expected_cost);
671672
assert!(actual_cost - expected_cost < UNIVERSAL_CANISTER_CYCLE_MARGIN);
673+
674+
// Test for malformatted txid in string form
675+
let cycles_before = setup.env.cycle_balance(setup.caller);
676+
let too_short_txid =
677+
"a80763842edc9a697a2114517cf0c138c5403a761ef63cfad1fa6993fa3475".to_string();
678+
let call_id = setup
679+
.submit_btc_checker_call(
680+
"check_transaction_str",
681+
Encode!(&CheckTransactionStrArgs {
682+
txid: too_short_txid
683+
})
684+
.unwrap(),
685+
CHECK_TRANSACTION_CYCLES_REQUIRED,
686+
)
687+
.expect("submit_call failed to return call id");
688+
let result = setup
689+
.env
690+
.await_call(call_id)
691+
.expect("the fetch request didn't finish");
692+
assert!(matches!(
693+
decode::<CheckTransactionResponse>(&result),
694+
CheckTransactionResponse::Unknown(CheckTransactionStatus::Error(
695+
CheckTransactionIrrecoverableError::InvalidTransactionId(_)
696+
))
697+
));
698+
699+
let cycles_after = setup.env.cycle_balance(setup.caller);
700+
let expected_cost = CHECK_TRANSACTION_CYCLES_SERVICE_FEE;
701+
let actual_cost = cycles_before - cycles_after;
702+
assert!(actual_cost > expected_cost);
703+
assert!(actual_cost - expected_cost < UNIVERSAL_CANISTER_CYCLE_MARGIN);
672704
}
673705

674706
fn tick_until_next_request(env: &PocketIc) -> Vec<CanisterHttpRequest> {

0 commit comments

Comments
 (0)