diff --git a/canister/proptest-regressions/blocktree.txt b/canister/proptest-regressions/blocktree.txt new file mode 100644 index 00000000..d92a754c --- /dev/null +++ b/canister/proptest-regressions/blocktree.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc f9ce64e7b8f2ec6d8dd535b56e1aad46443a3e6589852a73a6b2d9778e4e9870 # shrinks to input = _SerializeDeserializeArgs { tree: BlockTree { root: Block { block: Block { header: BlockHeader { version: 1, prev_blockhash: 0000000000000000000000000000000000000000000000000000000000000000, merkle_root: 91ee09dd76d9df4a91913067f13fd47dca48420335cd64ed5ca6262bfe3ecbeb, time: 0, bits: 545259519, nonce: 2 }, txdata: [Transaction { version: 1, lock_time: 0, input: [TxIn { previous_output: OutPoint { txid: 0000000000000000000000000000000000000000000000000000000000000000, vout: 4294967295 }, script_sig: Script(), sequence: 4294967295, witness: Witness { content: [], witness_elements: 0, last: 0, second_to_last: 0 } }], output: [TxOut { value: 5000000000, script_pubkey: Script(OP_DUP OP_HASH160 OP_PUSHBYTES_20 1c06d2ab6d4557646aeda8477edc4143bb728b7f OP_EQUALVERIFY OP_CHECKSIG) }] }] }, transactions: [Transaction { tx: Transaction { version: 1, lock_time: 0, input: [TxIn { previous_output: OutPoint { txid: 0000000000000000000000000000000000000000000000000000000000000000, vout: 4294967295 }, script_sig: Script(), sequence: 4294967295, witness: Witness { content: [], witness_elements: 0, last: 0, second_to_last: 0 } }], output: [TxOut { value: 5000000000, script_pubkey: Script(OP_DUP OP_HASH160 OP_PUSHBYTES_20 1c06d2ab6d4557646aeda8477edc4143bb728b7f OP_EQUALVERIFY OP_CHECKSIG) }] }, txid: RefCell { value: None } }], block_hash: RefCell { value: None }, mock_difficulty: None }, children: [] } } diff --git a/canister/proptest-regressions/unstable_blocks.txt b/canister/proptest-regressions/unstable_blocks.txt new file mode 100644 index 00000000..ce5be8ea --- /dev/null +++ b/canister/proptest-regressions/unstable_blocks.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 6aabdb0186750fb87205a01c1180677092a2a437c1d6c0659d6f7ff7cffe0059 # shrinks to start_range = 0, range_length = 2 diff --git a/canister/src/api/get_block_headers.rs b/canister/src/api/get_block_headers.rs index bd92be44..1cc504b3 100644 --- a/canister/src/api/get_block_headers.rs +++ b/canister/src/api/get_block_headers.rs @@ -1,17 +1,125 @@ +use bitcoin::consensus::Encodable; 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, - }); - } +use crate::{ + runtime::{performance_counter, print}, + state::main_chain_height, + with_state, with_state_mut, +}; + +// The maximum number of block headers that are allowed to be included in a single +// `GetBlockHeadersResponse`. +const MAX_BLOCK_HEADERS_PER_RESPONSE: u32 = 100; + +// Various profiling stats for tracking the performance of `get_block_headers`. +#[derive(Default, Debug)] +struct Stats { + // The total number of instructions used to process the request. + ins_total: u64, + + // The number of instructions used to build the block headers vec from stable blocks. + ins_build_block_headers_stable_blocks: u64, + + // The number of instructions used to build the block headers vec from unstable blocks. + ins_build_block_headers_unstable_blocks: u64, +} + +fn verify_and_return_effective_range( + request: &GetBlockHeadersRequest, +) -> Result<(u32, u32), GetBlockHeadersError> { + let chain_height = with_state(main_chain_height); + + if request.start_height > chain_height { + return Err(GetBlockHeadersError::StartHeightDoesNotExist { + requested: request.start_height, + chain_height, + }); } - Ok(()) + + let (effective_start_height, mut effective_end_height) = + if let Some(end_height) = request.end_height { + if end_height < request.start_height { + return Err(GetBlockHeadersError::StartHeightLargerThanEndHeight { + start_height: request.start_height, + end_height, + }); + } + + if end_height > chain_height { + return Err(GetBlockHeadersError::EndHeightDoesNotExist { + requested: end_height, + chain_height, + }); + } + // If `end_height` is provided then it should be the + // end of effective height range. + (request.start_height, end_height) + } else { + // If `end_height` is not provided then the end of effective + // range should be the last block of the chain. + (request.start_height, chain_height) + }; + + // Bound the length of block headers vec. + effective_end_height = std::cmp::min( + effective_end_height, + effective_start_height + MAX_BLOCK_HEADERS_PER_RESPONSE - 1, + ); + + Ok((effective_start_height, effective_end_height)) +} + +fn get_block_headers_internal( + request: &GetBlockHeadersRequest, +) -> Result<(GetBlockHeadersResponse, Stats), GetBlockHeadersError> { + let (start_height, end_height) = verify_and_return_effective_range(request)?; + + let mut stats: Stats = Stats::default(); + + // Build block headers vec. + let ins_start = performance_counter(); + + // Add requested block headers located in stable_blocks. + let mut vec_headers: Vec> = with_state(|s| { + s.stable_block_headers + .get_block_headers_in_range(std::ops::RangeInclusive::new(start_height, end_height)) + .map(|header_blob| header_blob.into()) + .collect() + }); + + let ins_after_stable_blocks = performance_counter(); + + stats.ins_build_block_headers_stable_blocks = ins_after_stable_blocks - ins_start; + + // Add requested block headers located in unstable_blocks. + with_state(|s| { + let unstable_block_headers = &mut s + .unstable_blocks + .get_block_headers_in_range( + s.stable_height(), + std::ops::RangeInclusive::new(start_height, end_height), + ) + // Serialize headers of unstable blocks. + .map(|header| { + let mut serialized_header = vec![]; + header.consensus_encode(&mut serialized_header).unwrap(); + serialized_header + }) + .collect(); + + vec_headers.append(unstable_block_headers) + }); + + stats.ins_build_block_headers_unstable_blocks = performance_counter() - ins_after_stable_blocks; + stats.ins_total = performance_counter(); + + Ok(( + GetBlockHeadersResponse { + tip_height: end_height, + block_headers: vec_headers, + }, + stats, + )) } /// Given a start height and an optional end height from request, @@ -27,26 +135,65 @@ fn verify_get_block_headers_request( pub fn get_block_headers( request: GetBlockHeadersRequest, ) -> Result { - verify_get_block_headers_request(request)?; - unimplemented!("get_block_headers is not implemented") + let (res, stats) = get_block_headers_internal(&request)?; + + // Observe metrics. + with_state_mut(|s| { + s.metrics.get_block_headers_total.observe(stats.ins_total); + + s.metrics + .get_block_headers_stable_blocks + .observe(stats.ins_build_block_headers_stable_blocks); + + s.metrics + .get_block_headers_unstable_blocks + .observe(stats.ins_build_block_headers_unstable_blocks); + }); + + // Print the number of instructions it took to process this request. + print(&format!("[INSTRUCTION COUNT] {:?}: {:?}", request, stats)); + Ok(res) } #[cfg(test)] mod test { - use ic_btc_interface::{GetBlockHeadersError, GetBlockHeadersRequest, InitConfig, Network}; - - use crate::api::get_block_headers; + use super::*; + use crate::{ + genesis_block, + state::{self, ingest_stable_blocks_into_utxoset, insert_block}, + test_utils::BlockBuilder, + with_state_mut, + }; + use bitcoin::consensus::Encodable; + use ic_btc_interface::{InitConfig, Network}; + use proptest::prelude::*; - #[test] - fn get_block_headers_malformed_heights() { + fn get_block_headers_helper() { + let network = Network::Regtest; crate::init(InitConfig { stability_threshold: Some(1), - network: Some(Network::Mainnet), + network: Some(network), ..Default::default() }); - let start_height = 3; - let end_height = 2; + let block1 = BlockBuilder::with_prev_header(genesis_block(network).header()).build(); + let block2 = BlockBuilder::with_prev_header(block1.clone().header()).build(); + + // Insert the blocks. + // Genesis block and block1 should be stable, while block2 should be unstable. + with_state_mut(|state| { + insert_block(state, block1).unwrap(); + insert_block(state, block2).unwrap(); + ingest_stable_blocks_into_utxoset(state); + }); + } + + #[test] + fn get_block_headers_malformed_heights() { + get_block_headers_helper(); + + let start_height = 1; + let end_height = 0; let err = get_block_headers(GetBlockHeadersRequest { start_height, @@ -56,10 +203,346 @@ mod test { assert_eq!( err, - GetBlockHeadersError::StartHeightLagerThanEndHeight { + GetBlockHeadersError::StartHeightLargerThanEndHeight { start_height, end_height, } ); } + + #[test] + fn start_height_does_not_exist() { + get_block_headers_helper(); + + let start_height: u32 = 3; + + let err = get_block_headers(GetBlockHeadersRequest { + start_height, + end_height: None, + }) + .unwrap_err(); + + assert_eq!( + err, + GetBlockHeadersError::StartHeightDoesNotExist { + requested: start_height, + chain_height: 2 + } + ); + } + + #[test] + fn end_height_does_not_exist() { + get_block_headers_helper(); + + let start_height: u32 = 1; + let end_height: u32 = 4; + + let err = get_block_headers(GetBlockHeadersRequest { + start_height, + end_height: Some(end_height), + }) + .unwrap_err(); + + assert_eq!( + err, + GetBlockHeadersError::EndHeightDoesNotExist { + requested: end_height, + chain_height: 2 + } + ); + } + + #[test] + fn genesis_block_only() { + let network = Network::Regtest; + crate::init(InitConfig { + stability_threshold: Some(1), + network: Some(network), + ..Default::default() + }); + + let mut genesis_header_blob = vec![]; + genesis_block(network) + .header() + .consensus_encode(&mut genesis_header_blob) + .unwrap(); + + // We request all block headers starting from height 0, until the end of the chain. + let response: GetBlockHeadersResponse = get_block_headers(GetBlockHeadersRequest { + start_height: 0, + end_height: None, + }) + .unwrap(); + + // The result should contain the header of the genesis block since it is the only block in the chain. + assert_eq!( + response, + GetBlockHeadersResponse { + tip_height: 0, + block_headers: vec![genesis_header_blob.clone()] + } + ); + + // We request a block at height 0. + let response: GetBlockHeadersResponse = get_block_headers(GetBlockHeadersRequest { + start_height: 0, + end_height: Some(0), + }) + .unwrap(); + + // The result should contain the header of the genesis block. + assert_eq!( + response, + GetBlockHeadersResponse { + tip_height: 0, + block_headers: vec![genesis_header_blob] + } + ); + } + + #[test] + fn single_block() { + let network = Network::Regtest; + crate::init(InitConfig { + stability_threshold: Some(1), + network: Some(network), + ..Default::default() + }); + + let block = BlockBuilder::with_prev_header(genesis_block(network).header()).build(); + + // Insert the block. + with_state_mut(|state| { + state::insert_block(state, block.clone()).unwrap(); + }); + + let mut genesis_header_blob = vec![]; + genesis_block(network) + .header() + .consensus_encode(&mut genesis_header_blob) + .unwrap(); + + // The response should contain the header of the genesis block. + assert_eq!( + get_block_headers(GetBlockHeadersRequest { + start_height: 0, + end_height: Some(0), + }) + .unwrap(), + GetBlockHeadersResponse { + tip_height: 0, + block_headers: vec![genesis_header_blob.clone()] + } + ); + + let mut block_header_blob = vec![]; + block + .header() + .consensus_encode(&mut block_header_blob) + .unwrap(); + + // The response should contain the header of `block`. + assert_eq!( + get_block_headers(GetBlockHeadersRequest { + start_height: 1, + end_height: Some(1), + }) + .unwrap(), + GetBlockHeadersResponse { + tip_height: 1, + block_headers: vec![block_header_blob.clone()] + } + ); + + // The response should contain the header of `block`. + assert_eq!( + get_block_headers(GetBlockHeadersRequest { + start_height: 1, + end_height: None, + }) + .unwrap(), + GetBlockHeadersResponse { + tip_height: 1, + block_headers: vec![block_header_blob.clone()] + } + ); + + // The response should contain headers of all blocks. + assert_eq!( + get_block_headers(GetBlockHeadersRequest { + start_height: 0, + end_height: Some(1), + }) + .unwrap(), + GetBlockHeadersResponse { + tip_height: 1, + block_headers: vec![genesis_header_blob.clone(), block_header_blob.clone()] + } + ); + + // The response should contain headers of all blocks. + assert_eq!( + get_block_headers(GetBlockHeadersRequest { + start_height: 0, + end_height: None, + }) + .unwrap(), + GetBlockHeadersResponse { + tip_height: 1, + block_headers: vec![genesis_header_blob.clone(), block_header_blob.clone()] + } + ); + } + + fn helper_initialize_and_get_header_blobs( + stability_threshold: u128, + block_num: u32, + network: Network, + ) -> Vec> { + crate::init(InitConfig { + stability_threshold: Some(stability_threshold), + network: Some(network), + ..Default::default() + }); + let genesis_block = genesis_block(network); + + let mut prev_block_header = *genesis_block.header(); + let mut genesis_header_blob = vec![]; + genesis_block + .header() + .consensus_encode(&mut genesis_header_blob) + .unwrap(); + + let mut blobs = vec![genesis_header_blob]; + + // Genesis block is already added hence we need to add `block_num - 1` more blocks. + for _ in 0..block_num - 1 { + let block = BlockBuilder::with_prev_header(&prev_block_header).build(); + prev_block_header = *block.header(); + + let mut block_blob = vec![]; + block.header().consensus_encode(&mut block_blob).unwrap(); + blobs.push(block_blob); + + with_state_mut(|state| insert_block(state, block).unwrap()); + } + + with_state_mut(ingest_stable_blocks_into_utxoset); + + blobs + } + + fn check_response( + blobs: &[Vec], + start_height: u32, + end_height: Option, + total_num_blocks: u32, + ) { + let response: GetBlockHeadersResponse = get_block_headers(GetBlockHeadersRequest { + start_height, + end_height, + }) + .unwrap(); + + // If the requested `end_height` is `None`, the tip should be the last block. + // The Length of block headers vec in response should be bounded by `MAX_BLOCK_HEADERS_PER_RESPONSE`. + let tip_height = end_height + .unwrap_or(total_num_blocks - 1) + .min(start_height + MAX_BLOCK_HEADERS_PER_RESPONSE - 1); + + assert_eq!( + response, + GetBlockHeadersResponse { + tip_height, + block_headers: blobs[start_height as usize..=tip_height as usize].into() + } + ); + } + + fn test_all_valid_combination_or_height_range(blobs: &[Vec], block_num: u32) { + for start_height in 0..block_num { + let mut end_height_range: Vec> = + (start_height..block_num).map(Some).collect::>(); + end_height_range.push(None); + for end_height in end_height_range { + check_response(blobs, start_height, end_height, block_num); + } + } + } + + #[test] + fn get_block_headers_chain_10_blocks_all_combinations() { + let stability_threshold = 3; + let block_num: u32 = 10; + let network = Network::Regtest; + + let blobs: Vec> = + helper_initialize_and_get_header_blobs(stability_threshold, block_num, network); + + test_all_valid_combination_or_height_range(&blobs, block_num); + } + + #[test] + fn get_block_headers_test_max_block_headers_per_response() { + let stability_threshold = 3; + let block_num: u32 = MAX_BLOCK_HEADERS_PER_RESPONSE * 2 + 3; + let network = Network::Regtest; + + let blobs: Vec> = + helper_initialize_and_get_header_blobs(stability_threshold, block_num, network); + + check_response(&blobs, 0, None, block_num); + check_response(&blobs, MAX_BLOCK_HEADERS_PER_RESPONSE / 2, None, block_num); + check_response(&blobs, MAX_BLOCK_HEADERS_PER_RESPONSE, None, block_num); + + check_response( + &blobs, + 0, + Some(MAX_BLOCK_HEADERS_PER_RESPONSE + 1), + block_num, + ); + check_response( + &blobs, + MAX_BLOCK_HEADERS_PER_RESPONSE / 2, + Some(3 * MAX_BLOCK_HEADERS_PER_RESPONSE / 2 + 1), + block_num, + ); + check_response( + &blobs, + MAX_BLOCK_HEADERS_PER_RESPONSE, + Some(2 * MAX_BLOCK_HEADERS_PER_RESPONSE + 1), + block_num, + ); + + check_response( + &blobs, + 0, + Some(2 * MAX_BLOCK_HEADERS_PER_RESPONSE + 1), + block_num, + ); + } + + #[test] + fn get_block_headers_proptest() { + let stability_threshold = 3; + let block_num = 200; + let blobs: Vec> = helper_initialize_and_get_header_blobs( + stability_threshold, + block_num, + Network::Regtest, + ); + + proptest!(|( + start_height in 0..=block_num - 1, + length in 1..=block_num)|{ + let end_height = if start_height + length - 1 < block_num { + Some(start_height + length - 1) + } else { + None + }; + check_response(&blobs, start_height, end_height, block_num); + } + ); + } } diff --git a/canister/src/block_header_store.rs b/canister/src/block_header_store.rs index e2bb86f0..8df323d4 100644 --- a/canister/src/block_header_store.rs +++ b/canister/src/block_header_store.rs @@ -71,6 +71,16 @@ impl BlockHeaderStore { .expect("block header must exist") }) } + + /// Returns iterator on block headers in the range `heights`. + pub fn get_block_headers_in_range( + &self, + heights: std::ops::RangeInclusive, + ) -> impl Iterator + '_ { + self.block_heights + .range(heights) + .map(move |(_, block_hash)| self.block_headers.get(&block_hash).unwrap()) + } } fn deserialize_block_header(block_header_blob: BlockHeaderBlob) -> BlockHeader { @@ -85,3 +95,50 @@ fn init_block_headers() -> StableBTreeMap { fn init_block_heights() -> StableBTreeMap { StableBTreeMap::init(crate::memory::get_block_heights_memory()) } + +#[cfg(test)] +mod test { + use bitcoin::consensus::Encodable; + use proptest::proptest; + + use crate::{ + block_header_store::BlockHeaderStore, test_utils::BlockBuilder, types::BlockHeaderBlob, + }; + + #[test] + fn test_get_block_headers_in_range() { + let mut headers = vec![]; + let block_0 = BlockBuilder::genesis().build(); + headers.push(*block_0.header()); + + let mut store = BlockHeaderStore::init(); + store.insert_block(&block_0, 0); + let block_num = 100; + + for i in 1..block_num { + let block = BlockBuilder::with_prev_header(&headers[i - 1]).build(); + headers.push(*block.header()); + store.insert_block(&block, i as u32); + } + + proptest!(|( + start_range in 0..=block_num - 1, + range_length in 1..=block_num)|{ + let requested_end = start_range + range_length - 1; + + let res: Vec= store.get_block_headers_in_range(std::ops::RangeInclusive::new(start_range as u32, requested_end as u32)).collect(); + + let end_range = std::cmp::min(requested_end, block_num - 1); + + assert_eq!(res.len(), end_range - start_range + 1); + + for i in start_range..=end_range{ + let mut expected_block_header = vec![]; + headers[i].consensus_encode(&mut expected_block_header).unwrap(); + let actual_block_header: Vec = res[i - start_range].clone().into(); + assert_eq!(expected_block_header, actual_block_header); + } + } + ); + } +} diff --git a/canister/src/main.rs b/canister/src/main.rs index bec7018b..0b96e752 100644 --- a/canister/src/main.rs +++ b/canister/src/main.rs @@ -77,9 +77,11 @@ pub fn bitcoin_get_utxos_query(request: GetUtxosRequest) -> ManualReply ManualReply { - match ic_btc_canister::get_block_headers(request) { + unimplemented!("The api is not implemented"); + #[allow(unreachable_code)] + match ic_btc_canister::get_block_headers(_request) { Ok(response) => ManualReply::one(response), Err(e) => ManualReply::reject(format!("get_block_headers failed: {:?}", e).as_str()), } diff --git a/canister/src/metrics.rs b/canister/src/metrics.rs index 09cf361f..033f7c75 100644 --- a/canister/src/metrics.rs +++ b/canister/src/metrics.rs @@ -13,6 +13,13 @@ pub struct Metrics { pub get_utxos_apply_unstable_blocks: InstructionHistogram, pub get_utxos_build_utxos_vec: InstructionHistogram, + #[serde(default = "default_get_block_headers_total")] + pub get_block_headers_total: InstructionHistogram, + #[serde(default = "default_get_block_headers_stable_blocks")] + pub get_block_headers_stable_blocks: InstructionHistogram, + #[serde(default = "default_get_block_headers_unstable_blocks")] + pub get_block_headers_unstable_blocks: InstructionHistogram, + pub get_balance_total: InstructionHistogram, pub get_balance_apply_unstable_blocks: InstructionHistogram, @@ -47,6 +54,10 @@ impl Default for Metrics { "Instructions needed to build the UTXOs vec in a get_utxos request.", ), + get_block_headers_total: default_get_block_headers_total(), + get_block_headers_stable_blocks: default_get_block_headers_stable_blocks(), + get_block_headers_unstable_blocks: default_get_block_headers_unstable_blocks(), + get_balance_total: InstructionHistogram::new( "ins_get_balance_total", "Instructions needed to execute a get_balance request.", @@ -130,6 +141,27 @@ impl InstructionHistogram { } } +fn default_get_block_headers_total() -> InstructionHistogram { + InstructionHistogram::new( + "ins_block_headers_total", + "Instructions needed to execute a get_block_headers request.", + ) +} + +fn default_get_block_headers_stable_blocks() -> InstructionHistogram { + InstructionHistogram::new( + "inst_count_get_block_headers_stable_blocks", + "Instructions needed to build the block headers vec in a get_block_headers request from stable blocks.", + ) +} + +fn default_get_block_headers_unstable_blocks() -> InstructionHistogram { + InstructionHistogram::new( + "inst_count_get_block_headers_unstable_blocks", + "Instructions needed to build the block headers vec in a get_block_headers request from unstable blocks.", + ) +} + #[cfg(test)] mod test { use super::*; diff --git a/canister/src/types.rs b/canister/src/types.rs index 4c233e09..ade1b3b8 100644 --- a/canister/src/types.rs +++ b/canister/src/types.rs @@ -342,6 +342,12 @@ impl From> for BlockHeaderBlob { } } +impl From for Vec { + fn from(block_header: BlockHeaderBlob) -> Vec { + block_header.0 + } +} + type PageNumber = u8; #[derive(CandidType, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/canister/src/unstable_blocks.rs b/canister/src/unstable_blocks.rs index a79a4e42..35e2eaf8 100644 --- a/canister/src/unstable_blocks.rs +++ b/canister/src/unstable_blocks.rs @@ -1,4 +1,5 @@ mod outpoints_cache; + use crate::{ blocktree::{BlockChain, BlockDoesNotExtendTree, BlockTree}, runtime::print, @@ -167,6 +168,32 @@ impl UnstableBlocks { chain.reverse(); chain } + + /// Returns block headers of all unstable blocks in height range `heights`. + pub fn get_block_headers_in_range( + &self, + stable_height: Height, + heights: std::ops::RangeInclusive, + ) -> impl Iterator { + if *heights.end() < stable_height { + // `stable_height` is larger than any height from the range, which implies none of the requested + // blocks are in unstable blocks, hence the result should be an empty iterator. + return Default::default(); + } + + // The last stable block is located in `unstable_blocks`, hence the height of the + // first block in `unstable_blocks` is equal to `stable_height`. + let heights_relative_to_unstable_blocks = std::ops::RangeInclusive::new( + heights.start().saturating_sub(stable_height) as usize, + heights.end().checked_sub(stable_height).unwrap() as usize, + ); + + get_main_chain(self).into_chain()[heights_relative_to_unstable_blocks] + .iter() + .map(|block| block.header()) + .collect::>() + .into_iter() + } } /// Returns a reference to the `anchor` block iff ∃ a child `C` of `anchor` that is stable. @@ -403,6 +430,7 @@ mod test { use super::*; use crate::test_utils::{BlockBuilder, BlockChainBuilder}; use ic_btc_interface::Network; + use proptest::proptest; #[test] fn empty() { @@ -997,4 +1025,59 @@ mod test { // is considered unstable. assert_eq!(peek(&unstable_blocks), None); } + + fn get_block_headers_helper(block_num: usize) -> (UnstableBlocks, Vec) { + let mut headers = vec![]; + let block_0 = BlockBuilder::genesis().build(); + headers.push(*block_0.header()); + + let network = Network::Mainnet; + let utxos = UtxoSet::new(network); + let mut unstable_blocks = UnstableBlocks::new(&utxos, 1, block_0.clone(), network); + + for i in 1..block_num { + let block = BlockBuilder::with_prev_header(&headers[i - 1]).build(); + headers.push(*block.header()); + push(&mut unstable_blocks, &utxos, block).unwrap(); + } + + (unstable_blocks, headers) + } + + #[test] + fn test_get_block_headers_in_range_in_stable_blocks() { + let block_num = 15; + + let (unstable_blocks, _) = get_block_headers_helper(block_num); + + let stable_height = 10; + let range = std::ops::RangeInclusive::new(0, stable_height - 1); + + // `stable_height` is larger than any height from the range, which implies none of the requested + // blocks are in unstable blocks, hence the result should be an empty iterator. + assert!(unstable_blocks + .get_block_headers_in_range(stable_height, range) + .eq([].iter())); + } + + #[test] + fn test_get_block_headers_in_range() { + let block_num = 100; + + let (unstable_blocks, headers) = get_block_headers_helper(block_num); + + proptest!(|( + start_range in 0..=block_num - 1, + range_length in 1..=block_num)|{ + let end_range = std::cmp::min(start_range + range_length - 1, block_num - 1 ); + + let mut result = unstable_blocks.get_block_headers_in_range(0, std::ops::RangeInclusive::new(start_range as u32, end_range as u32)).peekable(); + + for expected_result in headers.iter().take(end_range + 1).skip(start_range){ + assert_eq!(expected_result, *result.peek().unwrap()); + result.next(); + } + } + ); + } } diff --git a/e2e-tests/charge-cycles-on-reject.sh b/e2e-tests/charge-cycles-on-reject.sh old mode 100644 new mode 100755 diff --git a/e2e-tests/disable-api-if-not-fully-synced-flag.sh b/e2e-tests/disable-api-if-not-fully-synced-flag.sh index 4383b234..3356372e 100755 --- a/e2e-tests/disable-api-if-not-fully-synced-flag.sh +++ b/e2e-tests/disable-api-if-not-fully-synced-flag.sh @@ -67,18 +67,6 @@ 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 { diff --git a/e2e-tests/upgradability.sh b/e2e-tests/upgradability.sh index 67d67b59..dd8f757b 100644 --- a/e2e-tests/upgradability.sh +++ b/e2e-tests/upgradability.sh @@ -26,7 +26,7 @@ ARGUMENT="(record { get_current_fee_percentiles = 0; get_current_fee_percentiles_maximum = 0; send_transaction_base =0; - send_transaction_per_byte = 0; + send_transaction_per_byte = 0; }; syncing = variant { enabled }; api_access = variant { enabled }; diff --git a/interface/src/lib.rs b/interface/src/lib.rs index 506fa150..1e0421c2 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -372,7 +372,7 @@ pub enum GetBlockHeadersError { requested: Height, chain_height: Height, }, - StartHeightLagerThanEndHeight { + StartHeightLargerThanEndHeight { start_height: Height, end_height: Height, }, @@ -401,7 +401,7 @@ impl fmt::Display for GetBlockHeadersError { requested, chain_height ) } - Self::StartHeightLagerThanEndHeight { + Self::StartHeightLargerThanEndHeight { start_height, end_height, } => {