Skip to main content

arknet_common/
serialization.rs

1//! Serialization helpers for arknet.
2//!
3//! - **borsh** is the *only* encoding used for on-chain bytes. Canonical,
4//!   deterministic, consensus-critical. Every `struct` on the wire must
5//!   derive [`BorshSerialize`] + [`BorshDeserialize`].
6//! - **JSON** is used for human-facing surfaces: RPC responses, config
7//!   dumps, CLI output.
8//! - **Hex** is used for displaying hashes / keys in logs and error messages.
9//!
10//! [`BorshSerialize`]: borsh::BorshSerialize
11//! [`BorshDeserialize`]: borsh::BorshDeserialize
12
13use borsh::{BorshDeserialize, BorshSerialize};
14use serde::{de::DeserializeOwned, Serialize};
15
16use crate::errors::{CommonError, Result};
17
18// ─── Borsh (consensus-critical) ───────────────────────────────────────────
19
20/// Encode a value to canonical borsh bytes.
21///
22/// # Security
23/// Output of this function is consumed by consensus. Any non-determinism
24/// here would break state agreement across validators.
25pub fn to_borsh<T: BorshSerialize>(value: &T) -> Result<Vec<u8>> {
26    borsh::to_vec(value).map_err(|e| CommonError::Borsh(e.to_string()))
27}
28
29/// Decode a borsh-encoded byte slice.
30pub fn from_borsh<T: BorshDeserialize>(bytes: &[u8]) -> Result<T> {
31    borsh::from_slice(bytes).map_err(|e| CommonError::Borsh(e.to_string()))
32}
33
34// ─── JSON (human-facing) ──────────────────────────────────────────────────
35
36/// Encode a value as JSON.
37pub fn to_json<T: Serialize>(value: &T) -> Result<String> {
38    serde_json::to_string(value).map_err(Into::into)
39}
40
41/// Encode a value as pretty-printed JSON (for CLI output).
42pub fn to_json_pretty<T: Serialize>(value: &T) -> Result<String> {
43    serde_json::to_string_pretty(value).map_err(Into::into)
44}
45
46/// Decode a JSON string.
47pub fn from_json<T: DeserializeOwned>(s: &str) -> Result<T> {
48    serde_json::from_str(s).map_err(Into::into)
49}
50
51// ─── Hex (display-only) ───────────────────────────────────────────────────
52
53/// Encode bytes as lowercase hex with no `0x` prefix.
54pub fn to_hex(bytes: &[u8]) -> String {
55    hex::encode(bytes)
56}
57
58/// Decode hex with or without a `0x` prefix.
59pub fn from_hex(s: &str) -> Result<Vec<u8>> {
60    let s = s.strip_prefix("0x").unwrap_or(s);
61    hex::decode(s).map_err(|e| CommonError::InvalidArgument(e.to_string()))
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::types::{Address, PubKey, Signature, SignatureScheme};
68
69    #[test]
70    fn borsh_roundtrip_primitive() {
71        let v: u128 = 42_000_000_000;
72        let bytes = to_borsh(&v).unwrap();
73        let back: u128 = from_borsh(&bytes).unwrap();
74        assert_eq!(v, back);
75    }
76
77    #[test]
78    fn borsh_roundtrip_address() {
79        let a = Address::new([0xAB; 20]);
80        let bytes = to_borsh(&a).unwrap();
81        let back: Address = from_borsh(&bytes).unwrap();
82        assert_eq!(a, back);
83    }
84
85    #[test]
86    fn borsh_roundtrip_pubkey() {
87        let pk = PubKey::ed25519([0xCD; 32]);
88        let bytes = to_borsh(&pk).unwrap();
89        let back: PubKey = from_borsh(&bytes).unwrap();
90        assert_eq!(pk, back);
91    }
92
93    #[test]
94    fn borsh_is_deterministic() {
95        let sig = Signature::ed25519([0xFF; 64]);
96        let a = to_borsh(&sig).unwrap();
97        let b = to_borsh(&sig).unwrap();
98        assert_eq!(a, b, "borsh output must be bit-identical across calls");
99    }
100
101    #[test]
102    fn borsh_encodes_scheme_byte_first() {
103        // The scheme tag is the first byte of the encoded PubKey.
104        // This invariant is the load-bearing reason for crypto agility:
105        // validators can tell what scheme an old key uses from byte 0.
106        let pk = PubKey::ed25519([0x00; 32]);
107        let bytes = to_borsh(&pk).unwrap();
108        assert_eq!(bytes[0], SignatureScheme::Ed25519 as u8);
109    }
110
111    #[test]
112    fn borsh_rejects_truncated_input() {
113        let pk = PubKey::ed25519([0xAA; 32]);
114        let mut bytes = to_borsh(&pk).unwrap();
115        bytes.truncate(bytes.len() - 1);
116        let res: Result<PubKey> = from_borsh(&bytes);
117        assert!(res.is_err(), "decoding truncated pubkey must fail");
118    }
119
120    #[test]
121    fn json_roundtrip() {
122        let a = Address::new([0x99; 20]);
123        let s = to_json(&a).unwrap();
124        let back: Address = from_json(&s).unwrap();
125        assert_eq!(a, back);
126    }
127
128    #[test]
129    fn json_pretty_differs_from_json() {
130        let a = Address::new([0x01; 20]);
131        let plain = to_json(&a).unwrap();
132        let pretty = to_json_pretty(&a).unwrap();
133        assert_ne!(plain, pretty);
134    }
135
136    #[test]
137    fn hex_roundtrip_with_and_without_prefix() {
138        let bytes = vec![0xde, 0xad, 0xbe, 0xef];
139        assert_eq!(to_hex(&bytes), "deadbeef");
140        assert_eq!(from_hex("deadbeef").unwrap(), bytes);
141        assert_eq!(from_hex("0xdeadbeef").unwrap(), bytes);
142    }
143
144    #[test]
145    fn hex_rejects_garbage() {
146        assert!(from_hex("not hex").is_err());
147    }
148}