Skip to main content

sui_sdk/
digests.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Utilities for working with chain identifier strings.
5//!
6//! A chain identifier can appear in two formats:
7//! - the legacy short form: the first 4 bytes of the digest, hex-encoded, e.g. `35834a8a`. This is
8//!   the format returned by JSON-RPC, the format rendered by `ChainIdentifier`'s `Display`, and the
9//!   only format this tooling ever *writes* — into `Move.toml` and `client.yaml`.
10//! - the full Base58 form: the full genesis checkpoint digest, Base58-encoded (the format returned
11//!   by the gRPC and GraphQL APIs), e.g. `4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S`.
12//!
13//! We never write the Base58 form, but a user may paste it (e.g. copied from a gRPC/GraphQL
14//! response or an explorer) into a `Move.toml` `[environments]` entry, while the CLI environment
15//! and `client.yaml` cache hold the hex short form. Comparisons must therefore treat the two
16//! encodings of the same digest as equal.
17
18use std::str::FromStr;
19
20use sui_types::digests::{ChainIdentifier, CheckpointDigest};
21
22/// The full Base58-encoded genesis checkpoint digest for `chain_id` — the canonical chain
23/// identifier format. (`ChainIdentifier`'s `Display` renders the legacy 4-byte hex short form.)
24pub fn chain_id_base58(chain_id: &ChainIdentifier) -> String {
25    CheckpointDigest::new(*chain_id.as_bytes()).base58_encode()
26}
27
28/// Compare two chain identifier strings, each of which may independently be in the canonical
29/// Base58 form or the legacy hex short form. A short form matches a full form if it encodes the
30/// first 4 bytes of the digest. Strings in neither format (e.g. ad-hoc identifiers used by tests
31/// or other flavors) only match by exact string equality.
32pub fn chain_ids_match(a: &str, b: &str) -> bool {
33    match (decode_chain_id(a), decode_chain_id(b)) {
34        (Some(a), Some(b)) => {
35            let len = a.len().min(b.len());
36            a[..len] == b[..len]
37        }
38        _ => a == b,
39    }
40}
41
42/// Decode a chain identifier string into digest bytes: all 32 bytes for the canonical Base58
43/// form, the first 4 for the legacy hex short form; `None` if the string is in neither format.
44fn decode_chain_id(s: &str) -> Option<Vec<u8>> {
45    if let Ok(digest) = CheckpointDigest::from_str(s) {
46        return Some(digest.inner().to_vec());
47    }
48
49    let hex = s.strip_prefix("0x").unwrap_or(s);
50    if hex.len() == 8 {
51        let bytes: Option<Vec<u8>> = (0..8)
52            .step_by(2)
53            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
54            .collect();
55        return bytes;
56    }
57
58    None
59}
60
61#[cfg(test)]
62mod tests {
63    use sui_types::digests::{
64        MAINNET_CHAIN_IDENTIFIER_BASE58, TESTNET_CHAIN_IDENTIFIER_BASE58,
65        get_mainnet_chain_identifier, get_testnet_chain_identifier,
66    };
67
68    use super::*;
69
70    const MAINNET_SHORT: &str = "35834a8a";
71    const TESTNET_SHORT: &str = "4c78adac";
72
73    #[test]
74    fn base58_form_of_known_chains() {
75        assert_eq!(
76            chain_id_base58(&get_mainnet_chain_identifier()),
77            MAINNET_CHAIN_IDENTIFIER_BASE58
78        );
79        assert_eq!(
80            chain_id_base58(&get_testnet_chain_identifier()),
81            TESTNET_CHAIN_IDENTIFIER_BASE58
82        );
83    }
84
85    #[test]
86    fn full_matches_full() {
87        assert!(chain_ids_match(
88            MAINNET_CHAIN_IDENTIFIER_BASE58,
89            MAINNET_CHAIN_IDENTIFIER_BASE58
90        ));
91        assert!(!chain_ids_match(
92            MAINNET_CHAIN_IDENTIFIER_BASE58,
93            TESTNET_CHAIN_IDENTIFIER_BASE58
94        ));
95    }
96
97    #[test]
98    fn short_matches_full_in_either_order() {
99        assert!(chain_ids_match(
100            MAINNET_SHORT,
101            MAINNET_CHAIN_IDENTIFIER_BASE58
102        ));
103        assert!(chain_ids_match(
104            MAINNET_CHAIN_IDENTIFIER_BASE58,
105            MAINNET_SHORT
106        ));
107        assert!(!chain_ids_match(
108            TESTNET_SHORT,
109            MAINNET_CHAIN_IDENTIFIER_BASE58
110        ));
111    }
112
113    #[test]
114    fn short_matches_short() {
115        assert!(chain_ids_match(MAINNET_SHORT, MAINNET_SHORT));
116        assert!(chain_ids_match(MAINNET_SHORT, "35834A8A"));
117        assert!(chain_ids_match("0x35834a8a", MAINNET_SHORT));
118        assert!(!chain_ids_match(MAINNET_SHORT, TESTNET_SHORT));
119    }
120
121    #[test]
122    fn opaque_ids_match_exactly() {
123        assert!(chain_ids_match("localnet", "localnet"));
124        assert!(!chain_ids_match("localnet", "Localnet"));
125        assert!(!chain_ids_match(
126            "localnet",
127            MAINNET_CHAIN_IDENTIFIER_BASE58
128        ));
129    }
130}