Skip to content

Commit

Permalink
feat: Add stub for Bitcoin headers endpoint (#297)
Browse files Browse the repository at this point in the history
As stated in [here](dfinity/interface-spec#298),
candid interface should look as:

```
type bitcoin_block_header = blob;

type get_block_headers_request = record {
	start_height : nat32;
	end_height: opt nat32;
};

type get_block_headers_result = record {
	tip_height: nat32;
	block_headers: vec bitcoin_block_header;
};

bitcoin_get_block_headers : (get_block_headers_request) -> (get_block_headers_response);
```
This PR will add no-op endpoint for returning the block headers in the
[follow-up](#298) we
will implement specified function.
  • Loading branch information
dragoljub-duric authored May 21, 2024
1 parent 13a8853 commit 4d420e1
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 8 deletions.
20 changes: 18 additions & 2 deletions canister/candid.did
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ type address = text;

type block_hash = blob;

type block_header = blob;

type block_height = nat32;

type outpoint = record {
txid : blob;
vout : nat32;
Expand All @@ -18,7 +22,7 @@ type outpoint = record {
type utxo = record {
outpoint : outpoint;
value : satoshi;
height : nat32;
height : block_height;
};

type flag = variant {
Expand Down Expand Up @@ -68,7 +72,7 @@ type get_utxos_request = record {
type get_utxos_response = record {
utxos : vec utxo;
tip_block_hash : block_hash;
tip_height : nat32;
tip_height : block_height;
next_page : opt blob;
};

Expand All @@ -93,6 +97,16 @@ type set_config_request = record {
burn_cycles : opt flag;
};

type get_block_headers_request = record {
start_height : block_height;
end_height : opt block_height;
};

type get_block_headers_response = record {
tip_height : block_height;
block_headers : vec block_header;
};

service bitcoin : (config) -> {
bitcoin_get_balance : (get_balance_request) -> (satoshi);

Expand All @@ -104,6 +118,8 @@ service bitcoin : (config) -> {

bitcoin_get_current_fee_percentiles : (get_current_fee_percentiles_request) -> (vec millisatoshi_per_byte);

bitcoin_get_block_headers : (get_block_headers_request) -> (get_block_headers_response);

bitcoin_send_transaction : (send_transaction_request) -> ();

get_config : () -> (config) query;
Expand Down
2 changes: 2 additions & 0 deletions canister/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
mod fee_percentiles;
mod get_balance;
mod get_block_headers;
mod get_utxos;
mod metrics;
mod send_transaction;
mod set_config;
pub use fee_percentiles::get_current_fee_percentiles;
pub use get_balance::get_balance;
pub use get_balance::get_balance_query;
pub use get_block_headers::get_block_headers;
pub use get_utxos::get_utxos;
pub use get_utxos::get_utxos_query;
pub use metrics::get_metrics;
Expand Down
65 changes: 65 additions & 0 deletions canister/src/api/get_block_headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use ic_btc_interface::{GetBlockHeadersError, GetBlockHeadersRequest, GetBlockHeadersResponse};

fn verify_get_block_headers_request(
request: GetBlockHeadersRequest,
) -> Result<(), GetBlockHeadersError> {
if let Some(end_height) = request.end_height {
if end_height < request.start_height {
return Err(GetBlockHeadersError::StartHeightLagerThanEndHeight {
start_height: request.start_height,
end_height,
});
}
}
Ok(())
}

/// Given a start height and an optional end height from request,
/// the function returns the block headers in the provided range.
/// The range is inclusive, i.e., the block headers at the start
/// and end heights are returned as well.
/// If no end height is specified, all blocks until the tip height,
/// i.e., the largest available height, are returned. However, if
/// the range from the start height to the end height or the tip
/// height is large, only a prefix of the requested block headers
/// may be returned in order to bound the size of the response.
pub fn get_block_headers(
request: GetBlockHeadersRequest,
) -> Result<GetBlockHeadersResponse, GetBlockHeadersError> {
verify_get_block_headers_request(request)?;
unimplemented!("get_block_headers is not implemented")
}

#[cfg(test)]
mod test {
use ic_btc_interface::{Config, GetBlockHeadersError, GetBlockHeadersRequest, Network};

use crate::api::get_block_headers;

#[test]
fn get_block_headers_malformed_heights() {
crate::init(Config {
stability_threshold: 1,
network: Network::Mainnet,
..Default::default()
});

let start_height = 3;
let end_height = 2;

let err = get_block_headers(GetBlockHeadersRequest {
start_height,
end_height: Some(end_height),
})
.unwrap_err();

assert_eq!(
err,
GetBlockHeadersError::StartHeightLagerThanEndHeight {
start_height,
end_height,
}
);
}
}
29 changes: 27 additions & 2 deletions canister/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ pub use api::send_transaction;
pub use api::set_config;
pub use heartbeat::heartbeat;
use ic_btc_interface::{
Config, Flag, GetBalanceError, GetBalanceRequest, GetCurrentFeePercentilesRequest,
GetUtxosError, GetUtxosRequest, GetUtxosResponse, MillisatoshiPerByte, Network, Satoshi,
Config, Flag, GetBalanceError, GetBalanceRequest, GetBlockHeadersError, GetBlockHeadersRequest,
GetBlockHeadersResponse, GetCurrentFeePercentilesRequest, GetUtxosError, GetUtxosRequest,
GetUtxosResponse, MillisatoshiPerByte, Network, Satoshi,
};
use ic_btc_types::Block;
use ic_stable_structures::Memory;
Expand Down Expand Up @@ -134,6 +135,14 @@ pub fn get_utxos_query(request: GetUtxosRequest) -> Result<GetUtxosResponse, Get
api::get_utxos_query(request.into())
}

pub fn get_block_headers(
request: GetBlockHeadersRequest,
) -> Result<GetBlockHeadersResponse, GetBlockHeadersError> {
verify_api_access();
verify_synced();
api::get_block_headers(request)
}

pub fn get_config() -> Config {
with_state(|s| Config {
stability_threshold: s.unstable_blocks.stability_threshold() as u128,
Expand Down Expand Up @@ -484,6 +493,22 @@ mod test {
.unwrap();
}

#[test]
#[should_panic(expected = "Bitcoin API is disabled")]
fn get_block_headers_access_disabled() {
init(Config {
stability_threshold: 0,
network: Network::Mainnet,
api_access: Flag::Disabled,
..Default::default()
});
get_block_headers(GetBlockHeadersRequest {
start_height: 3,
end_height: None,
})
.unwrap();
}

#[test]
#[should_panic(expected = "Bitcoin API is disabled")]
fn get_utxos_query_access_disabled() {
Expand Down
12 changes: 10 additions & 2 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use ic_btc_canister::types::{HttpRequest, HttpResponse};
use ic_btc_interface::{
Config, GetBalanceRequest, GetCurrentFeePercentilesRequest, GetUtxosRequest,
MillisatoshiPerByte, SendTransactionRequest, SetConfigRequest,
Config, GetBalanceRequest, GetBlockHeadersRequest, GetCurrentFeePercentilesRequest,
GetUtxosRequest, MillisatoshiPerByte, SendTransactionRequest, SetConfigRequest,
};
use ic_cdk::api::call::{reject, reply};
use ic_cdk_macros::{heartbeat, init, inspect_message, post_upgrade, pre_upgrade, query, update};
Expand Down Expand Up @@ -76,6 +76,14 @@ pub fn bitcoin_get_utxos_query(request: GetUtxosRequest) {
};
}

#[update(manual_reply = true)]
pub fn bitcoin_get_block_headers(request: GetBlockHeadersRequest) {
match ic_btc_canister::get_block_headers(request) {
Ok(response) => reply((response,)),
Err(e) => reject(format!("get_block_headers failed: {:?}", e).as_str()),
};
}

#[update(manual_reply = true)]
async fn bitcoin_send_transaction(request: SendTransactionRequest) {
match ic_btc_canister::send_transaction(request).await {
Expand Down
12 changes: 12 additions & 0 deletions e2e-tests/disable-api-if-not-fully-synced-flag.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ if ! [[ $MSG = *"Canister state is not fully synced."* ]]; then
exit 1
fi

# bitcoin_get_block_headers should panic.
set +e
MSG=$(dfx canister call bitcoin bitcoin_get_block_headers '(record {
start_height = 0;
})' 2>&1);
set -e

if ! [[ $MSG = *"Canister state is not fully synced."* ]]; then
echo "FAIL"
exit 1
fi

# bitcoin_get_utxos_query should panic.
set +e
MSG=$(dfx canister call --query bitcoin bitcoin_get_utxos_query '(record {
Expand Down
71 changes: 69 additions & 2 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub type MillisatoshiPerByte = u64;
pub type BlockHash = Vec<u8>;
pub type Height = u32;
pub type Page = ByteBuf;
pub type BlockHeader = Vec<u8>;

#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
pub enum Network {
Expand Down Expand Up @@ -272,7 +273,7 @@ pub struct OutPoint {
pub struct Utxo {
pub outpoint: OutPoint,
pub value: Satoshi,
pub height: u32,
pub height: Height,
}

impl std::cmp::PartialOrd for Utxo {
Expand Down Expand Up @@ -333,7 +334,7 @@ pub struct GetUtxosRequest {
pub struct GetUtxosResponse {
pub utxos: Vec<Utxo>,
pub tip_block_hash: BlockHash,
pub tip_height: u32,
pub tip_height: Height,
pub next_page: Option<Page>,
}

Expand All @@ -346,6 +347,72 @@ pub enum GetUtxosError {
MalformedPage { err: String },
}

/// A request for getting the block headers from a given height.
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct GetBlockHeadersRequest {
pub start_height: Height,
pub end_height: Option<Height>,
}

/// The response returned for a request for getting the block headers from a given height.
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
pub struct GetBlockHeadersResponse {
pub tip_height: Height,
pub block_headers: Vec<BlockHeader>,
}

/// Errors when processing a `get_block_headers` request.
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
pub enum GetBlockHeadersError {
StartHeightDoesNotExist {
requested: Height,
chain_height: Height,
},
EndHeightDoesNotExist {
requested: Height,
chain_height: Height,
},
StartHeightLagerThanEndHeight {
start_height: Height,
end_height: Height,
},
}

impl fmt::Display for GetBlockHeadersError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StartHeightDoesNotExist {
requested,
chain_height,
} => {
write!(
f,
"The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
requested, chain_height
)
}
Self::EndHeightDoesNotExist {
requested,
chain_height,
} => {
write!(
f,
"The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
requested, chain_height
)
}
Self::StartHeightLagerThanEndHeight {
start_height,
end_height,
} => {
write!(
f,
"The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
}
}
}
}

/// A request for getting the current fee percentiles.
#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
pub struct GetCurrentFeePercentilesRequest {
Expand Down

0 comments on commit 4d420e1

Please sign in to comment.