1use ledger::FpExt;
2use mina_p2p_messages::bigint::BigInt;
3use mina_signer::Signature;
4use redux::Timestamp;
5use serde::{Deserialize, Serialize};
6
7use super::{
8 RpcNodeStatus, RpcNodeStatusSnarkPool, RpcNodeStatusTransactionPool,
9 RpcNodeStatusTransitionFrontier,
10};
11use crate::{p2p::PeerId, stats::block_producer::BlockProductionAttempt};
12use mina_node_account::{AccountPublicKey, AccountSecretKey};
13
14#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
17#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
18pub struct SignatureJson {
19 pub field: String,
20 pub scalar: String,
21}
22
23impl From<Signature> for SignatureJson {
24 fn from(sig: Signature) -> Self {
25 Self {
26 field: sig.rx.to_decimal(),
27 scalar: sig.s.to_decimal(),
28 }
29 }
30}
31
32impl TryInto<Signature> for SignatureJson {
33 type Error = String;
34
35 fn try_into(self) -> Result<Signature, Self::Error> {
36 let rx = BigInt::from_decimal(&self.field)
37 .map_err(|_| "Failed to parse decimals as BigInt")?
38 .try_into()
39 .map_err(|_| "Failed to convert rx BigInt to field element")?;
40 let s = BigInt::from_decimal(&self.scalar)
41 .map_err(|_| "Failed to parse decimals as BigInt")?
42 .try_into()
43 .map_err(|_| "Failed to convert rx BigInt to field element")?;
44
45 Ok(Signature::new(rx, s))
46 }
47}
48
49#[derive(Serialize, Debug, Clone)]
51#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
52pub struct SignedNodeHeartbeat {
53 pub version: u8,
54 pub payload: String,
56 pub submitter: AccountPublicKey,
57 pub signature: SignatureJson,
58}
59
60impl SignedNodeHeartbeat {
61 pub fn verify_signature(&self) -> bool {
63 use blake2::digest::{Update, VariableOutput};
64 use mina_signer::{CompressedPubKey, PubKey, Signer};
65
66 let signature = match self.signature.clone().try_into() {
67 Ok(sig) => sig,
68 Err(_) => return false,
69 };
70
71 let pk: CompressedPubKey = match self.submitter.clone().try_into() {
72 Ok(pk) => pk,
73 Err(_) => return false,
74 };
75
76 let pk = match PubKey::from_address(&pk.into_address()) {
77 Ok(pk) => pk,
78 Err(_) => return false,
79 };
80
81 let mut hasher = blake2::Blake2bVar::new(32).expect("Invalid Blake2bVar output size");
83 let mut blake2_hash = [0u8; 32];
84 hasher.update(self.payload.as_bytes());
85 hasher.finalize_variable(&mut blake2_hash).unwrap();
86
87 let digest = NodeHeartbeatPayloadDigest(blake2_hash);
88 let mut signer = mina_signer::create_legacy::<NodeHeartbeatPayloadDigest>(
89 mina_signer::NetworkId::TESTNET,
90 );
91
92 signer.verify(&signature, &pk, &digest)
93 }
94}
95
96#[derive(Serialize, Deserialize, Debug, Clone)]
98pub struct NodeHeartbeat {
99 pub status: NodeStatus,
100 pub node_timestamp: Timestamp,
101 pub peer_id: PeerId,
102 pub last_produced_block_info: Option<ProducedBlockInfo>,
104}
105
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
107pub struct ProducedBlockInfo {
108 pub height: u32,
109 pub global_slot: u32,
110 pub hash: String,
111 pub base64_encoded_header: String,
112}
113
114#[derive(Serialize, Deserialize, Debug, Clone)]
115pub struct NodeStatus {
116 pub chain_id: Option<String>,
117 pub transition_frontier: RpcNodeStatusTransitionFrontier,
118 pub peers_count: u32,
119 pub snark_pool: RpcNodeStatusSnarkPool,
120 pub transaction_pool: RpcNodeStatusTransactionPool,
121 pub current_block_production_attempt: Option<BlockProductionAttempt>,
122}
123
124impl From<RpcNodeStatus> for NodeStatus {
125 fn from(status: RpcNodeStatus) -> Self {
126 Self {
127 chain_id: status.chain_id,
128 transition_frontier: status.transition_frontier,
129 peers_count: status.peers.len() as u32,
130 snark_pool: status.snark_pool,
131 transaction_pool: status.transaction_pool,
132 current_block_production_attempt: status.current_block_production_attempt,
133 }
134 }
135}
136
137#[derive(Clone, Debug)]
139pub struct NodeHeartbeatPayloadDigest([u8; 32]);
140
141impl mina_hasher::Hashable for NodeHeartbeatPayloadDigest {
142 type D = mina_signer::NetworkId;
143
144 fn to_roinput(&self) -> mina_hasher::ROInput {
145 let mut hex = [0u8; 64];
146 hex::encode_to_slice(self.0, &mut hex).unwrap();
147
148 for b in hex.iter_mut() {
150 *b = b.reverse_bits();
151 }
152
153 mina_hasher::ROInput::new().append_bytes(&hex)
154 }
155
156 fn domain_string(network_id: Self::D) -> Option<String> {
157 match network_id {
158 Self::D::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX,
159 Self::D::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX,
160 }
161 .to_string()
162 .into()
163 }
164}
165
166impl NodeHeartbeat {
167 const CURRENT_VERSION: u8 = 1;
168
169 fn payload_and_digest(&self) -> (String, NodeHeartbeatPayloadDigest) {
171 use base64::{engine::general_purpose::URL_SAFE, Engine as _};
172 use blake2::{
173 digest::{Update, VariableOutput},
174 Blake2bVar,
175 };
176
177 let payload = serde_json::to_string(self).unwrap();
178 let encoded_payload = URL_SAFE.encode(&payload);
179
180 let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size");
181 let mut blake2_hash = [0u8; 32];
182
183 hasher.update(encoded_payload.as_bytes());
184 hasher.finalize_variable(&mut blake2_hash).unwrap();
185
186 (encoded_payload, NodeHeartbeatPayloadDigest(blake2_hash))
187 }
188
189 pub fn sign(&self, secret_key: &AccountSecretKey) -> SignedNodeHeartbeat {
191 let (payload, digest) = self.payload_and_digest();
192 let submitter = secret_key.public_key();
193
194 let signature = {
195 use mina_signer::{Keypair, Signer};
196 let mut signer = mina_signer::create_legacy::<NodeHeartbeatPayloadDigest>(
197 mina_signer::NetworkId::TESTNET,
198 );
199 let kp = Keypair::from(secret_key.clone());
200
201 let signature = signer.sign(&kp, &digest, false);
202 signature.into()
203 };
204
205 SignedNodeHeartbeat {
206 version: Self::CURRENT_VERSION,
207 payload,
208 submitter,
209 signature,
210 }
211 }
212}
213
214#[cfg(test)]
215pub(crate) mod tests {
216
217 use crate::rpc::{
218 RpcNodeStatusSnarkPool, RpcNodeStatusTransactionPool, RpcNodeStatusTransitionFrontier,
219 RpcNodeStatusTransitionFrontierSync,
220 };
221
222 use super::*;
223 use redux::Timestamp;
224
225 #[test]
226 fn test_heartbeat_signing() {
227 let heartbeat = create_test_heartbeat();
228 let secret_key = AccountSecretKey::deterministic(0);
229 let signed = heartbeat.sign(&secret_key);
230
231 println!("Private key: {}", secret_key);
232 println!("Public key: {}", secret_key.public_key());
233 println!("Payload: {}", signed.payload);
234 println!("Signature: {:?}", signed.signature);
235
236 assert_eq!(&signed.payload, "eyJzdGF0dXMiOnsiY2hhaW5faWQiOm51bGwsInRyYW5zaXRpb25fZnJvbnRpZXIiOnsiYmVzdF90aXAiOm51bGwsInN5bmMiOnsidGltZSI6bnVsbCwic3RhdHVzIjoiU3luY2VkIiwicGhhc2UiOiJSdW5uaW5nIiwidGFyZ2V0IjpudWxsfX0sInBlZXJzX2NvdW50IjoxMCwic25hcmtfcG9vbCI6eyJ0b3RhbF9qb2JzIjowLCJzbmFya3MiOjB9LCJ0cmFuc2FjdGlvbl9wb29sIjp7InRyYW5zYWN0aW9ucyI6MCwidHJhbnNhY3Rpb25zX2Zvcl9wcm9wYWdhdGlvbiI6MCwidHJhbnNhY3Rpb25fY2FuZGlkYXRlcyI6MH0sImN1cnJlbnRfYmxvY2tfcHJvZHVjdGlvbl9hdHRlbXB0IjpudWxsfSwibm9kZV90aW1lc3RhbXAiOjAsInBlZXJfaWQiOiIyYkVnQnJQVHpMOHdvdjJENEt6MzRXVkxDeFI0dUNhcnNCbUhZWFdLUUE1d3ZCUXpkOUgiLCJsYXN0X3Byb2R1Y2VkX2Jsb2NrIjpudWxsfQ==");
237 assert_eq!(
238 &signed.signature.field,
239 "9079786479394174309544438559429014966597223472549276883268325308999016287311"
240 );
241 assert_eq!(
242 &signed.signature.scalar,
243 "23390017492020277578751321763314031415515010579676039556553777274088622112706"
244 );
245 assert!(signed.verify_signature());
246 }
247
248 #[test]
249 fn test_heartbeat_signature_deterministic() {
250 let heartbeat = create_test_heartbeat();
251 let secret_key = AccountSecretKey::deterministic(0);
252
253 let signed1 = heartbeat.sign(&secret_key);
254 let signed2 = heartbeat.sign(&secret_key);
255
256 assert_eq!(signed1.payload, signed2.payload);
257 assert_eq!(signed1.signature, signed2.signature);
258 }
259
260 #[test]
261 fn test_heartbeat_different_keys_different_sigs() {
262 let heartbeat = create_test_heartbeat();
263 let sk1 = AccountSecretKey::deterministic(0);
264 let sk2 = AccountSecretKey::deterministic(1);
265
266 let signed1 = heartbeat.sign(&sk1);
267 let signed2 = heartbeat.sign(&sk2);
268
269 assert_eq!(signed1.payload, signed2.payload);
270 assert_ne!(signed1.signature, signed2.signature);
271 assert_ne!(signed1.submitter, signed2.submitter);
272 }
273
274 fn create_test_heartbeat() -> NodeHeartbeat {
275 NodeHeartbeat {
276 status: NodeStatus {
277 chain_id: None,
278 transition_frontier: RpcNodeStatusTransitionFrontier {
279 best_tip: None,
280 sync: RpcNodeStatusTransitionFrontierSync {
281 time: None,
282 status: "Synced".to_string(),
283 phase: "Running".to_string(),
284 target: None,
285 },
286 },
287 peers_count: 10,
288 snark_pool: RpcNodeStatusSnarkPool::default(),
289 transaction_pool: RpcNodeStatusTransactionPool::default(),
290 current_block_production_attempt: None,
291 },
292 node_timestamp: Timestamp::ZERO,
293 peer_id: "2bEgBrPTzL8wov2D4Kz34WVLCxR4uCarsBmHYXWKQA5wvBQzd9H"
294 .parse()
295 .unwrap(),
296 last_produced_block_info: None,
297 }
298 }
299}