Files
YvvandClaude Opus 5.5 67ddee28b2 v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/
- crates/ld-engine : formule inertielle (parité avec la maquette, garde C),
  coquille / changement de fond, assistant d'édition à règles, composition
  d'un document sous vote permanent (adoptions, aperçus, rivales).
- crates/trustin-access : qui voit, qui ouvre, qui gère les accès.
- crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de
  connect.monnaie-libre.fr), invitations, entrée en vigueur automatique,
  signalements de contradiction, traductions éditoriales, publication git,
  taille de la toile lue sur la chaîne.
- seeds/acte-engagement-certification.json : 33 articles, fr + en + es ;
  en vigueur : préambule, E1, E2 (seed a minima).
- maquette/ : frontend, backend et docker v2 déplacés tels quels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:04:42 +02:00

129 lines
4.4 KiB
Rust

//! W — the size of the Ğ1 web of trust, the electorate of Monnaie libre.
//!
//! Read on chain first (`Membership.CounterForMembership`, JSON-RPC
//! `state_getStorage` on a Duniter v2s node), from an indexer otherwise, and
//! kept in `settings`. A failed refresh keeps the last known value; the very
//! first start falls back to `LD_WOT_FALLBACK`.
use crate::AppState;
use crate::util;
use serde_json::json;
use std::time::Duration;
const REFRESH: Duration = Duration::from_secs(6 * 3600);
const PLAUSIBLE: std::ops::RangeInclusive<u32> = 100..=10_000_000;
fn twox_128(data: &[u8]) -> [u8; 16] {
let mut out = [0u8; 16];
out[..8].copy_from_slice(&twox_hash::XxHash64::oneshot(0, data).to_le_bytes());
out[8..].copy_from_slice(&twox_hash::XxHash64::oneshot(1, data).to_le_bytes());
out
}
/// Storage key of a plain storage value: `twox_128(pallet) ++ twox_128(item)`.
pub fn storage_key(pallet: &str, item: &str) -> String {
let mut key = Vec::with_capacity(32);
key.extend_from_slice(&twox_128(pallet.as_bytes()));
key.extend_from_slice(&twox_128(item.as_bytes()));
format!("0x{}", hex::encode(key))
}
/// SCALE `u32`, little endian.
pub fn decode_u32(hex_value: &str) -> Option<u32> {
let bytes = hex::decode(hex_value.trim_start_matches("0x")).ok()?;
let arr: [u8; 4] = bytes.get(..4)?.try_into().ok()?;
Some(u32::from_le_bytes(arr))
}
async fn from_chain(state: &AppState) -> Option<(u32, String)> {
let key = storage_key("Membership", "CounterForMembership");
for node in &state.cfg.rpc_endpoints {
let body = json!({ "jsonrpc": "2.0", "id": 1, "method": "state_getStorage", "params": [key] });
let res = state.http.post(node).json(&body).send().await;
let value: Option<serde_json::Value> = match res {
Ok(r) if r.status().is_success() => r.json().await.ok(),
_ => None,
};
if let Some(n) = value.as_ref().and_then(|v| v["result"].as_str()).and_then(decode_u32)
&& PLAUSIBLE.contains(&n)
{
return Some((n, format!("chain:{node}")));
}
}
None
}
async fn from_indexer(state: &AppState) -> Option<(u32, String)> {
let query = json!({ "query": "{ identities(filter:{isMember:{equalTo:true}}) { totalCount } }" });
for url in &state.cfg.indexers {
let res = state.http.post(url).json(&query).send().await;
let value: Option<serde_json::Value> = match res {
Ok(r) if r.status().is_success() => r.json().await.ok(),
_ => None,
};
let n = value
.as_ref()
.and_then(|v| v["data"]["identities"]["totalCount"].as_u64())
.and_then(|n| u32::try_from(n).ok());
if let Some(n) = n.filter(|n| PLAUSIBLE.contains(n)) {
return Some((n, format!("indexer:{url}")));
}
}
None
}
pub async fn refresh(state: &AppState) -> anyhow::Result<Option<u32>> {
let found = match from_chain(state).await {
Some(v) => Some(v),
None => from_indexer(state).await,
};
let Some((n, source)) = found else {
tracing::warn!("web of trust size: no node nor indexer answered, keeping the last value");
return Ok(None);
};
let now = util::now();
for (key, value) in [("wot_size", n.to_string()), ("wot_source", source.clone())] {
sqlx::query(
"INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
)
.bind(key)
.bind(value)
.bind(&now)
.execute(&state.db)
.await?;
}
tracing::info!(size = n, %source, "web of trust size");
Ok(Some(n))
}
pub async fn refresher(state: AppState) {
let mut tick = tokio::time::interval(REFRESH);
loop {
tick.tick().await;
if let Err(e) = refresh(&state).await {
tracing::warn!(error = %e, "web of trust refresh");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn storage_keys_match_substrate() {
// Well-known key of System.Number (block number) on any Substrate chain.
assert_eq!(
storage_key("System", "Number"),
"0x26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac"
);
}
#[test]
fn scale_u32() {
assert_eq!(decode_u32("0x71160000"), Some(5745));
assert_eq!(decode_u32("0x01"), None);
}
}