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}