//! Ğ1 account addresses (SS58). //! //! The same public key reads `g1…` on the Ğ1 chain (prefix 4450, what the //! indexer and Ğecko show) and `5…` in the generic Substrate form (what //! duniter-connect shows). Accounts are compared by key, never by text. /// The SS58 prefix of the Ğ1 chain. pub const G1_PREFIX: u16 = 4450; /// The longest text read as an address. An SS58 address of a 32-byte key is /// about 48 characters; base58 decoding costs the square of the length, so a /// longer text is refused before it is decoded (review of 74f96be: 250 000 /// characters took 32 s on a thread of the server). pub const MAX_LEN: usize = 64; const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; fn base58_decode(s: &str) -> Option> { // Big-endian bytes of the number, grown symbol by symbol. let mut number: Vec = Vec::new(); for c in s.bytes() { let mut carry = u32::try_from(ALPHABET.iter().position(|a| *a == c)?).ok()?; for b in number.iter_mut().rev() { carry += u32::from(*b) * 58; *b = (carry & 0xff) as u8; carry >>= 8; } while carry > 0 { number.insert(0, (carry & 0xff) as u8); carry >>= 8; } } let mut out = vec![0u8; s.bytes().take_while(|c| *c == b'1').count()]; out.extend(number); Some(out) } fn base58_encode(data: &[u8]) -> String { // Little-endian base-58 symbols. let mut symbols: Vec = Vec::new(); for byte in data { let mut carry = u32::from(*byte); for d in symbols.iter_mut() { carry += u32::from(*d) << 8; *d = (carry % 58) as u8; carry /= 58; } while carry > 0 { symbols.push((carry % 58) as u8); carry /= 58; } } let zeros = data.iter().take_while(|b| **b == 0).count(); std::iter::repeat_n('1', zeros).chain(symbols.iter().rev().map(|d| char::from(ALPHABET[usize::from(*d)]))).collect() } /// The two bytes that close an address: blake2b-512 of `SS58PRE`, the prefix and the key. fn checksum(payload: &[u8]) -> [u8; 2] { use blake2::{Blake2b512, Digest}; let mut h = Blake2b512::new(); h.update(b"SS58PRE"); h.update(payload); let out = h.finalize(); [out[0], out[1]] } fn prefix_bytes(prefix: u16) -> Vec { if prefix < 64 { vec![prefix as u8] } else { // Two bytes for the prefixes 64 to 16383, as Substrate writes them. vec![ (((prefix & 0b1111_1100) >> 2) as u8) | 0b0100_0000, ((prefix >> 8) as u8) | (((prefix & 0b11) as u8) << 6), ] } } /// The prefix and the 32-byte public key of an SS58 address, if it is one. pub fn decode(addr: &str) -> Option<(u16, [u8; 32])> { if addr.len() > MAX_LEN { return None; } let data = base58_decode(addr)?; let (prefix, at) = match *data.first()? { b @ 0..=63 => (u16::from(b), 1), b @ 64..=127 => { let b1 = *data.get(1)?; let lower = ((b & 0b0011_1111) << 2) | (b1 >> 6); let upper = b1 & 0b0011_1111; (u16::from(lower) | (u16::from(upper) << 8), 2) } _ => return None, }; if data.len() != at + 32 + 2 { return None; } let (payload, sum) = data.split_at(at + 32); if checksum(payload) != sum { return None; } Some((prefix, payload[at..].try_into().ok()?)) } /// The SS58 address of a public key under a prefix (up to 16383). pub fn encode(prefix: u16, key: &[u8; 32]) -> String { let mut data = prefix_bytes(prefix & 0x3fff); data.extend_from_slice(key); let sum = checksum(&data); data.extend_from_slice(&sum); base58_encode(&data) } /// The Ğ1 form of an address in any SS58 form. pub fn to_g1(addr: &str) -> Option { decode(addr).map(|(_, key)| encode(G1_PREFIX, &key)) } #[cfg(test)] mod tests { use super::*; fn hex32(s: &str) -> [u8; 32] { let bytes: Vec = (0..64).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect(); bytes.try_into().expect("32 bytes") } // Computed with @polkadot/util-crypto (decodeAddress / encodeAddress), 2026-10-30. const VECTORS: [(&str, &str, &str); 3] = [ ( "g1NrqKgJW9FoFabaYW82zhS9tDWdQcL1DGpivaJdj7GW9M6iv", "aba6b0dcf21a0ddff2e87b52225ddddf06e8c83afb7cd874e7438dd9aa7c2ee8", "5FwmZc7BSxiLyBDCuB6U6oW9sQNmXk5vNRRYEy8qXxo2FAkn", ), ( "g1LQM6MfnjdPTrgz3ubwhwnPdbWUUq2GGQCqRQuGgrke9vRMA", "3efad05b01361fe102ce8bff5e5fd1be7554af684c7e741aaab256cdf950b196", "5DVHLHUU3LJZFGciJf1BM9juFQDqkSLyVoY35ZmoHSw2pZ1s", ), ( "g1Pn1geRZnHcVdWdKkoYwQrAARDTXK1D7H3zuXj7GNayT1thu", "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", ), ]; #[test] fn g1_and_generic_forms_of_the_same_key() { for (g1, key, generic) in VECTORS { assert_eq!(decode(g1), Some((G1_PREFIX, hex32(key))), "{g1}"); assert_eq!(decode(generic), Some((42, hex32(key))), "{generic}"); assert_eq!(encode(G1_PREFIX, &hex32(key)), g1); assert_eq!(encode(42, &hex32(key)), generic); assert_eq!(to_g1(generic).as_deref(), Some(g1)); } } #[test] fn a_long_text_is_refused_before_it_is_decoded() { // Base58 decoding costs the square of the length, on a thread of the server: // 250 000 characters took 32 s (review of 74f96be). An SS58 address is about 48. for (g1, _, generic) in VECTORS { assert!(g1.len() <= 64 && generic.len() <= 64); } let long = "z".repeat(40_000); let t = std::time::Instant::now(); assert_eq!(decode(&long), None); assert_eq!(to_g1(&long), None); assert!(t.elapsed() < std::time::Duration::from_millis(20), "{:?}", t.elapsed()); // One character too many, even of leading ones (zero bytes), is refused. assert_eq!(decode(&format!("{}{}", "1".repeat(65 - VECTORS[0].0.len()), VECTORS[0].0)), None); } #[test] fn what_is_not_an_address() { let (g1, _, _) = VECTORS[0]; // One character changed: the checksum no longer holds. let mut tampered = g1.to_owned(); tampered.replace_range(10..11, if &g1[10..11] == "a" { "b" } else { "a" }); for bad in ["", "g1", "0OIl", &tampered, &g1[..g1.len() - 1], "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQ"] { assert_eq!(decode(bad), None, "{bad}"); } } }