Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ chunk_hashes.txt

*.log
*.csv

unstable_blocks/*
15 changes: 15 additions & 0 deletions canister/candid.did
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,19 @@ type get_block_headers_response = record {
block_headers : vec block_header;
};

type block_data = record {
prev_block_hash: block_hash;
block_hash: block_hash;
children: vec block_hash;
height: nat;
difficulty: nat;
no_difficulty_counter: nat;
};

type unstable_blocks_result = record {
data : vec block_data;
};

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

Expand All @@ -144,4 +157,6 @@ service bitcoin : (init_config) -> {
get_config : () -> (config) query;

set_config : (set_config_request) -> ();

get_unstable_blocks : () -> (unstable_blocks_result) query;
};
33 changes: 33 additions & 0 deletions canister/src/blocktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::fmt;
mod serde;
use std::ops::{Add, Sub};

use ic_btc_interface::BlockData;

/// Represents a non-empty block chain as:
/// * the first block of the chain
/// * the successors to this block (which can be an empty list)
Expand Down Expand Up @@ -317,6 +319,37 @@ impl BlockTree {
res
}

pub fn get_block_data(&self, network: Network, height: u128, mut no_difficulty_counter: u128) -> Vec<BlockData> {
let difficulty = self.root.difficulty(network);
if difficulty <= 1 {
no_difficulty_counter += 1; // Increment if block has no difficulty.
} else {
no_difficulty_counter = 0; // Reset if block has difficulty.
}
let mut res = vec![BlockData {
prev_block_hash: self
.root
.header()
.prev_blockhash
.as_raw_hash()
.as_byte_array()
.to_vec(),
block_hash: self.root.block_hash().to_vec(),
children: self
.children
.iter()
.map(|child| child.root.block_hash().to_vec())
.collect(),
height,
difficulty,
no_difficulty_counter,
}];
for child in self.children.iter() {
res.extend(child.get_block_data(network, height + 1, no_difficulty_counter));
}
res
}

// Returns a `BlockTree` where the hash of the root block matches the provided `block_hash`
// along with its depth if it exists, and `None` otherwise.
pub fn find_mut<'a>(&'a mut self, blockhash: &BlockHash) -> Option<(&'a mut BlockTree, u32)> {
Expand Down
8 changes: 8 additions & 0 deletions canister/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ use std::convert::TryInto;
use std::{cell::RefCell, cmp::max};
use utxo_set::UtxoSet;

use ic_btc_interface::UnstableBlocksResult;

/// The maximum number of blocks the canister can be behind the tip to be considered synced.
const SYNCED_THRESHOLD: u32 = 2;

Expand Down Expand Up @@ -162,6 +164,12 @@ pub fn get_config() -> Config {
})
}

pub fn get_unstable_blocks() -> UnstableBlocksResult {
with_state(|s| UnstableBlocksResult {
data: s.unstable_blocks.get_block_data(),
})
}

pub fn pre_upgrade() {
// Serialize the state.
let mut state_bytes = vec![];
Expand Down
7 changes: 7 additions & 0 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ pub fn get_config() -> Config {
ic_btc_canister::get_config()
}

use ic_btc_interface::UnstableBlocksResult;

#[query]
pub fn get_unstable_blocks() -> UnstableBlocksResult {
ic_btc_canister::get_unstable_blocks()
}

#[update]
fn set_config(request: SetConfigRequest) {
ic_btc_canister::set_config(request)
Expand Down
29 changes: 18 additions & 11 deletions canister/src/unstable_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
mod next_block_headers;
use self::next_block_headers::NextBlockHeaders;

use ic_btc_interface::BlockData;

/// Max allowed depth difference between the two longest branches
/// in the unstable block tree on `Testnet` and `Regtest`.
const MAX_TESTNET_UNSTABLE_DEPTH_DIFFERENCE: Depth = Depth::new(500);
Expand All @@ -23,7 +25,7 @@
///
/// When the number of unstable blocks exceeds this limit, the depth difference
/// drops to the current `stability_threshold` value.
const MAX_UNSTABLE_BLOCKS: usize = 1_500;

Check failure on line 28 in canister/src/unstable_blocks.rs

View workflow job for this annotation

GitHub Actions / clippy

constant `MAX_UNSTABLE_BLOCKS` is never used

error: constant `MAX_UNSTABLE_BLOCKS` is never used --> canister/src/unstable_blocks.rs:28:7 | 28 | const MAX_UNSTABLE_BLOCKS: usize = 1_500; | ^^^^^^^^^^^^^^^^^^^ | = note: `-D dead-code` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(dead_code)]`

/// Returns the maximum allowed depth difference between the two longest
/// branches in the unstable block tree on `Testnet` and `Regtest`
Expand All @@ -39,21 +41,22 @@
///
/// This applies only to test environments and does not affect `Mainnet`.
pub fn testnet_unstable_max_depth_difference(
total_unstable_blocks: usize,
stability_threshold: u32,
_total_unstable_blocks: usize,
_stability_threshold: u32,
) -> Depth {
let max_depth_diff = MAX_TESTNET_UNSTABLE_DEPTH_DIFFERENCE.get() as u32;
let min_depth_diff = stability_threshold.min(max_depth_diff - 1);
// let max_depth_diff = MAX_TESTNET_UNSTABLE_DEPTH_DIFFERENCE.get() as u32;
// let min_depth_diff = stability_threshold.min(max_depth_diff - 1);

if total_unstable_blocks >= MAX_UNSTABLE_BLOCKS {
return Depth::new(min_depth_diff as u64);
}
// if total_unstable_blocks >= MAX_UNSTABLE_BLOCKS {
// return Depth::new(min_depth_diff as u64);
// }

let range = (max_depth_diff - min_depth_diff) as f64;
let ratio = total_unstable_blocks as f64 / MAX_UNSTABLE_BLOCKS as f64;
let interpolated = max_depth_diff as f64 - ratio * range;
// let range = (max_depth_diff - min_depth_diff) as f64;
// let ratio = total_unstable_blocks as f64 / MAX_UNSTABLE_BLOCKS as f64;
// let interpolated = max_depth_diff as f64 - ratio * range;

Depth::new(interpolated.round() as u64)
// Depth::new(interpolated.round() as u64)
Comment on lines +47 to +58
Copy link

Copilot AI May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function 'testnet_unstable_max_depth_difference' contains several commented-out lines which may reduce clarity. Consider removing or clearly documenting the purpose of this debug code once it is no longer needed.

Copilot uses AI. Check for mistakes.
MAX_TESTNET_UNSTABLE_DEPTH_DIFFERENCE
}

/// A data structure for maintaining all unstable blocks.
Expand Down Expand Up @@ -151,6 +154,10 @@
self.tree.difficulty_based_depth(self.network)
}

pub fn get_block_data(&self) -> Vec<BlockData> {
self.tree.get_block_data(self.network, 0, 0)
}

/// Returns depth in BlockTree of Block with given BlockHash.
fn block_depth(&mut self, block_hash: &BlockHash) -> Result<u32, BlockDoesNotExtendTree> {
let (_, depth) = self
Expand Down
15 changes: 15 additions & 0 deletions interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,21 @@ impl Default for Config {
}
}

#[derive(CandidType, Deserialize, Debug)]
pub struct BlockData {
pub prev_block_hash: BlockHash,
pub block_hash: BlockHash,
pub children: Vec<BlockHash>,
pub height: u128,
pub difficulty: u128,
pub no_difficulty_counter: u128,
}

#[derive(CandidType, Deserialize, Debug)]
pub struct UnstableBlocksResult {
pub data: Vec<BlockData>,
}

#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
pub struct Fees {
/// The base fee to charge for all `get_utxos` requests.
Expand Down
16 changes: 16 additions & 0 deletions process_unstable_blocks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash

set -e

# Generate date-time prefix: e.g. 20250326_103012
PREFIX=$(date +"%Y%m%d_%H%M%S")
BASE="./unstable_blocks/${PREFIX}_output"

echo "Fetching unstable blocks to ${BASE}.txt ..."
dfx canister call --network testnet bitcoin_t get_unstable_blocks > "${BASE}.txt"

echo "Parsing ${BASE}.txt to ${BASE}.json ..."
./unstable_blocks.py "${BASE}.txt" "${BASE}.json"

echo "Generating blockchain graph to ${BASE}.png ..."
./visualize_blockchain_graphviz.py "${BASE}.json" "${BASE}.png"
59 changes: 59 additions & 0 deletions unstable_blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
import re
import sys
import json

def parse_blob(blob_str):
"""Convert blob with escaped characters to hex string."""
matches = re.findall(r'\\([0-9a-fA-F]{2})', blob_str)
return ''.join(matches)

def parse_vec_block_data(text):
block_pattern = re.compile(
r'record\s*{\s*height\s*=\s*([\d_]+)\s*:\s*nat;\s*'
r'block_hash\s*=\s*blob\s*"([^"]+)";\s*'
r'difficulty\s*=\s*([\d_]+)\s*:\s*nat;\s*'
r'children\s*=\s*vec\s*{([^}]*)};\s*'
r'no_difficulty_counter\s*=\s*([\d_]+)\s*:\s*nat;\s*'
r'prev_block_hash\s*=\s*blob\s*"([^"]+)";\s*}', re.DOTALL)

blocks = []
for match in block_pattern.finditer(text):
height = int(match.group(1).replace("_", ""))
block_hash = parse_blob(match.group(2))
difficulty = int(match.group(3).replace("_", ""))
children_raw = match.group(4)
children_blobs = re.findall(r'blob\s*"([^"]+)"', children_raw)
children = [parse_blob(b) for b in children_blobs]
no_difficulty_counter = int(match.group(5).replace("_", ""))
prev_block_hash = parse_blob(match.group(6))

blocks.append({
"height": height,
"difficulty": difficulty,
"no_difficulty_counter": no_difficulty_counter,
"prev_block_hash": prev_block_hash,
"block_hash": block_hash,
"children": children,
})
return {"data": blocks}

def main():
if len(sys.argv) != 3:
print("Usage: unstable_blocks.py <input.txt> <output.json>")
sys.exit(1)

with open(sys.argv[1], "r") as f:
candid_text = f.read()

parsed = parse_vec_block_data(candid_text)

with open(sys.argv[2], "w") as f:
json.dump(parsed, f, indent=2)

print(f"Saved parsed output to {sys.argv[2]}")

if __name__ == "__main__":
main()

# dfx canister call --network testnet bitcoin_t get_unstable_blocks > ./unstable_blocks/output.txt
51 changes: 51 additions & 0 deletions visualize_blockchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3

import json
import matplotlib.pyplot as plt
import networkx as nx

def short_hash(hash_hex):
return hash_hex[:6]

def load_blocks(file_path):
with open(file_path, "r") as f:
data = json.load(f)
return data["data"]

def build_graph(blocks):
G = nx.DiGraph()

hash_to_block = {b["block_hash"]: b for b in blocks}

for block in blocks:
label = f'H:{block["height"]} D:{block["difficulty"]} #{short_hash(block["block_hash"])}'
G.add_node(block["block_hash"], label=label)

for block in blocks:
for child_hash in block["children"]:
if child_hash in hash_to_block:
G.add_edge(block["block_hash"], child_hash)

return G

def draw_graph(G):
pos = nx.spring_layout(G, seed=42) # You can try different layouts like graphviz_layout if you have pygraphviz
labels = nx.get_node_attributes(G, 'label')

plt.figure(figsize=(14, 10))
nx.draw_networkx_nodes(G, pos, node_size=800, node_color='lightblue')
nx.draw_networkx_edges(G, pos, arrows=True, arrowstyle='->')
nx.draw_networkx_labels(G, pos, labels, font_size=8)

plt.title("Blockchain Visualization")
plt.axis("off")
plt.tight_layout()
plt.show()

def main():
blocks = load_blocks("./unstable_blocks/output.json")
G = build_graph(blocks)
draw_graph(G)

if __name__ == "__main__":
main()
Loading
Loading