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

refactor(ckbtc): use IC CDK to interact with Bitcoin canister #2921

Merged
merged 10 commits into from
Dec 6, 2024
10 changes: 4 additions & 6 deletions rs/bitcoin/ckbtc/minter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ pub async fn sign_transaction(
ecdsa_public_key: &ECDSAPublicKey,
output_account: &BTreeMap<tx::OutPoint, Account>,
unsigned_tx: tx::UnsignedTransaction,
) -> Result<tx::SignedTransaction, management::CallError> {
) -> Result<tx::SignedTransaction, CallError> {
use crate::address::{derivation_path, derive_public_key};

let mut signed_inputs = Vec::with_capacity(unsigned_tx.inputs.len());
Expand Down Expand Up @@ -1286,8 +1286,7 @@ pub trait CanisterRuntime {
/// Fetches all unspent transaction outputs (UTXOs) associated with the provided address in the specified Bitcoin network.
async fn bitcoin_get_utxos(
&self,
request: &GetUtxosRequest,
cycles: u64,
request: GetUtxosRequest,
) -> Result<GetUtxosResponse, CallError>;

async fn check_transaction(
Expand Down Expand Up @@ -1328,10 +1327,9 @@ impl CanisterRuntime for IcCanisterRuntime {

async fn bitcoin_get_utxos(
&self,
request: &GetUtxosRequest,
cycles: u64,
request: GetUtxosRequest,
) -> Result<GetUtxosResponse, CallError> {
management::call("bitcoin_get_utxos", cycles, &request).await
management::bitcoin_get_utxos(request).await
}

async fn check_transaction(
Expand Down
131 changes: 83 additions & 48 deletions rs/bitcoin/ckbtc/minter/src/management.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! This module contains async functions for interacting with the management canister.

use crate::logs::P0;
use crate::ECDSAPublicKey;
use crate::{tx, CanisterRuntime};
Expand All @@ -8,15 +7,19 @@ use ic_btc_checker::{
CheckAddressArgs, CheckAddressResponse, CheckTransactionArgs, CheckTransactionResponse,
};
use ic_btc_interface::{
Address, GetCurrentFeePercentilesRequest, GetUtxosRequest, GetUtxosResponse,
MillisatoshiPerByte, Network, Utxo, UtxosFilterInRequest,
Address, GetUtxosRequest, GetUtxosResponse, MillisatoshiPerByte, Network, OutPoint, Txid, Utxo,
UtxosFilterInRequest,
};
use ic_canister_log::log;
use ic_cdk::api::call::RejectionCode;
use ic_cdk::api::{
call::RejectionCode,
management_canister::bitcoin::{BitcoinNetwork, UtxoFilter},
};
use ic_management_canister_types::{
DerivationPath, ECDSAPublicKeyArgs, ECDSAPublicKeyResponse, EcdsaCurve, EcdsaKeyId,
};
use serde::de::DeserializeOwned;
use serde_bytes::ByteBuf;
use std::fmt;

/// Represents an error from a management canister call, such as
Expand All @@ -37,6 +40,13 @@ impl CallError {
pub fn reason(&self) -> &Reason {
&self.reason
}

pub fn from_cdk_error(method: &str, (code, msg): (RejectionCode, String)) -> CallError {
CallError {
method: String::from(method),
reason: Reason::from_reject(code, msg),
}
}
}

impl fmt::Display for CallError {
Expand Down Expand Up @@ -142,19 +152,8 @@ pub async fn get_utxos<R: CanisterRuntime>(
source: CallSource,
runtime: &R,
) -> Result<GetUtxosResponse, CallError> {
// NB. The minimum number of cycles that need to be sent with the call is 10B (4B) for
// Bitcoin mainnet (Bitcoin testnet):
// https://internetcomputer.org/docs/current/developer-docs/integrations/bitcoin/bitcoin-how-it-works#api-fees--pricing
let get_utxos_cost_cycles = match network {
Network::Mainnet => 10_000_000_000,
Network::Testnet | Network::Regtest => 4_000_000_000,
};

// Calls "bitcoin_get_utxos" method with the specified argument on the
// management canister.
async fn bitcoin_get_utxos<R: CanisterRuntime>(
req: &GetUtxosRequest,
cycles: u64,
req: GetUtxosRequest,
source: CallSource,
runtime: &R,
) -> Result<GetUtxosResponse, CallError> {
Expand All @@ -163,16 +162,15 @@ pub async fn get_utxos<R: CanisterRuntime>(
CallSource::Minter => &crate::metrics::GET_UTXOS_MINTER_CALLS,
}
.with(|cell| cell.set(cell.get() + 1));
runtime.bitcoin_get_utxos(req, cycles).await
runtime.bitcoin_get_utxos(req).await
}

let mut response = bitcoin_get_utxos(
&GetUtxosRequest {
GetUtxosRequest {
address: address.to_string(),
network: network.into(),
filter: Some(UtxosFilterInRequest::MinConfirmations(min_confirmations)),
},
get_utxos_cost_cycles,
source,
runtime,
)
Expand All @@ -183,12 +181,11 @@ pub async fn get_utxos<R: CanisterRuntime>(
// Continue fetching until there are no more pages.
while let Some(page) = response.next_page {
response = bitcoin_get_utxos(
&GetUtxosRequest {
GetUtxosRequest {
address: address.to_string(),
network: network.into(),
filter: Some(UtxosFilterInRequest::Page(page)),
},
get_utxos_cost_cycles,
source,
runtime,
)
Expand All @@ -202,50 +199,80 @@ pub async fn get_utxos<R: CanisterRuntime>(
Ok(response)
}

/// Fetches a subset of UTXOs for the specified address.
pub async fn bitcoin_get_utxos(request: GetUtxosRequest) -> Result<GetUtxosResponse, CallError> {
fn cdk_get_utxos_request(
request: GetUtxosRequest,
) -> ic_cdk::api::management_canister::bitcoin::GetUtxosRequest {
ic_cdk::api::management_canister::bitcoin::GetUtxosRequest {
address: request.address,
network: cdk_network(request.network.into()),
filter: request.filter.map(|filter| match filter {
UtxosFilterInRequest::MinConfirmations(confirmations)
| UtxosFilterInRequest::min_confirmations(confirmations) => {
UtxoFilter::MinConfirmations(confirmations)
}
UtxosFilterInRequest::Page(bytes) | UtxosFilterInRequest::page(bytes) => {
UtxoFilter::Page(bytes.into_vec())
}
}),
}
}

fn parse_cdk_get_utxos_response(
response: ic_cdk::api::management_canister::bitcoin::GetUtxosResponse,
) -> GetUtxosResponse {
GetUtxosResponse {
utxos: response
.utxos
.into_iter()
.map(|utxo| Utxo {
outpoint: OutPoint {
txid: Txid::try_from(utxo.outpoint.txid.as_slice())
.unwrap_or_else(|_| panic!("Unable to parse TXID")),
vout: utxo.outpoint.vout,
},
value: utxo.value,
height: utxo.height,
})
.collect(),
tip_block_hash: response.tip_block_hash,
tip_height: response.tip_height,
next_page: response.next_page.map(ByteBuf::from),
}
}

ic_cdk::api::management_canister::bitcoin::bitcoin_get_utxos(cdk_get_utxos_request(request))
.await
.map(|(response,)| parse_cdk_get_utxos_response(response))
.map_err(|err| CallError::from_cdk_error("bitcoin_get_utxos", err))
}
lpahlavi marked this conversation as resolved.
Show resolved Hide resolved

/// Returns the current fee percentiles on the Bitcoin network.
pub async fn get_current_fees(network: Network) -> Result<Vec<MillisatoshiPerByte>, CallError> {
let cost_cycles = match network {
Network::Mainnet => 100_000_000,
Network::Testnet => 40_000_000,
Network::Regtest => 0,
};

call(
"bitcoin_get_current_fee_percentiles",
cost_cycles,
&GetCurrentFeePercentilesRequest {
network: network.into(),
ic_cdk::api::management_canister::bitcoin::bitcoin_get_current_fee_percentiles(
ic_cdk::api::management_canister::bitcoin::GetCurrentFeePercentilesRequest {
network: cdk_network(network),
},
)
.await
.map(|(result,)| result)
.map_err(|err| CallError::from_cdk_error("bitcoin_get_current_fee_percentiles", err))
}

/// Sends the transaction to the network the management canister interacts with.
pub async fn send_transaction(
transaction: &tx::SignedTransaction,
network: Network,
) -> Result<(), CallError> {
use ic_cdk::api::management_canister::bitcoin::BitcoinNetwork;

let cdk_network = match network {
Network::Mainnet => BitcoinNetwork::Mainnet,
Network::Testnet => BitcoinNetwork::Testnet,
Network::Regtest => BitcoinNetwork::Regtest,
};

let tx_bytes = transaction.serialize();

ic_cdk::api::management_canister::bitcoin::bitcoin_send_transaction(
ic_cdk::api::management_canister::bitcoin::SendTransactionRequest {
transaction: tx_bytes,
network: cdk_network,
transaction: transaction.serialize(),
network: cdk_network(network),
},
)
.await
.map_err(|(code, msg)| CallError {
method: "bitcoin_send_transaction".to_string(),
reason: Reason::from_reject(code, msg),
})
.map_err(|err| CallError::from_cdk_error("bitcoin_send_transaction", err))
}

/// Fetches the ECDSA public key of the canister.
Expand Down Expand Up @@ -342,3 +369,11 @@ pub async fn check_transaction(
})?;
Ok(res)
}

fn cdk_network(network: Network) -> BitcoinNetwork {
match network {
Network::Mainnet => BitcoinNetwork::Mainnet,
Network::Testnet => BitcoinNetwork::Testnet,
Network::Regtest => BitcoinNetwork::Regtest,
}
}
2 changes: 1 addition & 1 deletion rs/bitcoin/ckbtc/minter/src/test_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub mod mock {
fn id(&self) -> Principal;
fn time(&self) -> u64;
fn global_timer_set(&self, timestamp: u64);
async fn bitcoin_get_utxos(&self, request: &GetUtxosRequest, cycles: u64) -> Result<GetUtxosResponse, CallError>;
async fn bitcoin_get_utxos(&self, request: GetUtxosRequest) -> Result<GetUtxosResponse, CallError>;
async fn check_transaction(&self, btc_checker_principal: Principal, utxo: &Utxo, cycle_payment: u128, ) -> Result<CheckTransactionResponse, CallError>;
async fn mint_ckbtc(&self, amount: u64, to: Account, memo: Memo) -> Result<u64, UpdateBalanceError>;
}
Expand Down
14 changes: 9 additions & 5 deletions rs/bitcoin/mock/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ fn set_tip_height(tip_height: u32) {
#[update]
fn bitcoin_get_utxos(utxos_request: GetUtxosRequest) -> GetUtxosResponse {
read_state(|s| {
assert_eq!(utxos_request.network, s.network.into());
assert_eq!(Network::from(utxos_request.network), s.network);

let mut utxos = s
.address_to_utxos
Expand All @@ -98,10 +98,14 @@ fn bitcoin_get_utxos(utxos_request: GetUtxosRequest) -> GetUtxosResponse {
.cloned()
.collect::<Vec<Utxo>>();

if let Some(UtxosFilterInRequest::MinConfirmations(min_confirmations)) =
utxos_request.filter
{
utxos.retain(|u| s.tip_height + 1 >= u.height + min_confirmations);
if let Some(filter) = utxos_request.filter {
match filter {
UtxosFilterInRequest::MinConfirmations(min_confirmations)
| UtxosFilterInRequest::min_confirmations(min_confirmations) => {
utxos.retain(|u| s.tip_height + 1 >= u.height + min_confirmations)
}
UtxosFilterInRequest::Page(_) | UtxosFilterInRequest::page(_) => {}
}
}

GetUtxosResponse {
Expand Down
Loading