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