openmina_core/chain_id.rs
1//! Chain identifier and network discrimination for Mina Protocol.
2//!
3//! This module provides the [`ChainId`] type, which uniquely identifies
4//! different Mina blockchain networks (Mainnet, Devnet, etc.) and ensures peers
5//! only connect to compatible networks. The chain ID is computed from protocol
6//! parameters, genesis state, and constraint system digests to create a
7//! deterministic network identifier.
8//!
9//! ## Purpose
10//!
11//! Chain IDs serve multiple critical functions in the Mina protocol:
12//!
13//! - **Network Isolation**: Prevents nodes from different networks (e.g.,
14//! mainnet vs devnet) from connecting to each other
15//! - **Protocol Compatibility**: Ensures all peers use the same protocol
16//! parameters
17//! - **Security**: Used in cryptographic operations and peer authentication
18//! - **Private Network Support**: Enables creation of isolated test networks
19//!
20//! ## Chain ID Computation
21//!
22//! The chain ID is a 32-byte Blake2b hash computed from:
23//!
24//! - **Genesis State Hash**: The hash of the initial blockchain state
25//! - **Constraint System Digests**: Hashes of the SNARK constraint systems
26//! - **Genesis Constants**: Protocol parameters like slot timing and consensus
27//! settings
28//! - **Protocol Versions**: Transaction and network protocol version numbers
29//! - **Transaction Pool Size**: Maximum transaction pool configuration
30//!
31//! This ensures that any change to fundamental protocol parameters results in a
32//! different chain ID, preventing incompatible nodes from connecting.
33//!
34//! ## Network Identifiers
35//!
36//! OpenMina includes predefined chain IDs for official networks:
37//!
38//! - [`MAINNET_CHAIN_ID`]: The production Mina blockchain
39//! - [`DEVNET_CHAIN_ID`]: The development/testing blockchain
40//!
41//! Custom networks can compute their own chain IDs using [`ChainId::compute()`].
42//!
43//! ## Usage in Networking
44//!
45//! Chain IDs are used throughout OpenMina's networking stack:
46//!
47//! - **Peer Discovery**: Nodes advertise their chain ID to find compatible
48//! peers
49//! - **Connection Authentication**: WebRTC and libp2p connections verify chain
50//! ID compatibility
51//! - **Private Networks**: The [`preshared_key()`](ChainId::preshared_key)
52//! method generates cryptographic keys for private network isolation
53//!
54//! ## Example
55//!
56//! ```rust
57//! use openmina_core::ChainId;
58//!
59//! // Use predefined network
60//! let mainnet_id = openmina_core::MAINNET_CHAIN_ID;
61//! println!("Mainnet ID: {}", mainnet_id);
62//!
63//! // Parse from hex string
64//! let chain_id = ChainId::from_hex("a7351abc7ddf2ea92d1b38cc8e636c271c1dfd2c081c637f62ebc2af34eb7cc1")?;
65//!
66//! // Generate preshared key for private networking
67//! let psk = chain_id.preshared_key();
68//! ```
69
70use mina_p2p_messages::v2::{
71 MinaBaseProtocolConstantsCheckedValueStableV1, StateHash, UnsignedExtendedUInt32StableV1,
72};
73use multihash::{Blake2b256, Hasher};
74use time::{macros::format_description, OffsetDateTime};
75
76use std::{
77 fmt::{self, Debug, Display, Formatter},
78 io::{Read, Write},
79};
80
81use binprot::{BinProtRead, BinProtWrite};
82use serde::{Deserialize, Deserializer, Serialize, Serializer};
83
84/// Unique identifier for a Mina blockchain network.
85///
86/// `ChainId` is a 32-byte cryptographic hash that uniquely identifies a
87/// specific Mina blockchain network. It ensures network isolation by preventing
88/// nodes from different chains (mainnet, devnet, custom testnets) from
89/// connecting to each other.
90///
91/// ## Security Properties
92///
93/// The chain ID provides several security guarantees:
94///
95/// - **Deterministic**: Always produces the same ID for identical protocol
96/// parameters
97/// - **Collision Resistant**: Uses Blake2b hashing to prevent ID conflicts
98/// - **Tamper Evident**: Any change to protocol parameters changes the chain ID
99/// - **Network Isolation**: Incompatible networks cannot connect accidentally
100///
101/// ## Computation Method
102///
103/// Chain IDs are computed using [`ChainId::compute()`] from these inputs:
104///
105/// 1. **Constraint System Digests**: MD5 hashes of SNARK constraint systems
106/// 2. **Genesis State Hash**: Hash of the initial blockchain state
107/// 3. **Genesis Constants**: Protocol timing and consensus parameters
108/// 4. **Protocol Versions**: Transaction and network protocol versions
109/// 5. **Transaction Pool Size**: Maximum mempool configuration
110///
111/// The computation uses Blake2b-256 to hash these components in a specific
112/// order, ensuring reproducible results across different implementations.
113///
114/// ## Network Usage
115///
116/// Chain IDs are used throughout the networking stack:
117///
118/// - **Peer Discovery**: Nodes broadcast their chain ID during discovery
119/// - **Connection Handshakes**: WebRTC offers include chain ID for validation
120/// - **Private Networks**: [`preshared_key()`](Self::preshared_key) generates
121/// libp2p private network keys
122/// - **Protocol Compatibility**: Ensures all peers use compatible protocol
123/// versions
124///
125/// ## Serialization Formats
126///
127/// Chain IDs support multiple serialization formats:
128///
129/// - **Hex String**: Human-readable format for configuration files
130/// - **Binary**: 32-byte array for network transmission
131/// - **JSON**: String representation for APIs and debugging
132///
133/// ## Example Usage
134///
135/// ```rust
136/// use openmina_core::{ChainId, MAINNET_CHAIN_ID};
137///
138/// // Use predefined mainnet ID
139/// let mainnet = MAINNET_CHAIN_ID;
140/// println!("Mainnet: {}", mainnet.to_hex());
141///
142/// // Parse from configuration
143/// let custom_id = ChainId::from_hex("29936104443aaf264a7f0192ac64b1c7173198c1ed404c1bcff5e562e05eb7f6")?;
144///
145/// // Generate private network key
146/// let psk = mainnet.preshared_key();
147/// ```
148#[derive(Clone, PartialEq, Eq)]
149pub struct ChainId([u8; 32]);
150
151fn md5_hash(data: u8) -> String {
152 let mut hasher = md5::Context::new();
153 hasher.consume(data.to_string().as_bytes());
154 let hash: Md5 = *hasher.compute();
155 hex::encode(hash)
156}
157
158type Md5 = [u8; 16];
159
160fn hash_genesis_constants(
161 constants: &MinaBaseProtocolConstantsCheckedValueStableV1,
162 tx_pool_max_size: &UnsignedExtendedUInt32StableV1,
163) -> [u8; 32] {
164 let mut hasher = Blake2b256::default();
165 let genesis_timestamp = OffsetDateTime::from_unix_timestamp_nanos(
166 (constants.genesis_state_timestamp.as_u64() * 1000000) as i128,
167 )
168 .unwrap();
169 let time_format =
170 format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:6]Z");
171 hasher.update(constants.k.to_string().as_bytes());
172 hasher.update(constants.slots_per_epoch.to_string().as_bytes());
173 hasher.update(constants.slots_per_sub_window.to_string().as_bytes());
174 hasher.update(constants.delta.to_string().as_bytes());
175 hasher.update(tx_pool_max_size.to_string().as_bytes());
176 hasher.update(genesis_timestamp.format(&time_format).unwrap().as_bytes());
177 hasher.finalize().try_into().unwrap()
178}
179
180impl ChainId {
181 /// Computes a chain ID from protocol parameters and network configuration.
182 ///
183 /// This method creates a deterministic 32-byte chain identifier by hashing
184 /// all the fundamental parameters that define a Mina blockchain network.
185 /// Any change to these parameters will result in a different chain ID,
186 /// ensuring network isolation and protocol compatibility.
187 ///
188 /// # Parameters
189 ///
190 /// * `constraint_system_digests` - MD5 hashes of the SNARK constraint
191 /// systems used for transaction and block verification
192 /// * `genesis_state_hash` - Hash of the initial blockchain state
193 /// * `genesis_constants` - Protocol constants including timing parameters,
194 /// consensus settings, and economic parameters
195 /// * `protocol_transaction_version` - Version number of the transaction
196 /// protocol
197 /// * `protocol_network_version` - Version number of the network protocol
198 /// * `tx_max_pool_size` - Maximum number of transactions in the mempool
199 ///
200 /// # Returns
201 ///
202 /// A new `ChainId` representing the unique identifier for this network
203 /// configuration.
204 ///
205 /// # Algorithm
206 ///
207 /// The computation process:
208 ///
209 /// 1. Hash all constraint system digests into a combined string
210 /// 2. Hash the genesis constants with transaction pool size
211 /// 3. Create Blake2b-256 hash of:
212 /// - Genesis state hash (as string)
213 /// - Combined constraint system hash
214 /// - Genesis constants hash (as hex)
215 /// - Protocol transaction version (as MD5 hash)
216 /// - Protocol network version (as MD5 hash)
217 ///
218 /// # Example
219 ///
220 /// ```rust
221 /// use openmina_core::ChainId;
222 /// use mina_p2p_messages::v2::UnsignedExtendedUInt32StableV1;
223 ///
224 /// let chain_id = ChainId::compute(
225 /// &constraint_digests,
226 /// &genesis_hash,
227 /// &protocol_constants,
228 /// 1, // transaction version
229 /// 1, // network version
230 /// &UnsignedExtendedUInt32StableV1::from(3000),
231 /// );
232 /// ```
233 pub fn compute(
234 constraint_system_digests: &[Md5],
235 genesis_state_hash: &StateHash,
236 genesis_constants: &MinaBaseProtocolConstantsCheckedValueStableV1,
237 protocol_transaction_version: u8,
238 protocol_network_version: u8,
239 tx_max_pool_size: &UnsignedExtendedUInt32StableV1,
240 ) -> ChainId {
241 let mut hasher = Blake2b256::default();
242 let constraint_system_hash = constraint_system_digests
243 .iter()
244 .map(hex::encode)
245 .reduce(|acc, el| acc + &el)
246 .unwrap_or_default();
247 let genesis_constants_hash = hash_genesis_constants(genesis_constants, tx_max_pool_size);
248 hasher.update(genesis_state_hash.to_string().as_bytes());
249 hasher.update(constraint_system_hash.to_string().as_bytes());
250 hasher.update(hex::encode(genesis_constants_hash).as_bytes());
251 hasher.update(md5_hash(protocol_transaction_version).as_bytes());
252 hasher.update(md5_hash(protocol_network_version).as_bytes());
253 ChainId(hasher.finalize().try_into().unwrap())
254 }
255
256 /// Generates a preshared key for libp2p private networking.
257 ///
258 /// This method creates a cryptographic key used by libp2p's private network
259 /// (Pnet) protocol to ensure only nodes with the same chain ID can connect.
260 /// The preshared key provides an additional layer of network isolation
261 /// beyond basic chain ID validation.
262 ///
263 /// # Algorithm
264 ///
265 /// The preshared key is computed as:
266 /// ```text
267 /// Blake2b-256("/coda/0.0.1/" + chain_id_hex)
268 /// ```
269 ///
270 /// The "/coda/0.0.1/" prefix is a protocol identifier that ensures the
271 /// preshared key is unique to the Mina protocol and not accidentally
272 /// compatible with other systems.
273 ///
274 /// # Returns
275 ///
276 /// A 32-byte array containing the preshared key for this chain ID.
277 ///
278 /// # Usage
279 ///
280 /// This key is used to configure libp2p's private network transport,
281 /// which encrypts all network traffic and prevents unauthorized nodes
282 /// from joining the network even if they know peer addresses.
283 ///
284 /// # Example
285 ///
286 /// ```rust
287 /// use openmina_core::MAINNET_CHAIN_ID;
288 ///
289 /// let psk = MAINNET_CHAIN_ID.preshared_key();
290 /// // Use psk to configure libp2p Pnet transport
291 /// ```
292 pub fn preshared_key(&self) -> [u8; 32] {
293 let mut hasher = Blake2b256::default();
294 hasher.update(b"/coda/0.0.1/");
295 hasher.update(self.to_hex().as_bytes());
296 let hash = hasher.finalize();
297 let mut psk_fixed: [u8; 32] = Default::default();
298 psk_fixed.copy_from_slice(hash.as_ref());
299 psk_fixed
300 }
301
302 /// Converts the chain ID to a hexadecimal string representation.
303 ///
304 /// This method creates a lowercase hex string of the 32-byte chain ID,
305 /// suitable for display, logging, configuration files, and JSON
306 /// serialization.
307 ///
308 /// # Returns
309 ///
310 /// A 64-character hexadecimal string representing the chain ID.
311 ///
312 /// # Example
313 ///
314 /// ```rust
315 /// use openmina_core::MAINNET_CHAIN_ID;
316 ///
317 /// let hex_id = MAINNET_CHAIN_ID.to_hex();
318 /// assert_eq!(hex_id.len(), 64);
319 /// println!("Mainnet ID: {}", hex_id);
320 /// ```
321 pub fn to_hex(&self) -> String {
322 hex::encode(self.0)
323 }
324
325 /// Parses a chain ID from a hexadecimal string.
326 ///
327 /// This method converts a hex string back into a `ChainId` instance.
328 /// The input string must represent exactly 32 bytes (64 hex characters).
329 /// Case-insensitive parsing is supported.
330 ///
331 /// # Parameters
332 ///
333 /// * `s` - A hexadecimal string representing the chain ID
334 ///
335 /// # Returns
336 ///
337 /// * `Ok(ChainId)` if the string is valid 64-character hex
338 /// * `Err(hex::FromHexError)` if the string is invalid or wrong length
339 ///
340 /// # Errors
341 ///
342 /// This method returns an error if:
343 /// - The string contains non-hexadecimal characters
344 /// - The string length is not exactly 64 characters
345 /// - The string represents fewer than 32 bytes
346 ///
347 /// # Example
348 ///
349 /// ```rust
350 /// use openmina_core::ChainId;
351 ///
352 /// let chain_id = ChainId::from_hex(
353 /// "a7351abc7ddf2ea92d1b38cc8e636c271c1dfd2c081c637f62ebc2af34eb7cc1"
354 /// )?;
355 /// ```
356 pub fn from_hex(s: &str) -> Result<ChainId, hex::FromHexError> {
357 let h = hex::decode(s)?;
358 let bs = h[..32]
359 .try_into()
360 .or(Err(hex::FromHexError::InvalidStringLength))?;
361 Ok(ChainId(bs))
362 }
363
364 /// Creates a chain ID from raw bytes.
365 ///
366 /// This method constructs a `ChainId` from a byte slice, taking the first
367 /// 32 bytes as the chain identifier. If the input has fewer than 32 bytes,
368 /// the remaining bytes are zero-padded.
369 ///
370 /// # Parameters
371 ///
372 /// * `bytes` - A byte slice containing at least 32 bytes
373 ///
374 /// # Returns
375 ///
376 /// A new `ChainId` instance created from the input bytes.
377 ///
378 /// # Panics
379 ///
380 /// This method will panic if the input slice has fewer than 32 bytes.
381 ///
382 /// # Example
383 ///
384 /// ```rust
385 /// use openmina_core::ChainId;
386 ///
387 /// let bytes = [0u8; 32]; // All zeros for testing
388 /// let chain_id = ChainId::from_bytes(&bytes);
389 /// ```
390 pub fn from_bytes(bytes: &[u8]) -> ChainId {
391 let mut arr = [0u8; 32];
392 arr.copy_from_slice(&bytes[..32]);
393 ChainId(arr)
394 }
395}
396
397impl BinProtWrite for ChainId {
398 fn binprot_write<W: Write>(&self, w: &mut W) -> std::io::Result<()> {
399 w.write_all(&self.0)
400 }
401}
402
403impl BinProtRead for ChainId {
404 fn binprot_read<R: Read + ?Sized>(r: &mut R) -> Result<Self, binprot::Error>
405 where
406 Self: Sized,
407 {
408 let mut bytes = [0; 32];
409 r.read_exact(&mut bytes)?;
410 Ok(Self(bytes))
411 }
412}
413
414impl Serialize for ChainId {
415 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
416 serializer.serialize_str(&self.to_hex())
417 }
418}
419
420impl<'de> Deserialize<'de> for ChainId {
421 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
422 let s = String::deserialize(deserializer)?;
423 ChainId::from_hex(&s).map_err(serde::de::Error::custom)
424 }
425}
426
427impl AsRef<[u8]> for ChainId {
428 fn as_ref(&self) -> &[u8] {
429 &self.0
430 }
431}
432
433impl Display for ChainId {
434 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
435 write!(f, "{}", self.to_hex())?;
436 Ok(())
437 }
438}
439
440impl Debug for ChainId {
441 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
442 write!(f, "ChainId({})", self)
443 }
444}
445
446/// Chain ID for the Mina development network (Devnet).
447///
448/// This is the official chain identifier for Mina's development and testing
449/// network.
450/// Devnet is used for:
451///
452/// - Protocol development and testing
453/// - New feature validation before mainnet deployment
454/// - Developer experimentation and testing
455/// - Stress testing and performance evaluation
456///
457/// The devnet chain ID ensures that devnet nodes cannot accidentally connect to
458/// mainnet, providing network isolation for development activities.
459///
460/// # Hex Representation
461///
462/// `29936104443aaf264a7f0192ac64b1c7173198c1ed404c1bcff5e562e05eb7f6`
463///
464/// # Usage
465///
466/// ```rust
467/// use openmina_core::DEVNET_CHAIN_ID;
468///
469/// println!("Devnet ID: {}", DEVNET_CHAIN_ID.to_hex());
470/// let psk = DEVNET_CHAIN_ID.preshared_key();
471/// ```
472pub const DEVNET_CHAIN_ID: ChainId = ChainId([
473 0x29, 0x93, 0x61, 0x04, 0x44, 0x3a, 0xaf, 0x26, 0x4a, 0x7f, 0x01, 0x92, 0xac, 0x64, 0xb1, 0xc7,
474 0x17, 0x31, 0x98, 0xc1, 0xed, 0x40, 0x4c, 0x1b, 0xcf, 0xf5, 0xe5, 0x62, 0xe0, 0x5e, 0xb7, 0xf6,
475]);
476
477/// Chain ID for the Mina production network (Mainnet).
478///
479/// This is the official chain identifier for Mina's production blockchain
480/// network. Mainnet is the live network where real MINA tokens are transacted
481/// and the blockchain consensus operates for production use.
482///
483/// Key characteristics:
484///
485/// - **Production Ready**: Used for real-world transactions and value transfer
486/// - **Consensus Network**: Participates in the live Mina protocol consensus
487/// - **Economic Security**: Protected by real economic incentives and staking
488/// - **Finality**: Transactions have real-world financial consequences
489///
490/// The mainnet chain ID ensures network isolation from test networks and
491/// prevents accidental cross-network connections that could compromise security.
492///
493/// # Hex Representation
494///
495/// `a7351abc7ddf2ea92d1b38cc8e636c271c1dfd2c081c637f62ebc2af34eb7cc1`
496///
497/// # Usage
498///
499/// ```rust
500/// use openmina_core::MAINNET_CHAIN_ID;
501///
502/// println!("Mainnet ID: {}", MAINNET_CHAIN_ID.to_hex());
503/// let psk = MAINNET_CHAIN_ID.preshared_key();
504/// ```
505pub const MAINNET_CHAIN_ID: ChainId = ChainId([
506 0xa7, 0x35, 0x1a, 0xbc, 0x7d, 0xdf, 0x2e, 0xa9, 0x2d, 0x1b, 0x38, 0xcc, 0x8e, 0x63, 0x6c, 0x27,
507 0x1c, 0x1d, 0xfd, 0x2c, 0x08, 0x1c, 0x63, 0x7f, 0x62, 0xeb, 0xc2, 0xaf, 0x34, 0xeb, 0x7c, 0xc1,
508]);
509
510#[cfg(test)]
511mod test {
512 use time::format_description::well_known::Rfc3339;
513
514 use super::*;
515 use crate::constants::*;
516
517 #[test]
518 fn test_devnet_chain_id() {
519 // First block after fork: https://devnet.minaexplorer.com/block/3NL93SipJfAMNDBRfQ8Uo8LPovC74mnJZfZYB5SK7mTtkL72dsPx
520 let genesis_state_hash = "3NL93SipJfAMNDBRfQ8Uo8LPovC74mnJZfZYB5SK7mTtkL72dsPx"
521 .parse()
522 .unwrap();
523
524 let mut protocol_constants = PROTOCOL_CONSTANTS.clone();
525 protocol_constants.genesis_state_timestamp =
526 OffsetDateTime::parse("2024-04-09T21:00:00Z", &Rfc3339)
527 .unwrap()
528 .into();
529
530 // Compute the chain id for the Devnet network and compare it the real one.
531 let chain_id = ChainId::compute(
532 crate::network::devnet::CONSTRAINT_SYSTEM_DIGESTS.as_slice(),
533 &genesis_state_hash,
534 &protocol_constants,
535 PROTOCOL_TRANSACTION_VERSION,
536 PROTOCOL_NETWORK_VERSION,
537 &UnsignedExtendedUInt32StableV1::from(TX_POOL_MAX_SIZE),
538 );
539 assert_eq!(chain_id, DEVNET_CHAIN_ID);
540 }
541
542 #[test]
543 fn test_mainnet_chain_id() {
544 // First block after fork: https://www.minaexplorer.com/block/3NK4BpDSekaqsG6tx8Nse2zJchRft2JpnbvMiog55WCr5xJZaKeP
545 let genesis_state_hash = "3NK4BpDSekaqsG6tx8Nse2zJchRft2JpnbvMiog55WCr5xJZaKeP"
546 .parse()
547 .unwrap();
548
549 let mut protocol_constants = PROTOCOL_CONSTANTS.clone();
550 protocol_constants.genesis_state_timestamp =
551 OffsetDateTime::parse("2024-06-05T00:00:00Z", &Rfc3339)
552 .unwrap()
553 .into();
554
555 // Compute the chain id for the Mainnet network and compare it the real one.
556 let chain_id = ChainId::compute(
557 crate::network::mainnet::CONSTRAINT_SYSTEM_DIGESTS.as_slice(),
558 &genesis_state_hash,
559 &protocol_constants,
560 PROTOCOL_TRANSACTION_VERSION,
561 PROTOCOL_NETWORK_VERSION,
562 &UnsignedExtendedUInt32StableV1::from(TX_POOL_MAX_SIZE),
563 );
564 assert_eq!(chain_id, MAINNET_CHAIN_ID);
565 }
566
567 #[test]
568 fn test_devnet_chain_id_as_hex() {
569 assert_eq!(
570 DEVNET_CHAIN_ID.to_hex(),
571 "29936104443aaf264a7f0192ac64b1c7173198c1ed404c1bcff5e562e05eb7f6"
572 );
573 }
574
575 #[test]
576 fn test_mainnet_chain_id_as_hex() {
577 assert_eq!(
578 MAINNET_CHAIN_ID.to_hex(),
579 "a7351abc7ddf2ea92d1b38cc8e636c271c1dfd2c081c637f62ebc2af34eb7cc1"
580 );
581 }
582}