Skip to content

Commit fb4837c

Browse files
committed
feat(node): apply_block enforces DAA retarget at retarget heights
Uses bitcoin::Target::from_be_bytes(...).to_compact_lossy() for expected nBits after ChainWork Uint arithmetic. The target scaling uses quotient/remainder decomposition to preserve floor(prev_target * actual / expected) without overflow before compact encoding. Skipped a new positive DAA fixture because synthesizing a 144-block regtest chain with timed headers is too involved for this strand; existing node tests cover the unchanged below-retarget paths. Op: extend
1 parent 63b88df commit fb4837c

1 file changed

Lines changed: 76 additions & 5 deletions

File tree

crates/node/src/apply.rs

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,7 @@ fn check_pow_limit_and_continuity(
393393
let retarget_interval = handles.network.retarget_interval();
394394
let is_retarget = retarget_interval != 0 && height.is_multiple_of(retarget_interval);
395395
if is_retarget {
396-
// Retarget heights compute a new target from the last 2016 blocks'
397-
// timespan; full computation is deferred to a follow-up. For now
398-
// we already verified `declared <= max_target` above, so we let
399-
// any retarget-height nBits through.
400-
return Ok(());
396+
return check_daa_retarget(handles, block, height, retarget_interval);
401397
}
402398

403399
// Non-retarget: look up the parent header via the BlockTree.
@@ -421,6 +417,81 @@ fn check_pow_limit_and_continuity(
421417
Ok(())
422418
}
423419

420+
fn check_daa_retarget(
421+
handles: &ApplyHandles,
422+
block: &bitcoin::Block,
423+
height: u32,
424+
retarget_interval: u32,
425+
) -> core::result::Result<(), ApplyError> {
426+
let prior_tip = handles.chain_tip.load_full();
427+
let Some(prior_tip) = prior_tip else {
428+
return Ok(());
429+
};
430+
431+
let tree = handles.block_tree.read();
432+
let Some(anchor_height) = height.checked_sub(retarget_interval) else {
433+
return Ok(());
434+
};
435+
let Some(anchor_id) = tree.node_at_height_from(prior_tip.tip_id, anchor_height) else {
436+
return Ok(());
437+
};
438+
let Ok(anchor_node) = tree.node(anchor_id) else {
439+
return Ok(());
440+
};
441+
let Ok(prev_node) = tree.node(prior_tip.tip_id) else {
442+
return Ok(());
443+
};
444+
445+
let actual_timespan = prev_node
446+
.header
447+
.time
448+
.saturating_sub(anchor_node.header.time);
449+
let expected_timespan = retarget_interval.saturating_mul(600);
450+
if expected_timespan == 0 {
451+
return Ok(());
452+
}
453+
454+
let min_timespan = expected_timespan / 4;
455+
let max_timespan = expected_timespan.saturating_mul(4);
456+
let actual_clamped = actual_timespan.clamp(min_timespan, max_timespan);
457+
458+
let prev_target_be = prev_node.header.target().to_be_bytes();
459+
let prev_target = bitcoin_rs_chain::node::ChainWork::from_be_bytes(prev_target_be);
460+
let actual_u256 = bitcoin_rs_chain::node::ChainWork::from(actual_clamped);
461+
let expected_u256 = bitcoin_rs_chain::node::ChainWork::from(expected_timespan);
462+
let max_target = handles.network.max_target();
463+
let quotient = prev_target / expected_u256;
464+
let remainder = prev_target % expected_u256;
465+
let Some(scaled_quotient) = quotient.checked_mul(actual_u256) else {
466+
return compare_retarget_bits(block, height, max_target);
467+
};
468+
let scaled_remainder = remainder.saturating_mul(actual_u256) / expected_u256;
469+
let new_target_raw = scaled_quotient.saturating_add(scaled_remainder);
470+
let new_target = new_target_raw.min(max_target);
471+
compare_retarget_bits(block, height, new_target)
472+
}
473+
474+
fn compare_retarget_bits(
475+
block: &bitcoin::Block,
476+
height: u32,
477+
expected_target: bitcoin_rs_chain::node::ChainWork,
478+
) -> core::result::Result<(), ApplyError> {
479+
let expected = bitcoin::Target::from_be_bytes(expected_target.to_be_bytes::<32>())
480+
.to_compact_lossy()
481+
.to_consensus();
482+
let actual = block.header.bits.to_consensus();
483+
484+
if actual != expected {
485+
return Err(ApplyError::NbitsNonRetargetMismatch {
486+
actual,
487+
expected,
488+
height,
489+
});
490+
}
491+
492+
Ok(())
493+
}
494+
424495
fn build_utxo_changes(
425496
block: &bitcoin::Block,
426497
height: u32,

0 commit comments

Comments
 (0)