Skip to content

Commit 9fcffc7

Browse files
mihailjianu1IDX GitHub Automation
andauthored
feat: enable testnet4 support in the bitcoin adapter (dfinity#3267)
This involved: - changing the seeds, - upgrading the bitcoin crate (see base branch) - finally, fixing a bug in the header validation, a fix which is required in the bitcoin canister too (and even on the bitcoin crate if we want to use their helpers): For testnet networks, the difficulty temporarily drops to the minimum if no block has been found in 20 minutes. [Block 30239](https://mempool.space/testnet4/block/00000000eaf8e0ea253d833614892aed70c55e5dc4b4d6709dd6420b8284debb) is an example of such block, and it (coincidentally) occurs right before a "difficulty change", once every 2016 blocks. With the previous implementation, this leads to the difficulty to be dropped massively (to the minimum) on the next difficulty batch, because the difficulty of an "epoch" block (in this case block 30240) depends directly on its previous block (block 30239, which had the "temporarily" drop in difficulty). The fix involved making the new difficulty depend on the difficulty of the previous "epoch block", which does not have a drop in difficulty. --------- Co-authored-by: IDX GitHub Automation <[email protected]>
1 parent a973255 commit 9fcffc7

File tree

8 files changed

+219
-182
lines changed

8 files changed

+219
-182
lines changed

Cargo.lock

Lines changed: 132 additions & 166 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ic-os/components/ic/ic-btc-adapter/generate-btc-adapter-config.sh

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,9 @@ if [ "${SOCKS_FILE}" != "" -a -e "${SOCKS_FILE}" ]; then
6969
read_socks_proxy "${SOCKS_FILE}"
7070
fi
7171

72-
BITCOIN_NETWORK='"testnet"'
73-
DNS_SEEDS='"testnet-seed.bitcoin.jonasschnelli.ch",
74-
"seed.tbtc.petertodd.org",
75-
"seed.testnet.bitcoin.sprovoost.nl",
76-
"testnet-seed.bluematt.me"'
72+
BITCOIN_NETWORK='"testnet4"'
73+
DNS_SEEDS='"seed.testnet4.bitcoin.sprovoost.nl",
74+
"seed.testnet4.wiz.biz"'
7775

7876
if [ "$MAINNET" = true ]; then
7977
BITCOIN_NETWORK='"bitcoin"'

rs/bitcoin/adapter/src/config.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,11 @@ pub fn address_limits(network: Network) -> (usize, usize) {
6262
match network {
6363
Network::Bitcoin => (500, 2000),
6464
Network::Testnet => (100, 1000),
65+
//TODO(mihailjianu): revisit these values
66+
Network::Testnet4 => (100, 1000),
6567
Network::Signet => (1, 1),
6668
Network::Regtest => (1, 1),
67-
other => unreachable!("Unsupported network: {:?}", other),
69+
_ => (1, 1),
6870
}
6971
}
7072

@@ -74,6 +76,7 @@ impl Config {
7476
match self.network {
7577
Network::Bitcoin => 8333,
7678
Network::Testnet => 18333,
79+
Network::Testnet4 => 48333,
7780
_ => 8333,
7881
}
7982
}

rs/bitcoin/adapter/src/get_successors_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ fn get_next_headers(
237237
fn are_multiple_blocks_allowed(network: Network, anchor_height: BlockHeight) -> bool {
238238
match network {
239239
Network::Bitcoin => anchor_height <= MAINNET_MAX_MULTI_BLOCK_ANCHOR_HEIGHT,
240-
Network::Testnet | Network::Signet | Network::Regtest => true,
240+
Network::Testnet | Network::Signet | Network::Regtest | Network::Testnet4 => true,
241241
other => unreachable!("Unsupported network: {:?}", other),
242242
}
243243
}

rs/bitcoin/validation/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ DEV_DEPENDENCIES = [
1414
"@crate_index//:csv",
1515
"@crate_index//:hex",
1616
"@crate_index//:proptest",
17+
"@crate_index//:rstest",
1718
]
1819

1920
MACRO_DEV_DEPENDENCIES = []

rs/bitcoin/validation/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ hex = { workspace = true }
1515
[dev-dependencies]
1616
csv = "1.1"
1717
proptest = "0.9.4"
18+
rstest = { workspace = true }

rs/bitcoin/validation/src/constants.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,18 @@ const TESTNET: &[(BlockHeight, &str)] = &[
5757
(546, "000000002a936ca763904c3c35fce2f3556c559c0214345d31b1bcebf76acb70")
5858
];
5959

60+
/// Bitcoin testnet checkpoints
61+
#[rustfmt::skip]
62+
const TESTNET4: &[(BlockHeight, &str)] = &[
63+
(64000, "000000000deb369dca3115f66e208733066f44c8cc177edd4b5f86869e6355b5")
64+
];
65+
6066
/// Returns the maximum difficulty target depending on the network
6167
pub fn max_target(network: &Network) -> Target {
6268
match network {
6369
Network::Bitcoin => Target::MAX_ATTAINABLE_MAINNET,
6470
Network::Testnet => Target::MAX_ATTAINABLE_TESTNET,
71+
Network::Testnet4 => Target::MAX_ATTAINABLE_TESTNET,
6572
Network::Regtest => Target::MAX_ATTAINABLE_REGTEST,
6673
Network::Signet => Target::MAX_ATTAINABLE_SIGNET,
6774
&other => unreachable!("Unsupported network: {:?}", other),
@@ -72,7 +79,7 @@ pub fn max_target(network: &Network) -> Target {
7279
/// readjusted in the network after a fixed time interval.
7380
pub fn no_pow_retargeting(network: &Network) -> bool {
7481
match network {
75-
Network::Bitcoin | Network::Testnet | Network::Signet => false,
82+
Network::Bitcoin | Network::Testnet | Network::Signet | Network::Testnet4 => false,
7683
Network::Regtest => true,
7784
&other => unreachable!("Unsupported network: {:?}", other),
7885
}
@@ -83,6 +90,7 @@ pub fn pow_limit_bits(network: &Network) -> CompactTarget {
8390
CompactTarget::from_consensus(match network {
8491
Network::Bitcoin => 0x1d00ffff,
8592
Network::Testnet => 0x1d00ffff,
93+
Network::Testnet4 => 0x1d00ffff,
8694
Network::Regtest => 0x207fffff,
8795
Network::Signet => 0x1e0377ae,
8896
&other => unreachable!("Unsupported network: {:?}", other),
@@ -94,7 +102,7 @@ pub fn checkpoints(network: &Network) -> HashMap<BlockHeight, BlockHash> {
94102
let points = match network {
95103
Network::Bitcoin => BITCOIN,
96104
Network::Testnet => TESTNET,
97-
Network::Testnet4 => &[],
105+
Network::Testnet4 => TESTNET4,
98106
Network::Signet => &[],
99107
Network::Regtest => &[],
100108
_ => &[],
@@ -114,7 +122,7 @@ pub fn latest_checkpoint_height(network: &Network, current_height: BlockHeight)
114122
let points = match network {
115123
Network::Bitcoin => BITCOIN,
116124
Network::Testnet => TESTNET,
117-
Network::Testnet4 => &[],
125+
Network::Testnet4 => TESTNET4,
118126
Network::Signet => &[],
119127
Network::Regtest => &[],
120128
_ => &[],

rs/bitcoin/validation/src/header.rs

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ pub fn validate_header(
9191
if let Err(err) = header.validate_pow(Target::from_compact(compact_target)) {
9292
match err {
9393
ValidationError::BadProofOfWork => println!("bad proof of work"),
94-
ValidationError::BadTarget => println!("bad target"),
94+
ValidationError::BadTarget => println!(
95+
"bad target {:?}, {:?}",
96+
Target::from_compact(compact_target),
97+
header.target()
98+
),
9599
_ => {}
96100
};
97101
return Err(ValidateHeaderError::InvalidPoWForComputedTarget);
@@ -164,7 +168,7 @@ fn get_next_compact_target(
164168
timestamp: u32,
165169
) -> CompactTarget {
166170
match network {
167-
Network::Testnet | Network::Regtest => {
171+
Network::Testnet | Network::Regtest | Network::Testnet4 => {
168172
if (prev_height + 1) % DIFFICULTY_ADJUSTMENT_INTERVAL != 0 {
169173
// This if statements is reached only for Regtest and Testnet networks
170174
// Here is the quote from "https://en.bitcoin.it/wiki/Testnet"
@@ -206,8 +210,9 @@ fn find_next_difficulty_in_chain(
206210
) -> CompactTarget {
207211
// This is the maximum difficulty target for the network
208212
let pow_limit_bits = pow_limit_bits(network);
213+
209214
match network {
210-
Network::Testnet | Network::Regtest => {
215+
Network::Testnet | Network::Regtest | Network::Testnet4 => {
211216
let mut current_header = *prev_header;
212217
let mut current_height = prev_height;
213218
let mut current_hash = current_header.block_hash();
@@ -282,7 +287,20 @@ fn compute_next_difficulty(
282287
let actual_interval =
283288
std::cmp::max((prev_header.time as i64) - (last_adjustment_time as i64), 0) as u64;
284289

285-
CompactTarget::from_next_work_required(prev_header.bits, actual_interval, *network)
290+
//TODO: ideally from_next_work_required works by itself
291+
// On Testnet networks, prev_header.bits could be different than last_adjustment_header.bits
292+
// if prev_header took more than 20 minutes to be created.
293+
// Testnet3 (mistakenly) uses the temporary difficulty drop of prev_header to calculate
294+
// the difficulty of th next epoch; this results in the whole epoch having a very low difficulty,
295+
// and therefore likely blockstorms.
296+
// Testnet4 uses the last_adjustment_header.bits to calculate the next epoch's difficulty, making it
297+
// more stable.
298+
//TODO(mihailjianu): add a test for testnet4.
299+
let previous_difficulty = match network {
300+
Network::Testnet4 => last_adjustment_header.bits,
301+
_ => prev_header.bits,
302+
};
303+
CompactTarget::from_next_work_required(previous_difficulty, actual_interval, *network)
286304
}
287305

288306
#[cfg(test)]
@@ -295,6 +313,8 @@ mod test {
295313
};
296314
use csv::Reader;
297315

316+
use rstest::rstest;
317+
298318
use super::*;
299319
use crate::constants::test::{
300320
MAINNET_HEADER_586656, MAINNET_HEADER_705600, MAINNET_HEADER_705601, MAINNET_HEADER_705602,
@@ -559,9 +579,49 @@ mod test {
559579
}
560580

561581
#[test]
562-
fn test_compute_next_difficulty_for_backdated_blocks() {
582+
fn test_compute_next_difficulty_for_temporary_difficulty_drops_testnet4() {
583+
// Arrange
584+
let network = Network::Testnet4;
585+
let chain_length = DIFFICULTY_ADJUSTMENT_INTERVAL - 1; // To trigger the difficulty adjustment.
586+
let genesis_difficulty = CompactTarget::from_consensus(473956288);
587+
588+
// Create the genesis header and initialize the header store with 2014 blocks
589+
let genesis_header = genesis_header(genesis_difficulty);
590+
let mut store = SimpleHeaderStore::new(genesis_header, 0);
591+
let mut last_header = genesis_header;
592+
for _ in 1..(chain_length - 1) {
593+
let new_header = BlockHeader {
594+
prev_blockhash: last_header.block_hash(),
595+
time: last_header.time + 1,
596+
..last_header
597+
};
598+
store.add(new_header);
599+
last_header = new_header;
600+
}
601+
// Add the last header in the epoch, which has the lowest difficulty, or highest possible target.
602+
// This can happen if the block is created more than 20 minutes after the previous block.
603+
let last_header_in_epoch = BlockHeader {
604+
prev_blockhash: last_header.block_hash(),
605+
time: last_header.time + 1,
606+
bits: max_target(&network).to_compact_lossy(),
607+
..last_header
608+
};
609+
store.add(last_header_in_epoch);
610+
611+
// Act.
612+
let difficulty =
613+
compute_next_difficulty(&network, &store, &last_header_in_epoch, chain_length);
614+
615+
// Assert.
616+
// Note: testnet3 would produce 473956288, as it depends on the previous header's difficulty.
617+
assert_eq!(difficulty, CompactTarget::from_consensus(470810608));
618+
}
619+
620+
#[rstest]
621+
#[case(Network::Testnet)]
622+
#[case(Network::Testnet4)]
623+
fn test_compute_next_difficulty_for_backdated_blocks(#[case] network: Network) {
563624
// Arrange: Set up the test network and parameters
564-
let network = Network::Testnet;
565625
let chain_length = DIFFICULTY_ADJUSTMENT_INTERVAL - 1; // To trigger the difficulty adjustment.
566626
let genesis_difficulty = CompactTarget::from_consensus(486604799);
567627

0 commit comments

Comments
 (0)