forked from yvv/decision
- 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>
129 lines
4.4 KiB
Rust
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);
|
|
}
|
|
}
|