@@ -5,6 +5,7 @@ use std::collections::HashMap;
55use std:: sync:: Arc ;
66use std:: time:: Duration ;
77
8+ use anyhow:: Context ;
89use anyhow:: Result ;
910use kyoto:: FeeRate ;
1011use 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
115120impl 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