tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-wasm-dist/tests/dist.rs Rust · 338 lines · 12.1 KB History Who touched what raw

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

1//! Certificación headless de la distribución por hash: CAS, integridad,
2//! concesión Ed25519 contra anillo, permisos efectivos, y correr la app
3//! resuelta de punta a punta. Sin red ni GPU.
4
5use agora_core::Keypair;
6use app_bus::Launch;
7use format::{ConcesionCapacidad, Hash, Permisos, PERMISO_GRAFO_ESCRITURA, PERMISO_RED};
8use llimphi_wasm_dist::{
9    bytecode_hash, grant_hash, hash_to_hex, resolve, resolve_from_catalog, resolve_launch,
10    resolve_manifest, verify_grant, verify_integrity, AppManifest, AppRef, BlobSource, Catalog,
11    CatalogEntry, DiskStore, DistError, EventPayload, MapSource, TrustRing, VerifiedAppExt,
12};
13
14/// El mismo wasm que corre el runner Tier 3 — lo distribuimos por hash.
15const COUNTER_WASM: &[u8] = include_bytes!("../../llimphi-wasm-runner/assets/counter.wasm");
16
17const SEED: [u8; 32] = [7; 32];
18
19fn tmpdir(nombre: &str) -> std::path::PathBuf {
20    let dir = std::env::temp_dir().join(format!("llimphi-wasm-dist-{nombre}"));
21    let _ = std::fs::remove_dir_all(&dir);
22    dir
23}
24
25/// Firma una concesión como lo haría `agora-cli wawa grant` (offline).
26fn firmar(kp: &Keypair, bytecode: Hash, permisos: Permisos) -> ConcesionCapacidad {
27    let mensaje = format::mensaje_capacidad(&bytecode, permisos);
28    ConcesionCapacidad {
29        bytecode,
30        permisos,
31        autor: kp.public_key(),
32        firma: kp.sign(&mensaje),
33    }
34}
35
36/// Source en memoria que devuelve bytes arbitrarios — para forzar tampering.
37struct FixedSource(Vec<u8>);
38impl BlobSource for FixedSource {
39    fn fetch(&self, _hash: &Hash) -> Option<Vec<u8>> {
40        Some(self.0.clone())
41    }
42}
43
44#[test]
45fn disk_store_round_trip() {
46    let store = DiskStore::open(tmpdir("rt")).unwrap();
47    let h = store.put(COUNTER_WASM).unwrap();
48    assert_eq!(h, bytecode_hash(COUNTER_WASM), "put devuelve el hash canónico");
49    assert_eq!(store.get(&h).as_deref(), Some(COUNTER_WASM), "get recupera los bytes");
50    assert_eq!(store.fetch(&[0u8; 32]), None, "hash desconocido ⇒ None");
51}
52
53#[test]
54fn integridad_detecta_tampering() {
55    let h = bytecode_hash(COUNTER_WASM);
56    assert!(verify_integrity(COUNTER_WASM, &h));
57
58    let mut roto = COUNTER_WASM.to_vec();
59    roto[100] ^= 0xff;
60    assert!(!verify_integrity(&roto, &h), "un byte cambiado ⇒ falla");
61
62    // Un source que entrega bytes corruptos bajo el hash legítimo es rechazado.
63    let source = FixedSource(roto);
64    let err = resolve(&source, &TrustRing::empty(), &AppRef::pure(h)).unwrap_err();
65    assert_eq!(err, DistError::IntegridadFallo);
66}
67
68#[test]
69fn concesion_valida_da_permisos() {
70    let store = DiskStore::open(tmpdir("grant-ok")).unwrap();
71    let h = store.put(COUNTER_WASM).unwrap();
72    let kp = Keypair::from_seed(SEED);
73    let grant = firmar(&kp, h, PERMISO_RED);
74    let trust = TrustRing::new(vec![kp.public_key()]);
75
76    // La concesión sola verifica y rinde sus permisos.
77    assert_eq!(verify_grant(&grant, &trust), Ok(PERMISO_RED));
78
79    // Resuelta con declarados = RED ⇒ efectivos = RED.
80    let app = AppRef {
81        bytecode: h,
82        declarados: PERMISO_RED,
83        concesion: Some(grant),
84    };
85    let verified = resolve(&store, &trust, &app).unwrap();
86    assert_eq!(verified.permisos, PERMISO_RED);
87    assert_eq!(verified.wasm, COUNTER_WASM);
88}
89
90#[test]
91fn concesion_autor_no_confiable() {
92    let kp = Keypair::from_seed(SEED);
93    let grant = firmar(&kp, bytecode_hash(COUNTER_WASM), PERMISO_RED);
94    // Anillo vacío: el autor no está → rechazo antes de gastar cripto.
95    assert_eq!(verify_grant(&grant, &TrustRing::empty()), Err(DistError::AutorNoConfiable));
96}
97
98#[test]
99fn concesion_firma_invalida() {
100    let kp = Keypair::from_seed(SEED);
101    let mut grant = firmar(&kp, bytecode_hash(COUNTER_WASM), PERMISO_RED);
102    grant.firma[0] ^= 0xff; // corrompe la firma
103    let trust = TrustRing::new(vec![kp.public_key()]);
104    assert_eq!(verify_grant(&grant, &trust), Err(DistError::FirmaInvalida));
105}
106
107#[test]
108fn concesion_para_otro_bytecode() {
109    let store = DiskStore::open(tmpdir("otro-bc")).unwrap();
110    let h = store.put(COUNTER_WASM).unwrap();
111    let kp = Keypair::from_seed(SEED);
112    // Concesión firmada para OTRO bytecode (hash distinto), válida en sí misma.
113    let otro = bytecode_hash(b"otra app distinta");
114    let grant = firmar(&kp, otro, PERMISO_RED);
115    let trust = TrustRing::new(vec![kp.public_key()]);
116
117    let app = AppRef {
118        bytecode: h,
119        declarados: PERMISO_RED,
120        concesion: Some(grant),
121    };
122    assert_eq!(
123        resolve(&store, &trust, &app).unwrap_err(),
124        DistError::ConcesionParaOtroBytecode
125    );
126}
127
128#[test]
129fn permisos_efectivos_son_interseccion() {
130    let store = DiskStore::open(tmpdir("interseccion")).unwrap();
131    let h = store.put(COUNTER_WASM).unwrap();
132    let kp = Keypair::from_seed(SEED);
133    // La concesión otorga RED|GRAFO, pero el manifiesto sólo declara RED.
134    let grant = firmar(&kp, h, PERMISO_RED | PERMISO_GRAFO_ESCRITURA);
135    let trust = TrustRing::new(vec![kp.public_key()]);
136
137    let app = AppRef {
138        bytecode: h,
139        declarados: PERMISO_RED,
140        concesion: Some(grant),
141    };
142    let verified = resolve(&store, &trust, &app).unwrap();
143    // efectivos = declarados & concedidos = RED. No escala a GRAFO.
144    assert_eq!(verified.permisos, PERMISO_RED);
145}
146
147#[test]
148fn resolve_launch_y_corre_la_app() {
149    // El camino "abrir app distribuida": el Launch lleva sólo el hash hex.
150    let store = DiskStore::open(tmpdir("e2e")).unwrap();
151    let h = store.put(COUNTER_WASM).unwrap();
152    let launch = Launch::Wasm {
153        bytecode_hex: hash_to_hex(&h),
154        grant_hex: None,
155    };
156
157    let verified = resolve_launch(&store, &TrustRing::empty(), &launch).unwrap();
158    assert_eq!(verified.bytecode, h);
159    assert_eq!(verified.permisos, 0, "UI pura sin concesión ⇒ sin permisos");
160
161    // Y la app resuelta CORRE: cargamos el guest y lo manejamos.
162    let mut guest = verified.load().unwrap();
163    let n0 = guest.view().children[0].text.as_ref().unwrap().content.clone();
164    assert_eq!(n0, "0");
165    guest.dispatch(0, EventPayload::Click).unwrap(); // botón +1 ⇒ Increment
166    let n1 = guest.view().children[0].text.as_ref().unwrap().content.clone();
167    assert_eq!(n1, "1", "la app distribuida por hash incrementa de verdad");
168}
169
170#[test]
171fn resolve_launch_con_concesion_da_permisos() {
172    // El Launch lleva bytecode_hex Y grant_hex: la app de la UI corre con
173    // permisos reales (descubrimiento de concesión desde el lanzamiento).
174    let store = DiskStore::open(tmpdir("launch-grant")).unwrap();
175    let bc = store.put(COUNTER_WASM).unwrap();
176    let kp = Keypair::from_seed(SEED);
177    let grant = firmar(&kp, bc, PERMISO_RED);
178    let gh = store.put_grant(&grant).unwrap();
179
180    let launch = Launch::Wasm {
181        bytecode_hex: hash_to_hex(&bc),
182        grant_hex: Some(hash_to_hex(&gh)),
183    };
184    let trust = TrustRing::new(vec![kp.public_key()]);
185    let verified = resolve_launch(&store, &trust, &launch).unwrap();
186    // declarados = MAX ⇒ efectivos = concedidos = RED.
187    assert_eq!(verified.permisos, PERMISO_RED);
188    assert!(verified.load().is_ok());
189}
190
191#[test]
192fn manifiesto_con_concesion_da_permisos_reales() {
193    // El descubrimiento de concesiones: el manifiesto referencia bytecode Y
194    // concesión por hash; ambos viven en el store; resolve_manifest los trae,
195    // verifica la cadena y rinde permisos reales.
196    let store = DiskStore::open(tmpdir("manifest-grant")).unwrap();
197    let bc = store.put(COUNTER_WASM).unwrap();
198    let kp = Keypair::from_seed(SEED);
199    let grant = firmar(&kp, bc, PERMISO_RED);
200    let gh = store.put_grant(&grant).unwrap();
201    assert_eq!(gh, grant_hash(&grant), "put_grant direcciona por grant_hash");
202
203    let manifest = AppManifest {
204        bytecode: bc,
205        declarados: PERMISO_RED,
206        concesion: Some(gh),
207    };
208    let trust = TrustRing::new(vec![kp.public_key()]);
209
210    let verified = resolve_manifest(&store, &trust, &manifest).unwrap();
211    assert_eq!(verified.permisos, PERMISO_RED, "permisos reales, no 0");
212    assert_eq!(verified.wasm, COUNTER_WASM);
213    // Y carga en el runner con ese permiso (que gatea host_net_request).
214    assert!(verified.load().is_ok());
215}
216
217#[test]
218fn manifiesto_concesion_faltante() {
219    // El manifiesto declara una concesión que el source NO tiene.
220    let mut src = MapSource::new();
221    let bc = src.put(COUNTER_WASM);
222    let manifest = AppManifest {
223        bytecode: bc,
224        declarados: PERMISO_RED,
225        concesion: Some([0xab; 32]), // hash que nadie sirve
226    };
227    assert_eq!(
228        resolve_manifest(&src, &TrustRing::empty(), &manifest).unwrap_err(),
229        DistError::ConcesionNoEncontrada
230    );
231}
232
233#[test]
234fn manifiesto_concesion_para_otro_bytecode() {
235    // Concesión válida y servida, pero firmada para OTRO bytecode.
236    let mut src = MapSource::new();
237    let bc = src.put(COUNTER_WASM);
238    let kp = Keypair::from_seed(SEED);
239    let grant = firmar(&kp, bytecode_hash(b"otra app"), PERMISO_RED);
240    let gh = src.put_grant(&grant);
241    let manifest = AppManifest {
242        bytecode: bc,
243        declarados: PERMISO_RED,
244        concesion: Some(gh),
245    };
246    let trust = TrustRing::new(vec![kp.public_key()]);
247    assert_eq!(
248        resolve_manifest(&src, &trust, &manifest).unwrap_err(),
249        DistError::ConcesionParaOtroBytecode
250    );
251}
252
253#[test]
254fn manifiesto_pura_corre_sin_permisos() {
255    let mut src = MapSource::new();
256    let bc = src.put(COUNTER_WASM);
257    let verified =
258        resolve_manifest(&src, &TrustRing::empty(), &AppManifest::pure(bc)).unwrap();
259    assert_eq!(verified.permisos, 0);
260    assert!(verified.load().is_ok());
261}
262
263#[test]
264fn manifiesto_round_trip() {
265    let m = AppManifest {
266        bytecode: [3; 32],
267        declarados: PERMISO_RED,
268        concesion: Some([7; 32]),
269    };
270    assert_eq!(AppManifest::deserializar(&m.serializar()).unwrap(), m);
271}
272
273#[test]
274fn catalogo_busca_y_corre_la_app_elegida() {
275    // El "qué apps hay": un catálogo nombra apps; buscas por texto, eliges una
276    // y se resuelve por su hash + corre. El catálogo mismo viaja como blob (su
277    // hash es su dirección de contenido).
278    let mut src = MapSource::new();
279    let bc = src.put(COUNTER_WASM); // bytecode al CAS
280    let kp = Keypair::from_seed(SEED);
281    let grant = firmar(&kp, bc, PERMISO_RED);
282    let gh = src.put_grant(&grant); // concesión al CAS
283    let trust = TrustRing::new(vec![kp.public_key()]);
284
285    let catalog = Catalog::new(vec![
286        CatalogEntry {
287            id: "counter".into(),
288            name: "Contador".into(),
289            description: "suma y resta en red".into(),
290            category: Some("demo".into()),
291            bytecode: bc,
292            declarados: PERMISO_RED,
293            concesion: Some(gh),
294        },
295        CatalogEntry {
296            id: "otra".into(),
297            name: "Otra".into(),
298            description: "app que no está en el CAS".into(),
299            category: Some("demo".into()),
300            bytecode: [9; 32],
301            declarados: 0,
302            concesion: None,
303        },
304    ]);
305
306    // El catálogo es content-addressed: serializa y rehashea estable.
307    assert_eq!(
308        Catalog::deserializar(&catalog.serializar()).unwrap().hash(),
309        catalog.hash()
310    );
311
312    // Búsqueda por texto encuentra el contador.
313    let hits = catalog.search("suma");
314    assert_eq!(hits.len(), 1);
315    assert_eq!(hits[0].id, "counter");
316
317    // Resolver por id: trae bytecode + concesión, verifica, permisos reales.
318    let verified = resolve_from_catalog(&src, &trust, &catalog, "counter").unwrap();
319    assert_eq!(verified.permisos, PERMISO_RED, "la concesión del catálogo da RED");
320
321    // Y CORRE.
322    let mut guest = verified.load().unwrap();
323    guest.dispatch(0, EventPayload::Click).unwrap();
324    assert_eq!(
325        guest.view().children[0].text.as_ref().unwrap().content,
326        "1",
327        "la app elegida del catálogo incrementa de verdad"
328    );
329
330    // Una entrada cuyo bytecode no está en el CAS no resuelve (pero el catálogo
331    // sí la lista — descubrir ≠ tener).
332    assert!(resolve_from_catalog(&src, &trust, &catalog, "otra").is_err());
333    // Un id ausente del catálogo.
334    assert_eq!(
335        resolve_from_catalog(&src, &trust, &catalog, "nada").unwrap_err(),
336        DistError::NoEncontrado
337    );
338}