vls_frontend/
heartbeat.rs

1use 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    // seconds since the epoch of the last notification
11    last_timestamp: u64,
12    // the last heartbeat we received
13    last_heartbeat: Option<SignedHeartbeat>,
14}
15
16pub struct HeartbeatMonitor {
17    pubkey: PublicKey,
18    secp: Secp256k1<All>,
19    log_prefix: String,
20    // number of seconds between notifications
21    notify_interval: u64,
22    // number of seconds until a heartbeat is considered stale
23    stale_interval: u64,
24    state: Mutex<State>,
25}
26
27#[cfg_attr(test, derive(PartialEq, Debug))]
28enum HeartbeatStatus {
29    // heartbeat is fresh
30    Fresh,
31    // heartbeat is stale
32    Stale,
33    // heartbeat is missing
34    Missing,
35    // heartbeat timestamp is in the future
36    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}