Skip to main content

sumchain_primitives/
address.rs

1//! Address type for SUM Chain accounts.
2//!
3//! Addresses are derived from Ed25519 public keys using Blake3,
4//! taking the last 20 bytes. Displayed in base58 with a checksum.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9use crate::{PrimitiveError, Result};
10
11/// 20-byte address derived from public key
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord, Serialize, Deserialize)]
13pub struct Address([u8; 20]);
14
15impl Address {
16    /// Size of address in bytes
17    pub const SIZE: usize = 20;
18
19    /// Zero address (for coinbase/system operations)
20    pub const ZERO: Address = Address([0u8; 20]);
21
22    /// Create address from raw bytes
23    pub fn new(bytes: [u8; 20]) -> Self {
24        Address(bytes)
25    }
26
27    /// Create address from a slice
28    pub fn from_slice(slice: &[u8]) -> Result<Self> {
29        if slice.len() != Self::SIZE {
30            return Err(PrimitiveError::InvalidLength {
31                expected: Self::SIZE,
32                got: slice.len(),
33            });
34        }
35        let mut bytes = [0u8; 20];
36        bytes.copy_from_slice(slice);
37        Ok(Address(bytes))
38    }
39
40    /// Derive address from a public key (32 bytes for Ed25519)
41    /// Takes Blake3 hash of pubkey, then last 20 bytes
42    pub fn from_public_key(pubkey: &[u8; 32]) -> Self {
43        let hash = blake3::hash(pubkey);
44        let hash_bytes = hash.as_bytes();
45        let mut bytes = [0u8; 20];
46        bytes.copy_from_slice(&hash_bytes[12..32]); // Last 20 bytes
47        Address(bytes)
48    }
49
50    /// Parse address from base58 string with checksum
51    /// Format: base58(address_bytes + checksum[0..4])
52    pub fn from_base58(s: &str) -> Result<Self> {
53        let decoded = bs58::decode(s)
54            .into_vec()
55            .map_err(|e| PrimitiveError::InvalidBase58(e.to_string()))?;
56
57        if decoded.len() != Self::SIZE + 4 {
58            return Err(PrimitiveError::InvalidLength {
59                expected: Self::SIZE + 4,
60                got: decoded.len(),
61            });
62        }
63
64        let (addr_bytes, checksum) = decoded.split_at(Self::SIZE);
65
66        // Verify checksum: first 4 bytes of Blake3(Blake3(addr_bytes))
67        let hash1 = blake3::hash(addr_bytes);
68        let hash2 = blake3::hash(hash1.as_bytes());
69
70        if &hash2.as_bytes()[0..4] != checksum {
71            return Err(PrimitiveError::InvalidChecksum);
72        }
73
74        Self::from_slice(addr_bytes)
75    }
76
77    /// Convert to base58 string with checksum
78    pub fn to_base58(&self) -> String {
79        let hash1 = blake3::hash(&self.0);
80        let hash2 = blake3::hash(hash1.as_bytes());
81
82        let mut with_checksum = Vec::with_capacity(Self::SIZE + 4);
83        with_checksum.extend_from_slice(&self.0);
84        with_checksum.extend_from_slice(&hash2.as_bytes()[0..4]);
85
86        bs58::encode(with_checksum).into_string()
87    }
88
89    /// Create from hex string
90    pub fn from_hex(s: &str) -> Result<Self> {
91        let s = s.strip_prefix("0x").unwrap_or(s);
92        let bytes = hex::decode(s).map_err(|e| PrimitiveError::InvalidHex(e.to_string()))?;
93        Self::from_slice(&bytes)
94    }
95
96    /// Convert to hex string (with 0x prefix)
97    pub fn to_hex(&self) -> String {
98        format!("0x{}", hex::encode(self.0))
99    }
100
101    /// Get raw bytes
102    pub fn as_bytes(&self) -> &[u8; 20] {
103        &self.0
104    }
105
106    /// Check if this is the zero address
107    pub fn is_zero(&self) -> bool {
108        self.0 == [0u8; 20]
109    }
110}
111
112impl fmt::Debug for Address {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        write!(f, "Address({})", self.to_base58())
115    }
116}
117
118impl fmt::Display for Address {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{}", self.to_base58())
121    }
122}
123
124impl AsRef<[u8]> for Address {
125    fn as_ref(&self) -> &[u8] {
126        &self.0
127    }
128}
129
130impl From<[u8; 20]> for Address {
131    fn from(bytes: [u8; 20]) -> Self {
132        Address(bytes)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_from_public_key() {
142        let pubkey = [0u8; 32];
143        let addr = Address::from_public_key(&pubkey);
144        assert!(!addr.is_zero()); // Hash of zeros is not zero
145    }
146
147    #[test]
148    fn test_base58_roundtrip() {
149        let addr = Address::from_public_key(&[42u8; 32]);
150        let b58 = addr.to_base58();
151        let addr2 = Address::from_base58(&b58).unwrap();
152        assert_eq!(addr, addr2);
153    }
154
155    #[test]
156    fn test_hex_roundtrip() {
157        let addr = Address::from_public_key(&[1u8; 32]);
158        let hex = addr.to_hex();
159        let addr2 = Address::from_hex(&hex).unwrap();
160        assert_eq!(addr, addr2);
161    }
162
163    #[test]
164    fn test_invalid_checksum() {
165        let addr = Address::from_public_key(&[1u8; 32]);
166        let mut b58 = addr.to_base58();
167
168        // Corrupt the string
169        let chars: Vec<char> = b58.chars().collect();
170        let last = chars.last().unwrap();
171        let new_last = if *last == 'a' { 'b' } else { 'a' };
172        b58.pop();
173        b58.push(new_last);
174
175        let result = Address::from_base58(&b58);
176        assert!(result.is_err());
177    }
178
179    #[test]
180    fn test_zero_address() {
181        assert!(Address::ZERO.is_zero());
182    }
183}