Skip to content

Commit a221357

Browse files
committed
[kyoto-hardening] resolve chain checkpoint from start_height
Kyoto seeds its header / compact-filter sync from a HeaderCheckpoint. We had two hard-coded mainnet activation checkpoints and a genesis fallback for everything else, so signet / testnet pods re-walked ~300k headers on every rebuild. Combined with the supervisor restart loop, that meant each Kyoto bounce cost several minutes of catch-up before deposits could be confirmed again. Resolve the checkpoint once at monitor startup: keep the mainnet activation fast-path, otherwise call bitcoind getblockhash(start_height) and use that as the checkpoint. Store the result on the Monitor so the supervision loop reuses it across rebuilds without re-RPC.
1 parent 4fa2ee2 commit a221357

1 file changed

Lines changed: 132 additions & 14 deletions

File tree

crates/hashi/src/btc_monitor/monitor.rs

Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::collections::HashMap;
55
use std::sync::Arc;
66
use std::time::Duration;
77

8+
use anyhow::Context;
89
use anyhow::Result;
910
use kyoto::FeeRate;
1011
use kyoto::HeaderCheckpoint;
@@ -92,6 +93,10 @@ pub struct Monitor {
9293
bitcoind_rpc: Arc<corepc_client::client_sync::v29::Client>,
9394
client_tx: tokio::sync::mpsc::Sender<MonitorMessage>,
9495
requester: kyoto::Requester,
96+
/// Starting point for Kyoto's header / compact-filter sync. Resolved
97+
/// once at startup and reused on every rebuild so Kyoto resumes from
98+
/// `start_height` rather than re-walking from genesis after a restart.
99+
chain_checkpoint: HeaderCheckpoint,
95100
tip: Option<HeaderCheckpoint>,
96101
block_height_tx: tokio::sync::watch::Sender<u32>,
97102
pending_deposits: Vec<PendingDeposit>,
@@ -113,17 +118,10 @@ where
113118
}
114119

115120
impl Monitor {
116-
fn build_kyoto_node(config: &MonitorConfig) -> (kyoto::Node, kyoto::Client) {
117-
let checkpoint = match config.network {
118-
bitcoin::Network::Bitcoin if config.start_height > 709_631 => {
119-
kyoto::HeaderCheckpoint::taproot_activation()
120-
}
121-
bitcoin::Network::Bitcoin if config.start_height > 481_823 => {
122-
kyoto::HeaderCheckpoint::segwit_activation()
123-
}
124-
network => kyoto::HeaderCheckpoint::from_genesis(network),
125-
};
126-
121+
fn build_kyoto_node(
122+
config: &MonitorConfig,
123+
checkpoint: HeaderCheckpoint,
124+
) -> (kyoto::Node, kyoto::Client) {
127125
let mut builder = kyoto::Builder::new(config.network)
128126
.add_peers(config.trusted_peers.iter().cloned())
129127
// Only connect to the configured trusted peers. Prevents Kyoto from
@@ -140,6 +138,61 @@ impl Monitor {
140138
builder.build()
141139
}
142140

141+
/// Pick a chain-state checkpoint without contacting bitcoind. Returns
142+
/// `None` when the caller needs to resolve a hash via RPC (i.e.
143+
/// non-mainnet with a non-zero `start_height`).
144+
fn builtin_checkpoint(
145+
network: bitcoin::Network,
146+
start_height: u32,
147+
) -> Option<HeaderCheckpoint> {
148+
// Mainnet ships with two well-known activation checkpoints baked into
149+
// Kyoto. Prefer them when applicable — they avoid an RPC roundtrip
150+
// and match the network's deployment schedule.
151+
if network == bitcoin::Network::Bitcoin {
152+
if start_height > 709_631 {
153+
return Some(HeaderCheckpoint::taproot_activation());
154+
}
155+
if start_height > 481_823 {
156+
return Some(HeaderCheckpoint::segwit_activation());
157+
}
158+
}
159+
if start_height == 0 {
160+
return Some(HeaderCheckpoint::from_genesis(network));
161+
}
162+
None
163+
}
164+
165+
/// Resolve the Kyoto chain-state checkpoint for this configuration.
166+
///
167+
/// On mainnet, prefers the built-in activation checkpoints. On other
168+
/// networks, falls through to a bitcoind `getblockhash(start_height)`
169+
/// RPC so Kyoto can resume from `start_height` instead of re-walking
170+
/// every header from genesis on each rebuild.
171+
async fn resolve_chain_checkpoint(
172+
bitcoind_rpc: &Arc<corepc_client::client_sync::v29::Client>,
173+
network: bitcoin::Network,
174+
start_height: u32,
175+
) -> Result<HeaderCheckpoint> {
176+
if let Some(checkpoint) = Self::builtin_checkpoint(network, start_height) {
177+
return Ok(checkpoint);
178+
}
179+
180+
let block_hash = btc_rpc_call(bitcoind_rpc, move |rpc| {
181+
rpc.get_block_hash(start_height as u64)
182+
})
183+
.await
184+
.with_context(|| format!("bitcoind getblockhash({start_height}) failed"))?
185+
.block_hash()
186+
.with_context(|| format!("parsing block hash at height {start_height}"))?;
187+
188+
info!(
189+
height = start_height,
190+
hash = %block_hash,
191+
"Resolved Kyoto chain-state checkpoint from bitcoind",
192+
);
193+
Ok(HeaderCheckpoint::new(start_height, block_hash))
194+
}
195+
143196
/// Run a BTC monitor with the given configuration.
144197
/// Returns the client for interacting with the monitor and a Service for lifecycle management.
145198
pub fn run(config: MonitorConfig, metrics: Arc<Metrics>) -> Result<(MonitorClient, Service)> {
@@ -156,14 +209,24 @@ impl Monitor {
156209
async move {
157210
let bitcoind_rpc = Arc::new(bitcoind_rpc);
158211

159-
// Build initial Kyoto node.
160-
let (kyoto_node, kyoto_client) = Self::build_kyoto_node(&config);
212+
// Resolve the chain-state checkpoint once. The supervision
213+
// loop reuses it on every Kyoto rebuild, so the RPC roundtrip
214+
// is amortized across the lifetime of the monitor.
215+
let chain_checkpoint = Self::resolve_chain_checkpoint(
216+
&bitcoind_rpc,
217+
config.network,
218+
config.start_height,
219+
)
220+
.await?;
221+
222+
let (kyoto_node, kyoto_client) = Self::build_kyoto_node(&config, chain_checkpoint);
161223

162224
let mut monitor = Monitor {
163225
config,
164226
metrics,
165227
bitcoind_rpc,
166228
requester: kyoto_client.requester.clone(),
229+
chain_checkpoint,
167230
client_tx,
168231
tip: None,
169232
block_height_tx,
@@ -251,7 +314,8 @@ impl Monitor {
251314
debug!("Sleeping {delay:?} before rebuilding Kyoto");
252315
tokio::time::sleep(delay).await;
253316

254-
let (new_node, new_client) = Self::build_kyoto_node(&self.config);
317+
let (new_node, new_client) =
318+
Self::build_kyoto_node(&self.config, self.chain_checkpoint);
255319
current_node = new_node;
256320
current_client = new_client;
257321
self.requester = current_client.requester.clone();
@@ -1264,4 +1328,58 @@ mod tests {
12641328
assert!(d <= max, "{d:?} > base + jitter");
12651329
}
12661330
}
1331+
1332+
#[test]
1333+
fn mainnet_above_taproot_uses_taproot_activation() {
1334+
let cp = Monitor::builtin_checkpoint(bitcoin::Network::Bitcoin, 800_000).unwrap();
1335+
assert_eq!(cp, HeaderCheckpoint::taproot_activation());
1336+
}
1337+
1338+
#[test]
1339+
fn mainnet_above_segwit_below_taproot_uses_segwit_activation() {
1340+
let cp = Monitor::builtin_checkpoint(bitcoin::Network::Bitcoin, 600_000).unwrap();
1341+
assert_eq!(cp, HeaderCheckpoint::segwit_activation());
1342+
}
1343+
1344+
#[test]
1345+
fn zero_height_uses_genesis_for_any_network() {
1346+
for net in [
1347+
bitcoin::Network::Bitcoin,
1348+
bitcoin::Network::Signet,
1349+
bitcoin::Network::Testnet4,
1350+
bitcoin::Network::Regtest,
1351+
] {
1352+
let cp = Monitor::builtin_checkpoint(net, 0).unwrap();
1353+
assert_eq!(cp, HeaderCheckpoint::from_genesis(net));
1354+
}
1355+
}
1356+
1357+
#[test]
1358+
fn non_mainnet_nonzero_height_requires_rpc() {
1359+
// For signet / testnet / regtest with a real start_height, the
1360+
// caller must resolve the hash via bitcoind RPC — no built-in
1361+
// checkpoint applies.
1362+
for net in [
1363+
bitcoin::Network::Signet,
1364+
bitcoin::Network::Testnet4,
1365+
bitcoin::Network::Regtest,
1366+
] {
1367+
assert_eq!(Monitor::builtin_checkpoint(net, 297_756), None);
1368+
}
1369+
}
1370+
1371+
#[test]
1372+
fn mainnet_below_segwit_uses_genesis_when_zero_otherwise_rpc() {
1373+
// Pre-segwit mainnet with start_height == 0 → genesis.
1374+
let cp = Monitor::builtin_checkpoint(bitcoin::Network::Bitcoin, 0).unwrap();
1375+
assert_eq!(
1376+
cp,
1377+
HeaderCheckpoint::from_genesis(bitcoin::Network::Bitcoin)
1378+
);
1379+
// Pre-segwit mainnet with a non-zero start_height → no built-in.
1380+
assert_eq!(
1381+
Monitor::builtin_checkpoint(bitcoin::Network::Bitcoin, 400_000),
1382+
None
1383+
);
1384+
}
12671385
}

0 commit comments

Comments
 (0)