Files
decision/crates/ld-engine/src/address.rs
T
YvvandClaude Opus 5.5 e63e2ec283 v3 : vote par la chaîne, corrections de la relecture de 74f96be (serveur)
- adresse : refusée au-delà de 64 caractères avant tout décodage (le base58
  coûte le carré de la longueur : 80 000 caractères prenaient 38 s) ;
- tâche de fond : 50 bulletins par passe, le moins récemment lu d'abord ;
  lu une dernière fois à son échéance, une heure de grâce si l'indexeur se tait ;
- plafond de 20 par identité : en attente, plus lâchés depuis une heure ;
  purge des lâchés après un jour, des expirés après une semaine ;
- bulletin Ğecko : seul un commentaire payé le tranche, une copie nue s'ignore ;
- finalité : seul un bloc ≤ squidStatus.height compte ; recherche filtrée
  (auteur lié, ou virement vers le compte des votes) et paginée par 50 ;
- « ton vote » : un bulletin en attente plus ancien qu'un vote confirmé se tait ;
- reçu (GET /api/ballots/{id}/receipt) et historique (GET /api/ballots),
  pour qui l'a demandé seulement ;
- un nom vu seulement par un vote sur la chaîne reste libre pour un lien d'équipe ;
- totaux affichés isolés (shown_tally) pour une publication par paliers.

Les sondes de la relecture deviennent des tests, vus rouges avant le correctif.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:14:54 +01:00

186 lines
6.6 KiB
Rust

//! Ğ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<Vec<u8>> {
// Big-endian bytes of the number, grown symbol by symbol.
let mut number: Vec<u8> = 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<u8> = 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<u8> {
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<String> {
decode(addr).map(|(_, key)| encode(G1_PREFIX, &key))
}
#[cfg(test)]
mod tests {
use super::*;
fn hex32(s: &str) -> [u8; 32] {
let bytes: Vec<u8> = (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}");
}
}
}