Skip to content

Commit 6b978aa

Browse files
committed
fix: snapshot getblockchaininfo tip fields
1 parent 7654f0d commit 6b978aa

3 files changed

Lines changed: 108 additions & 28 deletions

File tree

CONCEPTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ Bitcoin Core's C++ consensus engine (`libbitcoinkernel`), compiled into `bitcoin
3939
### bitcoinconsensus
4040
Removed historical script verification backend. Previously linked as an extracted C library for non-taproot script checks before being deleted in favor of `bitcoinkernel`. The library lacked complete-prevout and Taproot script-path verification capabilities required for current mainnet script validation (exposed by block 938344 during mainnet IBD).
4141

42+
### Difficulty-1 target
43+
The network-independent reference target used by Bitcoin Core's difficulty
44+
calculation: compact nBits `0x1d00ffff`, rather than the selected network's
45+
PoW limit. Confusing the two makes every network report difficulty `1.0` at
46+
its easiest target. See
47+
`docs/solutions/logic-errors/core-float-parity-is-value-parity-not-json-text-parity.md`.
48+
49+
### Float value/text parity
50+
The distinction between equal IEEE-754 values and equal serialized spellings.
51+
Core's UniValue uses `%.16g`, while the live RPC path's sonic-rs serializer
52+
uses shortest-round-trip formatting, so compatibility means preserving the
53+
value and operation order, not forcing JSON text to match. See
54+
`docs/solutions/logic-errors/core-float-parity-is-value-parity-not-json-text-parity.md`.
55+
4256
### Rust interpreter (portable posture)
4357
The pure-Rust script verification path maintained alongside the bitcoinkernel default. Enabled under `--no-default-features` without C++ build dependencies. Its non-Taproot path is a stub that accepts only a bare `OP_TRUE` spend with an empty scriptSig and witness, so it cannot validate ordinary spends either, and it has no Taproot script-path support. What it does verify is the Taproot key path, in full. It is retained for differential testing and lightweight non-production environments; a mainnet sync stops early on the first real spend.
4458

crates/rpc/src/handlers/chain.rs

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use alloc::sync::Arc;
22
use bitcoin::consensus::encode::deserialize;
33
use bitcoin::hex::{DisplayHex as _, FromHex as _};
44
use core::str::FromStr as _;
5+
use core::{fmt, fmt::Write as _};
56

6-
use bitcoin_rs_chain::NodeStatus;
7+
use bitcoin_rs_chain::{NodeStatus, TipSnapshot};
78
use bitcoin_rs_primitives::Hash256;
89
use bitcoin_rs_pruning::policy::CORE_REORG_SAFETY_MARGIN;
910
use sonic_rs::{JsonContainerTrait as _, JsonValueTrait, Value, json};
@@ -14,21 +15,19 @@ use crate::handlers::{ensure_no_params, optional_bool, params_array, required_st
1415

1516
pub(crate) fn getblockchaininfo(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
1617
ensure_no_params(params)?;
17-
let applied = ctx.applied_height();
18+
let applied_tip = ctx.applied_tip.load_full();
19+
let applied = applied_tip.as_ref().map_or(0, |tip| tip.height);
1820
let headers = ctx.height();
19-
let (difficulty, time, mediantime) =
20-
ctx.applied_tip
21-
.load_full()
22-
.map_or((0.0, 0_u64, 0_u64), |tip| {
23-
let tree = ctx.block_tree.read();
24-
tree.node(tip.tip_id).map_or((0.0, 0, 0), |node| {
25-
(
26-
ctx.difficulty_for_bits(node.header.bits),
27-
u64::from(node.header.time),
28-
u64::from(tree.median_time_past_at(tip.tip_id, 11).unwrap_or(0)),
29-
)
30-
})
31-
});
21+
let (difficulty, time, mediantime) = applied_tip.as_ref().map_or((0.0, 0_u64, 0_u64), |tip| {
22+
let tree = ctx.block_tree.read();
23+
tree.node(tip.tip_id).map_or((0.0, 0, 0), |node| {
24+
(
25+
ctx.difficulty_for_bits(node.header.bits),
26+
u64::from(node.header.time),
27+
u64::from(tree.median_time_past_at(tip.tip_id, 11).unwrap_or(0)),
28+
)
29+
})
30+
});
3231
let verification_progress = if headers > 0 {
3332
f64::from(applied) / f64::from(headers)
3433
} else {
@@ -47,8 +46,13 @@ pub(crate) fn getblockchaininfo(ctx: &Arc<Context>, params: &Value) -> Result<Va
4746
fold_block_records(&blocks, applied, None)
4847
};
4948
let prune_status = ctx.prune_status();
50-
let bestblockhash = ctx.applied_hash().to_string_be();
51-
let chainwork = ctx.chainwork_hex();
49+
let bestblockhash = applied_tip
50+
.as_ref()
51+
.map_or_else(Hash256::default, |tip| tip.hash)
52+
.to_string_be();
53+
let chainwork = applied_tip
54+
.as_deref()
55+
.map_or_else(|| ctx.chainwork_hex(), chainwork_hex);
5256
let mut response = sonic_rs::Object::new();
5357
let _ = response.insert(&"chain", chain);
5458
let _ = response.insert(&"blocks", applied);
@@ -68,6 +72,15 @@ pub(crate) fn getblockchaininfo(ctx: &Arc<Context>, params: &Value) -> Result<Va
6872
let _ = response.insert(&"warnings", "");
6973
Ok(Value::from(response))
7074
}
75+
76+
fn chainwork_hex(tip: &TipSnapshot) -> String {
77+
let bytes: [u8; 32] = tip.chainwork.to_be_bytes();
78+
let mut out = String::with_capacity(bytes.len() * 2);
79+
for byte in bytes {
80+
let _: fmt::Result = write!(&mut out, "{byte:02x}");
81+
}
82+
out
83+
}
7184
pub(crate) fn getdifficulty(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
7285
ensure_no_params(params)?;
7386
let difficulty = {
@@ -1359,6 +1372,52 @@ mod tests {
13591372
);
13601373
}
13611374

1375+
#[test]
1376+
fn getblockchaininfo_uses_one_applied_tip_snapshot() {
1377+
use bitcoin_rs_chain::ChainWork;
1378+
1379+
let ctx = context_with_tip(
1380+
bitcoin_rs_primitives::Network::Regtest,
1381+
0x207f_ffff,
1382+
&[100, 300, 200],
1383+
);
1384+
let Some(applied) = ctx.applied_tip.load_full() else {
1385+
panic!("applied tip missing");
1386+
};
1387+
let applied_chainwork = ChainWork::from_be_bytes([1; 32]);
1388+
ctx.set_applied_tip(TipSnapshot {
1389+
chainwork: applied_chainwork,
1390+
..(*applied).clone()
1391+
});
1392+
ctx.set_chain_tip(TipSnapshot {
1393+
tip_id: applied.tip_id,
1394+
height: 99,
1395+
chainwork: ChainWork::from_be_bytes([2; 32]),
1396+
hash: Hash256::from_le_bytes(&[9; 32]),
1397+
});
1398+
1399+
let result = getblockchaininfo(&ctx, &json!([]))
1400+
.unwrap_or_else(|err| panic!("getblockchaininfo failed: {err}"));
1401+
let expected_hash = applied.hash.to_string_be();
1402+
let expected_chainwork = "01".repeat(32);
1403+
assert_eq!(
1404+
result.get("blocks").and_then(JsonValueTrait::as_u64),
1405+
Some(2)
1406+
);
1407+
assert_eq!(
1408+
result.get("headers").and_then(JsonValueTrait::as_u64),
1409+
Some(99)
1410+
);
1411+
assert_eq!(
1412+
result.get("bestblockhash").and_then(JsonValueTrait::as_str),
1413+
Some(expected_hash.as_str())
1414+
);
1415+
assert_eq!(
1416+
result.get("chainwork").and_then(JsonValueTrait::as_str),
1417+
Some(expected_chainwork.as_str())
1418+
);
1419+
}
1420+
13621421
#[test]
13631422
fn getblockchaininfo_size_on_disk_uses_metadata_body_size() {
13641423
let genesis = genesis_block(bitcoin::Network::Regtest);

docs/solutions/logic-errors/core-float-parity-is-value-parity-not-json-text-parity.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The same difficulty helper also affected `getblockheader`, `getblock`,
3535
The original calculation divided the current target by the network's own
3636
proof-of-work limit. That makes the easiest target on every network report
3737
`1.0`, even though Core defines difficulty as a multiple of the
38-
difficulty-1 target and therefore uses the mainnet difficulty-1 reference
38+
difficulty-1 target and therefore uses that network-independent reference
3939
independently of the selected network.
4040

4141
There is a second compatibility trap after the value is corrected. Core's
@@ -49,21 +49,27 @@ The operation order matters for the final IEEE-754 bit. For
4949
`0x207fffff`, both implementations produce the double whose bits correspond
5050
to `4.6565423739069247e-10`.
5151

52-
Core's RPC layer then formats that double with `%.16g`, while serde emits the
53-
shortest round-trip representation. Consequently, Core prints
54-
`4.656542373906925e-10` and serde prints `4.6565423739069247e-10`; these are
55-
different strings even though the underlying value is the same.
52+
Core's RPC layer then formats that double with `%.16g` through UniValue, while
53+
the production RPC path here serializes its `sonic_rs::Value` with
54+
`sonic_rs::to_string`. sonic-rs delegates finite f64 formatting to its
55+
shortest-round-trip `zmij` formatter. Consequently, Core prints
56+
`4.656542373906925e-10` and sonic-rs prints `4.6565423739069247e-10`; these
57+
are different strings even though the underlying value is the same.
5658

5759
## Fix
5860

5961
Compute difficulty with the Core mantissa ratio and the repeated `256.0`
6062
scaling loop. Guard a zero mantissa and return `0.0` for that impossible but
6163
representable header value rather than dividing by zero.
6264

63-
Assert compatibility at the value level with `f64::to_bits()`, not by
64-
comparing JSON text. Do not add a one-ULP adjustment to make serde's shortest
65-
spelling resemble Core's `%.16g` output: that changes the API value and makes
66-
it differ from Core's value precisely to match Core's formatting.
65+
For direct algorithm tests, where both results are still pre-serialization
66+
doubles, assert compatibility at the value level with `f64::to_bits()`. Once
67+
either result has crossed the wire, Core's `%.16g` rendering can parse back to
68+
an adjacent double; compare with an appropriate tolerance or normalize the
69+
wire representation instead of requiring exact parsed-bit equality. Do not
70+
add a one-ULP adjustment to make sonic-rs's shortest spelling resemble Core's
71+
`%.16g` output: that changes the API value and makes it differ from Core's
72+
value precisely to match Core's formatting.
6773

6874
## Why This Works
6975

@@ -74,8 +80,9 @@ representations of the same IEEE-754 number.
7480

7581
## Prevention
7682

77-
- Compare cross-node floating-point RPC results by exact value or
78-
`f64::to_bits()`, not by serialized decimal text.
83+
- Compare direct cross-node floating-point algorithm results by exact value or
84+
`f64::to_bits()` before serialization. For values parsed from RPC text, use
85+
a documented tolerance or canonicalize the representation before comparing.
7986
- Preserve the reference implementation's floating-point operation order;
8087
algebraically equivalent target division or `powi` can change the last bit.
8188
- Treat a rendering mismatch as a serializer issue. Never change a numeric

0 commit comments

Comments
 (0)