vls_frontend/
heartbeat.rs1use bitcoin::secp256k1::{All, PublicKey, Secp256k1};
2use bitcoin::Network;
3use lightning_signer::bitcoin;
4use lightning_signer::node::SignedHeartbeat;
5use log::*;
6use std::sync::Mutex;
7use std::time::SystemTime;
8
9struct State {
10 last_timestamp: u64,
12 last_heartbeat: Option<SignedHeartbeat>,
14}
15
16pub struct HeartbeatMonitor {
17 pubkey: PublicKey,
18 secp: Secp256k1<All>,
19 log_prefix: String,
20 notify_interval: u64,
22 stale_interval: u64,
24 state: Mutex<State>,
25}
26
27#[cfg_attr(test, derive(PartialEq, Debug))]
28enum HeartbeatStatus {
29 Fresh,
31 Stale,
33 Missing,
35 Future,
37}
38
39impl HeartbeatMonitor {
40 pub fn new(network: Network, pubkey: PublicKey, log_prefix: String) -> Self {
41 let (notify_interval, stale_interval) = match network {
42 Network::Bitcoin => (60, 3600),
43 Network::Testnet => (60, 3600),
44 Network::Regtest => (5, 5),
45 Network::Signet => (5, 5),
46 _ => unreachable!(),
47 };
48 Self {
49 pubkey,
50 secp: Secp256k1::new(),
51 log_prefix,
52 notify_interval,
53 stale_interval,
54 state: Mutex::new(State { last_timestamp: 0, last_heartbeat: None }),
55 }
56 }
57
58 pub fn on_heartbeat(&self, heartbeat: SignedHeartbeat) {
59 let ok = heartbeat.verify(&self.pubkey, &self.secp);
60 if ok {
61 let mut state = self.state.lock().unwrap();
62 debug!("{} heartbeat: height {:?}", self.log_prefix, heartbeat.heartbeat.chain_height);
63 state.last_heartbeat = Some(heartbeat);
64 state.last_timestamp = Self::now();
65 } else {
66 error!(
67 "{} heartbeat signature verify failed: {:?} pubkey {}",
68 self.log_prefix, heartbeat, self.pubkey
69 );
70 }
71 }
72
73 pub fn on_tick(&self) {
74 let now = Self::now();
75 let mut state = self.state.lock().unwrap();
76 match status(state.last_heartbeat.as_ref(), now, self.stale_interval) {
77 HeartbeatStatus::Fresh => {}
78 HeartbeatStatus::Stale =>
79 if now > state.last_timestamp + self.notify_interval {
80 error!(
81 "{} heartbeat stale: {:?}",
82 self.log_prefix,
83 state.last_heartbeat.as_ref()
84 );
85 state.last_timestamp = now;
86 },
87 HeartbeatStatus::Missing =>
88 if now > state.last_timestamp + self.notify_interval {
89 error!("{} no heartbeat", self.log_prefix);
90 state.last_timestamp = now;
91 },
92 HeartbeatStatus::Future => {
93 error!(
94 "{} heartbeat timestamp in the future: {:?} now {}",
95 self.log_prefix,
96 state.last_heartbeat.as_ref(),
97 now
98 );
99 }
100 }
101 }
102
103 fn now() -> u64 {
104 SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
105 }
106}
107
108fn status(
109 heartbeat_opt: Option<&SignedHeartbeat>,
110 now: u64,
111 stale_interval: u64,
112) -> HeartbeatStatus {
113 if let Some(heartbeat) = heartbeat_opt.as_ref() {
114 let heartbeat_ts = heartbeat.heartbeat.current_timestamp as u64;
115 if now < heartbeat_ts {
116 HeartbeatStatus::Future
117 } else if now > heartbeat_ts + stale_interval {
118 HeartbeatStatus::Stale
119 } else {
120 HeartbeatStatus::Fresh
121 }
122 } else {
123 HeartbeatStatus::Missing
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use bitcoin::hashes::Hash;
130 use bitcoin::BlockHash;
131 use lightning_signer::bitcoin;
132 use lightning_signer::node::Heartbeat;
133
134 #[test]
135 fn status_test() {
136 let heartbeat = super::SignedHeartbeat {
137 heartbeat: Heartbeat {
138 chain_tip: BlockHash::all_zeros(),
139 chain_height: 0,
140 chain_timestamp: 0,
141 current_timestamp: 1000,
142 },
143 signature: [0; 64].to_vec(),
144 };
145 assert_eq!(super::status(Some(&heartbeat), 999, 100), super::HeartbeatStatus::Future);
146 assert_eq!(super::status(None, 1000, 100), super::HeartbeatStatus::Missing);
147 assert_eq!(super::status(Some(&heartbeat), 1000, 100), super::HeartbeatStatus::Fresh);
148 assert_eq!(super::status(Some(&heartbeat), 1100, 100), super::HeartbeatStatus::Fresh);
149 assert_eq!(super::status(Some(&heartbeat), 1101, 100), super::HeartbeatStatus::Stale);
150 }
151}