diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md index 6e9b7b5..9d39b19 100644 --- a/renaser/CHANGELOG.md +++ b/renaser/CHANGELOG.md @@ -1222,3 +1222,89 @@ misma capacidad a los apps: tres capacidades nuevas (`sys_net_mac`, - Captura de pantalla: la ventana `pregon :: voz hacia la red` aparece teselada, su pestaña sale en la barra de tareas entre `bitacora` y `tonada`, y la consola anuncia `manifiesto :: 9 apps nacidas del grafo`. + +--- + +## Fase 20 — Akasha Over Ether: el grafo distribuido — 2026-05-23 + +La red dejo de ser un puro medio para mensajes ad-hoc y se vuelve el cable +por donde el GRAFO de objetos se extiende a otras maquinas renaser. Tres +mensajes bastan; `postcard` los serializa; el EtherType `0x88B5` los +transporta; ningun byte de TCP, IP, DNS o cabecera transporte de los anos 80. + +### Añadido +- **Crate `akasha/`** — nucleo `#![no_std]` compartido (gemela de `formato`), + excluida del workspace anfitrion. Define `MensajeAkasha` con tres + variantes —`SolicitarObjeto(id)`, `ProveedorObjeto(id, payload)`, + `AnunciarRaiz(id)`—, las helpers `componer_frame(src, dst, msg)` y + `analizar_frame(bytes) -> Result<(Mac, MensajeAkasha)>`, y las constantes + `ETHER_TYPE_AKASHA = 0x88B5`, `MAX_PAYLOAD_AKASHA = 1486`. Seis pruebas + unitarias cubren el ida-y-vuelta de cada variante, la distincion entre + EtherType ajeno, frame truncado y payload basura. +- **Modulo `kernel/src/akasha.rs`** — el respondedor del nucleo. Tres oficios: + 1. **Demuxer** (`drenar_y_demultiplexar`): drena la cola RX del dispositivo + virtio-net y demultiplexa: frames AoE con payload valido los procesa + `procesar`; el resto va a la cola del userspace. + 2. **Atencion de mensajes** (`procesar`): `SolicitarObjeto` busca en + `almacen` y, si tiene el objeto, responde `ProveedorObjeto` unicast; + `ProveedorObjeto` verifica integridad (`blake3(payload) == id`) y + absorbe al grafo local con `almacen::almacenar`; `AnunciarRaiz` se + contabiliza y, si no tenemos el nodo, le solicita al emisor. + 3. **Faro** (`difundir_raiz` cada 5 s): difunde por broadcast el hash del + manifiesto actual — el faro Akasha que delata nuestra presencia en la + red de capa-2. +- **Cola del userspace** — el demuxer empuja los frames no-AoE a una + `VecDeque>` con profundidad maxima 64; `sys_net_recibir` ahora + pasa por `akasha::pop_usuario(buf)` en lugar de leer directamente del + driver. Asi el kernel filtra Akasha sin disputarle paquetes al userspace. +- **`tic_compositor`** — el punto de entrada de los oficios per-fotograma de + AoE, llamado desde el tic del compositor (cuyo latido es fiable). El + demuxer corre en cada vuelta; el faro se dispara cuando el reloj monotono + cruza `INTERVALO_FARO_MS` (5 s). +- **Contadores** (`ResumenAkasha`): `rx_solicitudes`, `rx_proveedores`, + `rx_anuncios`, `tx_*` equivalentes, `rx_descartados`, `usuario_encolados`, + `usuario_desbordados`. Quedaran de fundamento para un futuro indicador AoE + en la barra de tareas. + +### Cambiado +- **`tarea_red`** vuelve a su rol simple: enviar UN ARP request al gateway de + QEMU al arrancar. El demuxer y el faro pasaron al tic del compositor — + cuyo latido cooperativo es el unico GARANTIZADO bajo carga del reactor. + Un bucle propio en `tarea_red` (intentado primero) escala mal porque la + cadencia de `EsperaFrame::await` se degrada cuando los apps WASM bajo + `wasmi` ocupan el reactor (cada tic de app son decenas de ms de + interprete); medirlo contra el reloj monotono y enganchar al compositor + resuelve la falla con limpieza. +- **`sys_net_recibir`** lee de la cola del userspace que mantiene `akasha`, + no del driver. El bloque doc en `wasm/env.rs` lo refleja. +- **`drivers::red::recibir_en`** queda como primitiva del driver con + `#[allow(dead_code)]`: ya no la consume el sistema, pero la conservamos + para futuras herramientas de diagnostico que quieran leer la cola RX cruda. +- **Demuxer politico**: frames `0x88B5` con payload basura para postcard — + por ejemplo el saludo en texto plano de `pregon`— se cuentan en + `RX_DESCARTADOS` pero TAMBIEN viajan a la cola del userspace. Asi pregon + o cualquier otro app que comparta el EtherType experimental pueda + recibirlos sin que el kernel se los robe. + +### Verificado +- `cargo test -p akasha` — seis pruebas en verde: + ``` + test pruebas::anuncio_de_raiz_viaja_compacto ... ok + test pruebas::componer_y_analizar_solicitud_es_simetrico ... ok + test pruebas::ethertype_ajeno_se_distingue ... ok + test pruebas::frame_demasiado_corto_se_distingue ... ok + test pruebas::payload_invalido_se_distingue ... ok + test pruebas::proveedor_lleva_payload_arbitrario ... ok + ``` +- QEMU headless 60 s, `-object filter-dump,file=/tmp/renaser-aoe.pcap`. El + pcap captura 14 frames: + ``` + ARP: 2 (request del kernel + reply del gateway) + pregon: 1 (saludo texto plano) + AnunciarRaiz: 11 (faros del kernel, Δ promedio 5.86 s sobre 60 s) + ``` + Cada AnunciarRaiz lleva como payload el hash del manifiesto: + `2f3deadfcc7dae25..` (postcard de 33 bytes — variante + hash — sobre un + frame Ethernet total de 47 bytes). +- COM1 vuelca `akasha :: ANUNCIO emitido :: raiz=2f3deadfcc7dae25..` por + cada disparo; en una corrida de 60 s aparecen 11 anuncios consecutivos. diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md index d78cc73..1bbe374 100644 --- a/renaser/CLAUDE.md +++ b/renaser/CLAUDE.md @@ -92,10 +92,14 @@ lanzador a la izquierda y reloj `mm:ss` a la derecha que late cada segundo— la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste entre arranques en el grafo de objetos (tipografía 8×8 embebida)— la Fase 18 COMPLETA —red: virtio-net + ARP al gateway de QEMU + recepción de -paquetes registrada por COM1— y la Fase 19 COMPLETA —voz del userspace +paquetes registrada por COM1—, la Fase 19 COMPLETA —voz del userspace hacia la red: capacidades `sys_net_mac` / `sys_net_enviar` / `sys_net_recibir` + la app `pregon` que pregona su presencia con un broadcast Ethernet -y muestra el tráfico entrante—. Todo verificado en QEMU. Ver `ROADMAP.md`. +y muestra el tráfico entrante— y la Fase 20 COMPLETA —Akasha Over Ether: +crate `akasha` (no_std compartido) con `MensajeAkasha` postcard sobre +EtherType 0x88B5 + respondedor en el kernel (demuxer, atención de +SolicitarObjeto / ProveedorObjeto / AnunciarRaiz, faro periódico cada +5 s contra reloj monótono)—. Todo verificado en QEMU. Ver `ROADMAP.md`. ## Flujo de trabajo diff --git a/renaser/Cargo.lock b/renaser/Cargo.lock index 6dea5a4..76a61db 100644 --- a/renaser/Cargo.lock +++ b/renaser/Cargo.lock @@ -2,6 +2,14 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "akasha" +version = "0.1.0" +dependencies = [ + "postcard", + "serde", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -456,6 +464,7 @@ dependencies = [ name = "kernel" version = "0.1.0" dependencies = [ + "akasha", "bootloader_api", "crossbeam-queue", "embedded-graphics", diff --git a/renaser/Cargo.toml b/renaser/Cargo.toml index 1560dd0..b470169 100644 --- a/renaser/Cargo.toml +++ b/renaser/Cargo.toml @@ -15,10 +15,11 @@ resolver = "2" members = ["boot"] # El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte, # cada cual con su propio target; quedan fuera del espacio de trabajo. -# `formato` —el formato del grafo en disco— tambien se excluye: es un nucleo -# `no_std` que enlaza el kernel bare-metal, asi que se compila como dependencia -# de cada lado (kernel y boot) y no como miembro del workspace anfitrion. -exclude = ["kernel", "apps", "formato"] +# `formato` —el formato del grafo en disco— y `akasha` —el protocolo Akasha +# Over Ether (Fase 20)— tambien se excluyen: son nucleos `no_std` que enlaza +# el kernel bare-metal, asi que se compilan como dependencia de cada lado +# (kernel y boot) y no como miembros del workspace anfitrion. +exclude = ["kernel", "apps", "formato", "akasha"] # ----------------------------------------------------------------------------- # Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`. diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md index aff99c2..8a31aee 100644 --- a/renaser/DIARIO.md +++ b/renaser/DIARIO.md @@ -608,4 +608,31 @@ que la voz ya no es de uno solo. Ahora la casa habla en coro. --- +## Akasha — el grafo se vuelve aire + +Pregón gritaba sin alfabeto: una frase corta dirigida a quien quisiera +oír. Hoy la casa aprende su primer idioma propio para la red, y se le +ocurre uno sencillo, con apenas tres palabras: *pide*, *toma*, *aquí +estoy*. Pide un objeto del archivo por su huella; recibe el objeto en +respuesta; o anuncia, sin destinatario, la huella de su biblioteca +actual para que cualquiera que esté escuchando sepa por dónde puede +acompañarla. + +El nombre es viejo. *Akasha* es la palabra sánscrita para el éter, el +soporte de lo sutil; *Akasha Over Ether* es nuestro chiste a dos +niveles: el grafo viaja por el éter del cable, y la huella del grafo +—las treinta y dos cifras del BLAKE3— es la firma sutil de cada +objeto. Con tres frases bastan: el archivo de la casa deja de ser +exclusivamente local. Quien sintonice la red de la sala podrá, +mañana, recibir un objeto suyo o pedirle uno. + +De momento la casa habla sola. Cada cinco segundos, sin esperar a +nadie, suelta su faro: *aquí estoy, mi raíz es esta*. Si otra renaser +se encendiera en la misma sala, escucharía y podría empezar a pedir +los objetos que le faltan. Hoy no pasa eso porque sólo hay una casa, +pero el alfabeto está. Y, cuando un día se encienda la segunda, +empezará el diálogo sin que haya que añadir ni una palabra más. + +--- + *El diario continúa. La próxima página la escribirá la próxima jornada.* diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md index a47af82..ce6f28c 100644 --- a/renaser/ROADMAP.md +++ b/renaser/ROADMAP.md @@ -357,6 +357,49 @@ Líneas abiertas posteriores: una pila mínima ARP/IP/UDP para los apps; un servidor de eco / un cliente DNS de juguete; reciclado de las ranuras de ventana cerradas; audio con varias voces (PCM). +## Fase 20 — Akasha Over Ether: el grafo distribuido (completada) + +El grafo de objetos persistentes —direccionado por contenido, ya BLAKE3— +deja de ser una propiedad LOCAL del disco y se vuelve una propiedad +DISTRIBUIDA del cable. Sin TCP, sin IP, sin DNS: tres mensajes y un +EtherType propio bastan. + +- Crate `akasha/` (no_std compartido, gemela de `formato`): el + protocolo. `MensajeAkasha` con `SolicitarObjeto(id)`, + `ProveedorObjeto(id, payload)`, `AnunciarRaiz(id)`. Serializado con + `postcard`. Helpers `componer_frame` y `analizar_frame`. EtherType + experimental `0x88B5`. Seis pruebas unitarias. +- Módulo `kernel/src/akasha.rs`: el respondedor. Drena la cola RX del + dispositivo de red y **demultiplexa**: los frames AoE legítimos los + procesa en el núcleo; el resto va a una cola del userspace que + `sys_net_recibir` lee. Atiende `SolicitarObjeto` consultando + `almacen`, absorbe `ProveedorObjeto` verificando integridad, registra + `AnunciarRaiz` y, si ignoramos el nodo anunciado, le pedimos al emisor. +- Faro periódico: cada 5 s, `AnunciarRaiz` con el hash del manifiesto se + difunde por broadcast. La cadencia se mide contra el reloj monótono — + no contra los awaits—, por lo que el ritmo del faro es independiente + de cuánto trabajo del reactor consume cada vuelta. +- `tic_compositor`: el pulso fiable. Los oficios AoE se enganchan al tic + del compositor (cuyo latido cooperativo es el único garantizado bajo + carga del reactor); una tarea propia con `EsperaFrame::await` en bucle + no escalaba porque el intérprete `wasmi` de los apps degrada la + cadencia de los awaits a varios cientos de ms. +- `sys_net_recibir` lee de la cola del userspace que mantiene `akasha`, + no del driver. `drivers::red::recibir_en` queda como primitiva del + driver para futuras herramientas de diagnóstico. + +Verificada con `cargo test -p akasha` (6 pruebas verdes) y con QEMU +headless + `-object filter-dump`. En 60 s de captura: 11 `AnunciarRaiz` +con Δ promedio 5.86 s, un ARP request + reply y el saludo en texto +plano de `pregon`. La casa ya tiene su propio idioma de red — y un faro +que late. + +Líneas abiertas posteriores: una pila ARP/IP/UDP mínima para los apps; +un peer real en una segunda VM para ver la replicación end-to-end +`AnunciarRaiz → SolicitarObjeto → ProveedorObjeto`; un indicador AoE en +la barra de tareas que pinte los contadores de `ResumenAkasha`; +reciclado de las ranuras de ventana cerradas; audio con varias voces. + ## Principios que persisten entre fases - Reutilizar infraestructura madura de la comunidad antes que reinventar. diff --git a/renaser/akasha/Cargo.lock b/renaser/akasha/Cargo.lock new file mode 100644 index 0000000..43f3ec6 --- /dev/null +++ b/renaser/akasha/Cargo.lock @@ -0,0 +1,129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "akasha" +version = "0.1.0" +dependencies = [ + "postcard", + "serde", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/renaser/akasha/Cargo.toml b/renaser/akasha/Cargo.toml new file mode 100644 index 0000000..6288fb7 --- /dev/null +++ b/renaser/akasha/Cargo.toml @@ -0,0 +1,38 @@ +# ============================================================================= +# renaser :: akasha — protocolo Akasha Over Ether (AoE) +# ----------------------------------------------------------------------------- +# Nucleo `#![no_std]` COMPARTIDO: la verdad UNICA del protocolo de red nativo +# de renaser. Lo enlaza el kernel bare-metal (target `x86_64-unknown-none`) y, +# por ser no_std, lo compila tambien sin friccion el anfitrion `boot` y +# cualquier otra pieza que quiera dialogar en el mismo idioma — incluida +# alguna futura app del userspace. +# +# El protocolo extiende el GRAFO de objetos persistentes (Fase 6) sobre una +# red Ethernet cruda, sin pasar por TCP/IP. Tres mensajes bastan: solicitar +# un objeto por su hash BLAKE3, proveerlo de vuelta, anunciar la raiz actual +# del sistema. Se serializa con `postcard` —compacto y deterministico— y +# viaja en frames de EtherType experimental `0x88B5`. +# +# Queda EXCLUIDO del espacio de trabajo (ver el Cargo.toml raiz), como el +# kernel y como `formato`: lo consume un paquete bare-metal, asi que fija +# sus versiones de forma explicita. +# ============================================================================= + +[package] +name = "akasha" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" +authors = ["JL Soltech "] +description = "renaser :: protocolo Akasha Over Ether (AoE) — grafo distribuido sobre Ethernet" + +[lib] +bench = false +doctest = false + +[dependencies] +# `serde` aporta los rasgos de (de)serializacion; `postcard` los materializa en +# un formato binario compacto, el mismo que ya usa `formato` para el disco. +# Asi el grafo habla el mismo idioma en el disco y en el cable. +serde = { version = "1", default-features = false, features = ["alloc", "derive"] } +postcard = { version = "1", default-features = false, features = ["alloc"] } diff --git a/renaser/akasha/src/lib.rs b/renaser/akasha/src/lib.rs new file mode 100644 index 0000000..12ef6b1 --- /dev/null +++ b/renaser/akasha/src/lib.rs @@ -0,0 +1,252 @@ +// ============================================================================= +// renaser :: akasha — Akasha Over Ether (AoE) +// ----------------------------------------------------------------------------- +// Fase 19 demostro que un app del userspace puede inyectar frames Ethernet +// crudos al cable —`pregon` gritando «hola» con su EtherType experimental—, +// pero ese saludo era texto plano, sin estructura. La Fase 20 da el siguiente +// paso natural: **fundar un protocolo nativo del ecosistema** para que +// renaser hable consigo mismo a traves de la red, sin TCP, sin IP, sin las +// sobrecargas de cabecera de los años 80. +// +// AoE viaja DIRECTAMENTE sobre Ethernet (`EtherType = 0x88B5`, rango +// experimental reservado por IEEE para uso local). Cada frame transporta un +// `MensajeAkasha` serializado con `postcard` —el mismo codec que ya usa +// `formato` para el grafo en disco—. Tres mensajes bastan: +// +// 1. `SolicitarObjeto(id)` — pide un nodo del grafo por su hash BLAKE3. +// 2. `ProveedorObjeto(id, d)` — responde con el payload binario del nodo. +// 3. `AnunciarRaiz(id)` — difunde el hash de la raiz del sistema. +// +// Con esto basta para extender el grafo de objetos —direccionado por +// contenido, inmutable, ya BLAKE3— a OTRAS maquinas renaser que escuchen en +// la misma red de capa-2. El receptor de un `AnunciarRaiz` puede comparar +// hashes, descubrir que le falta un nodo, pedirlo con `SolicitarObjeto` y +// ensamblar el grafo del par. El grafo deja de ser una propiedad LOCAL del +// disco y se vuelve una propiedad DISTRIBUIDA del cable. +// +// Esta crate es la unica verdad del protocolo. Es un nucleo `#![no_std]` —el +// kernel bare-metal la enlaza—; cualquier app, daemon u orquestador que +// hable AoE consume estos mismos tipos. Ningun otro modulo redefine ni un +// byte del trazado del frame. +// ============================================================================= + +#![no_std] +#![deny(unsafe_op_in_unsafe_fn)] + +extern crate alloc; + +use alloc::vec::Vec; + +use serde::{Deserialize, Serialize}; + +// ============================================================================= +// Constantes del protocolo en el cable +// ============================================================================= + +/// EtherType de AoE, rango experimental reservado por IEEE para uso local +/// (0x88B5/0x88B6). renaser elige el primero. Cualquier frame del cable que +/// no porte este EtherType NO es Akasha, y se entrega al userspace. +pub const ETHER_TYPE_AKASHA: u16 = 0x88B5; + +/// Tamaño de la cabecera Ethernet, en bytes (dst + src + ethertype). +pub const CABECERA_ETHERNET: usize = 14; + +/// Maximo del payload AoE serializado, en bytes. Acotado para que el frame +/// completo (cabecera + payload) no exceda una MTU Ethernet sin VLAN +/// (1500 - 14 = 1486) y se transmita SIN fragmentar. Si un mensaje no cabe, +/// el llamante debe partirlo en varios objetos del grafo y referirse a ellos. +pub const MAX_PAYLOAD_AKASHA: usize = 1486; + +/// MAC de broadcast — la difunde todo Ethernet, equivalente a `255.255.255.255` +/// en IPv4 pero en capa-2 pura. +pub const MAC_BROADCAST: [u8; 6] = [0xff; 6]; + +/// El identificador de un objeto del grafo: el hash BLAKE3 de su forma +/// serializada. En un almacen direccionado por contenido, la identidad ES el +/// contenido. Coincide byte a byte con `formato::Hash` —AoE habla el mismo +/// idioma de identidad que el grafo en disco—. +pub type ObjectId = [u8; 32]; + +/// Una direccion MAC, en seis bytes. +pub type Mac = [u8; 6]; + +// ============================================================================= +// El mensaje +// ============================================================================= + +/// Un mensaje AoE — la unidad de protocolo que viaja en un frame de +/// `ETHER_TYPE_AKASHA`. Tres variantes bastan para fundar un grafo distribuido: +/// +/// - `SolicitarObjeto` pregunta por un objeto identificado por su hash. +/// - `ProveedorObjeto` responde con el payload binario del objeto. El +/// receptor recompone el `formato::Objeto` aplicando su deserializador a +/// `payload` y verificando que `blake3(payload) == id`. +/// - `AnunciarRaiz` difunde el hash de la raiz —el ancla del sistema +/// actual—. Sirve como faro: quien escuche y carezca de ese nodo en su +/// grafo local puede iniciar una solicitud. +/// +/// La codificacion `postcard` es estable y compacta: un `SolicitarObjeto` +/// ocupa 33 bytes (1 de variante + 32 de hash) en el cable. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum MensajeAkasha { + /// Solicito el objeto identificado por este hash. + SolicitarObjeto(ObjectId), + /// Aqui esta el payload binario del objeto identificado por este hash. + /// El payload es la forma serializada `postcard` de un `formato::Objeto`, + /// y el receptor DEBE verificar que `blake3(payload) == id` antes de + /// confiar en el contenido. + ProveedorObjeto(ObjectId, Vec), + /// Difundo el hash de mi raiz actual — quien me escuche y le falte este + /// nodo en su grafo local puede pedirmelo. + AnunciarRaiz(ObjectId), +} + +/// El motivo por el que un frame AoE no se pudo componer o analizar. +#[derive(Clone, Copy, Debug)] +pub enum ErrorAkasha { + /// El payload serializado excede `MAX_PAYLOAD_AKASHA`. + PayloadDemasiadoLargo, + /// El frame en el cable es mas corto que la cabecera Ethernet. + FrameDemasiadoCorto, + /// El EtherType del frame no es `ETHER_TYPE_AKASHA`. + EtherTypeAjeno, + /// `postcard` no supo deserializar el payload (basura, version distinta, + /// otro protocolo experimental ajeno). + PayloadInvalido, +} + +// ============================================================================= +// Composicion y analisis de frames +// ============================================================================= + +/// Compone un frame Ethernet completo —cabecera (14 bytes) + payload AoE +/// (`postcard(mensaje)`)— listo para entregar al driver. El llamante elige el +/// destino (`MAC_BROADCAST` para difundir) y firma el frame con su propia +/// MAC. Devuelve el frame completo, o un error si el mensaje serializado +/// excede `MAX_PAYLOAD_AKASHA`. +pub fn componer_frame( + src: Mac, + dst: Mac, + mensaje: &MensajeAkasha, +) -> Result, ErrorAkasha> { + let payload = + postcard::to_allocvec(mensaje).map_err(|_| ErrorAkasha::PayloadDemasiadoLargo)?; + if payload.len() > MAX_PAYLOAD_AKASHA { + return Err(ErrorAkasha::PayloadDemasiadoLargo); + } + let mut frame = Vec::with_capacity(CABECERA_ETHERNET + payload.len()); + frame.extend_from_slice(&dst); + frame.extend_from_slice(&src); + frame.extend_from_slice(ÐER_TYPE_AKASHA.to_be_bytes()); + frame.extend_from_slice(&payload); + Ok(frame) +} + +/// Analiza un frame Ethernet entrante. Si su EtherType es `ETHER_TYPE_AKASHA` +/// y el payload deserializa como un `MensajeAkasha`, devuelve la MAC de origen +/// y el mensaje. En cualquier otro caso —EtherType ajeno, frame truncado, +/// `postcard` no le encuentra sentido al payload— devuelve un `ErrorAkasha` +/// que distingue el motivo: el llamante puede entonces decidir si lo entrega +/// al userspace (otro protocolo) o lo descarta (basura). +pub fn analizar_frame(frame: &[u8]) -> Result<(Mac, MensajeAkasha), ErrorAkasha> { + if frame.len() < CABECERA_ETHERNET { + return Err(ErrorAkasha::FrameDemasiadoCorto); + } + let etype = u16::from_be_bytes([frame[12], frame[13]]); + if etype != ETHER_TYPE_AKASHA { + return Err(ErrorAkasha::EtherTypeAjeno); + } + let mut src: Mac = [0; 6]; + src.copy_from_slice(&frame[6..12]); + let payload = &frame[CABECERA_ETHERNET..]; + let mensaje: MensajeAkasha = + postcard::from_bytes(payload).map_err(|_| ErrorAkasha::PayloadInvalido)?; + Ok((src, mensaje)) +} + +// ============================================================================= +// Pruebas +// ============================================================================= + +#[cfg(test)] +mod pruebas { + use super::*; + use alloc::vec; + + const MAC_A: Mac = [0x52, 0x54, 0x00, 0x12, 0x34, 0x56]; + const MAC_B: Mac = [0x52, 0x54, 0x00, 0xAB, 0xCD, 0xEF]; + const HASH_X: ObjectId = [0x11; 32]; + + #[test] + fn componer_y_analizar_solicitud_es_simetrico() { + let msg = MensajeAkasha::SolicitarObjeto(HASH_X); + let frame = componer_frame(MAC_A, MAC_B, &msg).unwrap(); + // Cabecera: dst + src + ethertype. + assert_eq!(&frame[0..6], &MAC_B); + assert_eq!(&frame[6..12], &MAC_A); + assert_eq!(&frame[12..14], ÐER_TYPE_AKASHA.to_be_bytes()); + let (src, rec) = analizar_frame(&frame).unwrap(); + assert_eq!(src, MAC_A); + match rec { + MensajeAkasha::SolicitarObjeto(id) => assert_eq!(id, HASH_X), + otro => panic!("variante inesperada: {otro:?}"), + } + } + + #[test] + fn anuncio_de_raiz_viaja_compacto() { + let frame = + componer_frame(MAC_A, MAC_BROADCAST, &MensajeAkasha::AnunciarRaiz(HASH_X)) + .unwrap(); + // 14 cabecera + 1 byte variante + 32 hash = 47 bytes en el cable. + assert_eq!(frame.len(), 47); + assert_eq!(&frame[0..6], &MAC_BROADCAST); + } + + #[test] + fn proveedor_lleva_payload_arbitrario() { + let payload = vec![1u8, 2, 3, 4, 5, 6, 7, 8]; + let msg = MensajeAkasha::ProveedorObjeto(HASH_X, payload.clone()); + let frame = componer_frame(MAC_A, MAC_B, &msg).unwrap(); + let (_, rec) = analizar_frame(&frame).unwrap(); + match rec { + MensajeAkasha::ProveedorObjeto(id, p) => { + assert_eq!(id, HASH_X); + assert_eq!(p, payload); + } + otro => panic!("variante inesperada: {otro:?}"), + } + } + + #[test] + fn frame_demasiado_corto_se_distingue() { + assert!(matches!( + analizar_frame(&[0u8; 8]), + Err(ErrorAkasha::FrameDemasiadoCorto) + )); + } + + #[test] + fn ethertype_ajeno_se_distingue() { + // Cabecera valida pero EtherType de IPv4 (0x0800). + let mut frame = [0u8; CABECERA_ETHERNET]; + frame[12] = 0x08; + frame[13] = 0x00; + assert!(matches!( + analizar_frame(&frame), + Err(ErrorAkasha::EtherTypeAjeno) + )); + } + + #[test] + fn payload_invalido_se_distingue() { + // EtherType correcto, pero el payload es basura para postcard. + let mut frame = [0u8; CABECERA_ETHERNET + 4]; + frame[12..14].copy_from_slice(ÐER_TYPE_AKASHA.to_be_bytes()); + frame[14..].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]); + assert!(matches!( + analizar_frame(&frame), + Err(ErrorAkasha::PayloadInvalido) + )); + } +} diff --git a/renaser/kernel/Cargo.toml b/renaser/kernel/Cargo.toml index 0573f72..8f8ae6e 100644 --- a/renaser/kernel/Cargo.toml +++ b/renaser/kernel/Cargo.toml @@ -76,6 +76,14 @@ virtio-drivers = { version = "0.13", default-features = false, features = ["allo # sola verdad del formato de disco, imposible de divergir entre los dos lados. formato = { path = "../formato" } +# --- Fase 20 :: Akasha Over Ether --- +# El protocolo nativo de red de renaser: mensajes `MensajeAkasha` serializados +# con `postcard`, sobre Ethernet crudo. Mismo patron que `formato`: nucleo +# `no_std` compartido, no miembro del workspace anfitrion. El kernel y boot lo +# enlazan, y cualquier app o daemon que quiera hablar AoE en el futuro tomara +# la misma crate — una sola verdad del protocolo. +akasha = { path = "../akasha" } + # --- Fase 8 (preparación) :: el compositor --- # `mirada-layout` es el motor de teselado del compositor de brahman — # geometría pura (rectángulos, foco, Z-order), `no_std`, sin smithay ni diff --git a/renaser/kernel/src/akasha.rs b/renaser/kernel/src/akasha.rs new file mode 100644 index 0000000..c6f5360 --- /dev/null +++ b/renaser/kernel/src/akasha.rs @@ -0,0 +1,431 @@ +// ============================================================================= +// renaser :: kernel/src/akasha — Fase 20 :: Akasha Over Ether +// ----------------------------------------------------------------------------- +// El servicio del kernel que habla AoE: el respondedor de Akasha Over Ether. +// Tres oficios: +// +// 1. Drena la cola RX del dispositivo de red y DEMULTIPLEXA cada frame: +// - Si su EtherType es `0x88B5` y el payload deserializa como un +// `MensajeAkasha`, lo procesa en el kernel. +// - Cualquier otro frame se encola hacia el userspace: las apps lo +// recibiran via `sys_net_recibir` como hasta ahora. +// 2. Atiende los mensajes AoE: +// - `SolicitarObjeto(id)` → si tenemos `id` en el grafo, +// respondemos con `ProveedorObjeto`. +// - `ProveedorObjeto(id, p)` → si la integridad cuadra, lo absorbemos +// al grafo local. +// - `AnunciarRaiz(id)` → se contabiliza y, si no tenemos el +// nodo, se le solicita al emisor. +// 3. Difunde periodicamente nuestra raiz del grafo (`AnunciarRaiz` con el +// hash del manifiesto) — el faro que delata nuestra presencia. +// +// Con esto, el grafo de objetos —direccionado por contenido, ya BLAKE3— deja +// de ser una propiedad local del disco y empieza a ser una propiedad +// distribuida del cable. Tres frames bastan, sin TCP, sin IP, sin DNS. +// ============================================================================= + +use alloc::collections::VecDeque; +use alloc::vec::Vec; +use core::fmt::Write; +use core::sync::atomic::{AtomicU64, Ordering}; + +use spin::{Mutex, Once}; + +use crate::async_system::reloj; + +use akasha::{ + analizar_frame, componer_frame, ErrorAkasha, Mac, MensajeAkasha, MAC_BROADCAST, +}; +use formato::Hash; + +use crate::almacen; +use crate::baliza; +use crate::drivers::red; + +// ============================================================================= +// Cola del userspace — frames NO-AoE en espera de `sys_net_recibir` +// ============================================================================= + +/// Cuantos frames como mucho retenemos en la cola del userspace. Cota dura: +/// si el userspace se duerme, los frames mas antiguos se pierden antes que +/// la cola se desborde. +const PROFUNDIDAD_COLA_USUARIO: usize = 64; + +/// La cola FIFO de frames que NO son AoE y aguardan a `sys_net_recibir`. Cada +/// frame se copia tal cual lo entrega el driver; el userspace ve la cabecera +/// Ethernet completa, como antes de la Fase 20. +static COLA_USUARIO: Mutex>> = Mutex::new(VecDeque::new()); + +// ============================================================================= +// Contadores — la voz que la barra y el diagnostico leen +// ============================================================================= + +/// Frames AoE procesados en RX, por variante. +static RX_SOLICITUDES: AtomicU64 = AtomicU64::new(0); +static RX_PROVEEDORES: AtomicU64 = AtomicU64::new(0); +static RX_ANUNCIOS: AtomicU64 = AtomicU64::new(0); + +/// Frames AoE emitidos en TX, por variante. +static TX_SOLICITUDES: AtomicU64 = AtomicU64::new(0); +static TX_PROVEEDORES: AtomicU64 = AtomicU64::new(0); +static TX_ANUNCIOS: AtomicU64 = AtomicU64::new(0); + +/// Frames descartados en RX por ser basura (payload AoE invalido). +static RX_DESCARTADOS: AtomicU64 = AtomicU64::new(0); + +/// Frames no-AoE encolados hacia el userspace. +static USUARIO_ENCOLADOS: AtomicU64 = AtomicU64::new(0); +/// Frames no-AoE descartados por desbordamiento de la cola del userspace. +static USUARIO_DESBORDADOS: AtomicU64 = AtomicU64::new(0); + +/// La MAC con la que firmamos los frames AoE. La cachea `montar`. +static NUESTRA_MAC: Once = Once::new(); + +// ============================================================================= +// Montaje +// ============================================================================= + +/// Anota la MAC del dispositivo de red. La llama el orquestador cuando el +/// driver entrega su `Mac` tras `red::montar`. +pub fn montar(mac: Mac) { + NUESTRA_MAC.call_once(|| mac); +} + +// ============================================================================= +// Demultiplexor — el oficio numero 1 de la Fase 20 +// ============================================================================= + +/// Drena la cola RX del dispositivo y demultiplexa cada frame: +/// - Frames AoE (EtherType `0x88B5` con payload valido) → `procesar`. +/// - Cualquier otro frame → cola del userspace (lo recogera `sys_net_recibir`). +/// +/// Llamada en CADA fotograma desde la tarea cooperativa de red. El cerrojo +/// del driver virtio-net se libera entre frame y frame; el del userspace solo +/// se toma para empujar, sin solapar con el del driver. +pub fn drenar_y_demultiplexar() { + let mac = match NUESTRA_MAC.get().copied() { + Some(m) => m, + None => return, + }; + red::drenar_rx(|frame| { + // Intentar analizar como AoE. Si el EtherType cuadra Y el payload + // deserializa, procesamos en el kernel; el frame NO viaja al + // userspace —el protocolo es asunto del nucleo—. + match analizar_frame(frame) { + Ok((origen, mensaje)) => procesar(mensaje, origen, mac), + // EtherType ajeno o frame truncado: a la cola del userspace. + Err(ErrorAkasha::EtherTypeAjeno) | Err(ErrorAkasha::FrameDemasiadoCorto) => { + encolar_para_usuario(frame) + } + // EtherType nuestro pero payload no-postcard: el ejemplo canonico + // es el saludo en texto plano de `pregon`. NO es Akasha legitimo, + // pero tampoco basura del cable — es un protocolo userspace que + // comparte EtherType. Lo contamos y lo dejamos pasar al userspace + // para que el app destinatario (si lo hay) lo recoja. + Err(ErrorAkasha::PayloadInvalido) | Err(ErrorAkasha::PayloadDemasiadoLargo) => { + RX_DESCARTADOS.fetch_add(1, Ordering::Relaxed); + encolar_para_usuario(frame); + } + } + }); +} + +/// Encola un frame entero hacia el userspace. Si la cola esta llena, descarta +/// el mas antiguo —preferimos perder el pasado a quedar sordos del futuro—. +fn encolar_para_usuario(frame: &[u8]) { + let mut cola = COLA_USUARIO.lock(); + if cola.len() >= PROFUNDIDAD_COLA_USUARIO { + cola.pop_front(); + USUARIO_DESBORDADOS.fetch_add(1, Ordering::Relaxed); + } + cola.push_back(frame.to_vec()); + USUARIO_ENCOLADOS.fetch_add(1, Ordering::Relaxed); +} + +// ============================================================================= +// Interfaz que ve `sys_net_recibir` +// ============================================================================= + +/// Saca UN frame de la cola del userspace hacia `buf`. Devuelve los bytes +/// copiados (acotados por `buf.len()`), o `0` si no hay frame pendiente. El +/// reemplazo natural de `red::recibir_en` para el lado WASM —ahora el +/// userspace ya no compite con el kernel por la cola RX del dispositivo—. +pub fn pop_usuario(buf: &mut [u8]) -> usize { + let frame = match COLA_USUARIO.lock().pop_front() { + Some(f) => f, + None => return 0, + }; + let n = frame.len().min(buf.len()); + buf[..n].copy_from_slice(&frame[..n]); + n +} + +// ============================================================================= +// Atencion de mensajes — el oficio numero 2 +// ============================================================================= + +/// Procesa UN mensaje AoE entrante. Contabiliza, traza y —si toca— responde. +fn procesar(mensaje: MensajeAkasha, origen: Mac, nuestra: Mac) { + match mensaje { + MensajeAkasha::SolicitarObjeto(id) => { + RX_SOLICITUDES.fetch_add(1, Ordering::Relaxed); + atender_solicitud(id, origen, nuestra); + } + MensajeAkasha::ProveedorObjeto(id, payload) => { + RX_PROVEEDORES.fetch_add(1, Ordering::Relaxed); + absorber_proveedor(id, &payload, origen); + } + MensajeAkasha::AnunciarRaiz(id) => { + RX_ANUNCIOS.fetch_add(1, Ordering::Relaxed); + atender_anuncio(id, origen, nuestra); + } + } +} + +/// Si tenemos el objeto que nos piden, le respondemos al solicitante (unicast) +/// con un `ProveedorObjeto` que carga la forma serializada del nodo. La +/// integridad se preserva por construccion: `payload` es exactamente la +/// secuencia que rehashea al `id` pedido. Si no lo tenemos, no decimos nada +/// —AoE no tiene «not found»; un par puede preguntarle a otro—. +fn atender_solicitud(id: Hash, origen: Mac, nuestra: Mac) { + let objeto = match almacen::recuperar(&id) { + Ok(Some(o)) => o, + Ok(None) => { + let _ = writeln!( + baliza::Serie, + "akasha :: solicitud rechazada (objeto ausente) :: {}", + FormatoHash(&id) + ); + return; + } + Err(motivo) => { + let _ = writeln!(baliza::Serie, "akasha :: solicitud fallida :: {motivo}"); + return; + } + }; + let payload = match objeto.serializar() { + Ok(p) => p, + Err(_) => return, + }; + // Defensa en profundidad: el rehash DEBE coincidir. postcard es canonico + // pero verificamos antes de poner algo en el cable que dice ser `id`. + if formato::hash(&payload) != id { + let _ = writeln!( + baliza::Serie, + "akasha :: rehash no coincide al servir :: descartado" + ); + return; + } + let mensaje = MensajeAkasha::ProveedorObjeto(id, payload); + if enviar(&mensaje, nuestra, origen).is_ok() { + TX_PROVEEDORES.fetch_add(1, Ordering::Relaxed); + let _ = writeln!( + baliza::Serie, + "akasha :: PROVEEDOR enviado :: {} -> {}", + FormatoHash(&id), + FormatoMac(&origen) + ); + } +} + +/// Acepta un objeto venido del cable. Verifica que su forma serializada +/// rehashea al `id` que el remitente afirma; si cuadra, lo deposita en el +/// grafo local; si no, lo descarta con una traza. La unica entrada de +/// integridad esta aqui: el grafo local no admite mentiras. +fn absorber_proveedor(id: Hash, payload: &[u8], origen: Mac) { + if formato::hash(payload) != id { + let _ = writeln!( + baliza::Serie, + "akasha :: proveedor rechazado (rehash no coincide) :: src={}", + FormatoMac(&origen) + ); + return; + } + let objeto = match formato::Objeto::deserializar(payload) { + Ok(o) => o, + Err(motivo) => { + let _ = writeln!( + baliza::Serie, + "akasha :: proveedor no deserializable :: {motivo}" + ); + return; + } + }; + match almacen::almacenar(objeto.datos, objeto.hijos) { + Ok(hash) => { + let _ = writeln!( + baliza::Serie, + "akasha :: PROVEEDOR absorbido :: {} <- {}", + FormatoHash(&hash), + FormatoMac(&origen) + ); + } + Err(motivo) => { + let _ = writeln!( + baliza::Serie, + "akasha :: no se pudo absorber proveedor :: {motivo}" + ); + } + } +} + +/// Un par nos anuncio su raiz. Si la conocemos, no hay nada que hacer; si no, +/// le pedimos el nodo: el `AnunciarRaiz` es el faro que basta para iniciar la +/// replicacion. +fn atender_anuncio(id: Hash, origen: Mac, nuestra: Mac) { + // ¿Lo tenemos ya? Entonces nada que pedir. + if let Ok(Some(_)) = almacen::recuperar(&id) { + let _ = writeln!( + baliza::Serie, + "akasha :: anuncio reconocido (ya lo tenemos) :: {} de {}", + FormatoHash(&id), + FormatoMac(&origen) + ); + return; + } + // No lo tenemos — pedirlo al emisor en unicast. + let mensaje = MensajeAkasha::SolicitarObjeto(id); + if enviar(&mensaje, nuestra, origen).is_ok() { + TX_SOLICITUDES.fetch_add(1, Ordering::Relaxed); + let _ = writeln!( + baliza::Serie, + "akasha :: SOLICITUD enviada :: {} -> {}", + FormatoHash(&id), + FormatoMac(&origen) + ); + } +} + +// ============================================================================= +// Difusion periodica de nuestra raiz — el oficio numero 3 +// ============================================================================= + +/// Intervalo entre faros AoE consecutivos, en milisegundos. 5 s es un +/// compromiso conservador: lo bastante frecuente para descubrir vecinos +/// nuevos sin saturar la red de capa-2 con anuncios. +const INTERVALO_FARO_MS: u64 = 5_000; + +/// Marca del reloj monotono (`reloj::milisegundos`) en la que se difundira el +/// proximo faro. La inicializamos a `0` — la primera difusion ocurre cuanto +/// antes pase `tic_compositor` con el manifiesto montado—. +static PROXIMO_FARO_MS: AtomicU64 = AtomicU64::new(0); + +/// Punto de entrada que el tic del compositor llama una vez por fotograma. +/// Junta los dos oficios AoE que viven en linea con el latido del escritorio: +/// - drenar la cola RX del dispositivo y demultiplexar Akasha vs userspace, +/// - difundir nuestra raiz cada `INTERVALO_FARO_MS` segun reloj monotono. +/// +/// El compositor late de forma fiable (avanza el reloj de la barra cada +/// segundo, prueba indirecta de que su tarea se atiende sin atascos); por +/// eso elegimos su tic como portador. La cadencia del faro se mide contra +/// el reloj monotono y NO contra los awaits, asi el ritmo del faro es +/// independiente de cuanto trabajo del reactor consume cada vuelta. +pub fn tic_compositor() { + drenar_y_demultiplexar(); + let ahora = reloj::milisegundos(); + let proximo = PROXIMO_FARO_MS.load(Ordering::Relaxed); + if ahora >= proximo { + difundir_raiz(); + PROXIMO_FARO_MS.store(ahora + INTERVALO_FARO_MS, Ordering::Relaxed); + } +} + +/// Anuncia, por broadcast, el hash del manifiesto actual. Es el faro de +/// renaser: quien escuche en la red de capa-2 sabra de nuestra existencia y +/// del nodo raiz del grafo. Si aun no hay manifiesto anclado, no se difunde +/// nada — el silencio es preferible a un faro vacio. +pub fn difundir_raiz() { + let nuestra = match NUESTRA_MAC.get().copied() { + Some(m) => m, + None => return, + }; + let id = match almacen::manifiesto() { + Some(h) => h, + None => return, + }; + let mensaje = MensajeAkasha::AnunciarRaiz(id); + if enviar(&mensaje, nuestra, MAC_BROADCAST).is_ok() { + TX_ANUNCIOS.fetch_add(1, Ordering::Relaxed); + let _ = writeln!( + baliza::Serie, + "akasha :: ANUNCIO emitido :: raiz={}", + FormatoHash(&id) + ); + } +} + +// ============================================================================= +// Helpers internos +// ============================================================================= + +/// Compone un frame AoE y lo entrega al driver. Punto de salida unico de TX. +fn enviar(mensaje: &MensajeAkasha, src: Mac, dst: Mac) -> Result<(), ()> { + let frame = componer_frame(src, dst, mensaje).map_err(|_| ())?; + red::enviar(&frame).map_err(|motivo| { + let _ = writeln!(baliza::Serie, "akasha :: envio fallido :: {motivo}"); + }) +} + +// ============================================================================= +// Lectores de estado — los expone la barra de tareas (futuro indicador) y el +// diagnostico de COM1 +// ============================================================================= + +/// Resumen de actividad AoE, en una sola lectura coherente. Los enteros se +/// leen `Relaxed`: el resumen es informativo, no transaccional. +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +pub struct ResumenAkasha { + pub rx_solicitudes: u64, + pub rx_proveedores: u64, + pub rx_anuncios: u64, + pub tx_solicitudes: u64, + pub tx_proveedores: u64, + pub tx_anuncios: u64, + pub rx_descartados: u64, + pub usuario_encolados: u64, + pub usuario_desbordados: u64, +} + +#[allow(dead_code)] +pub fn resumen() -> ResumenAkasha { + ResumenAkasha { + rx_solicitudes: RX_SOLICITUDES.load(Ordering::Relaxed), + rx_proveedores: RX_PROVEEDORES.load(Ordering::Relaxed), + rx_anuncios: RX_ANUNCIOS.load(Ordering::Relaxed), + tx_solicitudes: TX_SOLICITUDES.load(Ordering::Relaxed), + tx_proveedores: TX_PROVEEDORES.load(Ordering::Relaxed), + tx_anuncios: TX_ANUNCIOS.load(Ordering::Relaxed), + rx_descartados: RX_DESCARTADOS.load(Ordering::Relaxed), + usuario_encolados: USUARIO_ENCOLADOS.load(Ordering::Relaxed), + usuario_desbordados: USUARIO_DESBORDADOS.load(Ordering::Relaxed), + } +} + +// ============================================================================= +// Formateadores cortos para la traza de COM1 — sin allocations innecesarias +// ============================================================================= + +struct FormatoHash<'h>(&'h Hash); +impl core::fmt::Display for FormatoHash<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + // Primeros 8 bytes en hex — suficiente para distinguir en una traza. + for b in &self.0[..8] { + write!(f, "{b:02x}")?; + } + write!(f, "..") + } +} + +struct FormatoMac<'m>(&'m Mac); +impl core::fmt::Display for FormatoMac<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for (i, b) in self.0.iter().enumerate() { + if i > 0 { + write!(f, ":")?; + } + write!(f, "{b:02x}")?; + } + Ok(()) + } +} diff --git a/renaser/kernel/src/drivers/red.rs b/renaser/kernel/src/drivers/red.rs index 83bb051..5a7f4e6 100644 --- a/renaser/kernel/src/drivers/red.rs +++ b/renaser/kernel/src/drivers/red.rs @@ -217,7 +217,12 @@ pub fn drenar_rx(mut callback: F) { /// Recibe UN paquete: copia su contenido en `buf` y devuelve los bytes /// copiados, o `0` si no hay paquete pendiente. La interfaz que el host -/// expone a los apps via `sys_net_recibir` — un paquete por llamada (Fase 19). +/// expuso a los apps via `sys_net_recibir` en la Fase 19. Desde la Fase 20 +/// el kernel toma la cola RX para FILTRAR Akasha (`akasha::drenar_y_demultiplexar` +/// + `akasha::pop_usuario`); `recibir_en` queda como primitiva del driver, +/// disponible para futuros consumidores directos (p. ej., una herramienta de +/// diagnostico que quiera leer la cola RX sin demultiplexar). +#[allow(dead_code)] pub fn recibir_en(buf: &mut [u8]) -> usize { let Some(tarjeta) = TARJETA.get() else { return 0; diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs index c730cdb..be6339d 100644 --- a/renaser/kernel/src/main.rs +++ b/renaser/kernel/src/main.rs @@ -45,6 +45,7 @@ use bootloader_api::{entry_point, BootInfo}; use spin::{Mutex, Once}; // --- Subsistemas del kernel --- +mod akasha; mod almacen; mod async_system; mod baliza; @@ -165,6 +166,10 @@ async fn tarea_compositor() { // FASE 16 :: avanzar el reloj de la barra de tareas — recompone si el // segundo cambio respecto al ultimo mostrado. Si no, vuelve enseguida. compositor::tick_reloj(); + // FASE 20 :: pulso del oficio AoE — el demultiplexor de RX en cada + // tic (cero coste sin trafico), y el faro `AnunciarRaiz` cada 5 s + // (medido contra el reloj monotono, no contra los awaits). + akasha::tic_compositor(); // FASE 10 :: atender las altas en vivo. Por cada `Alt+N` pendiente, // dar a luz una aplicacion nueva — el compositor solo conto la // peticion; instanciar el WASM es trabajo del orquestador. @@ -174,16 +179,14 @@ async fn tarea_compositor() { } } -/// FASE 18/19 — la primera voz del kernel hacia la red. Envia un ARP request -/// al gateway de QEMU para anunciarse, y termina: a partir de la Fase 19, los -/// apps drenan la cola RX por su cuenta via `sys_net_recibir`, asi que el -/// kernel no le quita paquetes a nadie. +/// FASE 18 — saludo inicial a la red. La tarea es CORTA y de un solo tiro: +/// deja estabilizarse la cola RX del dispositivo y envia un ARP request al +/// gateway de QEMU para anunciarse en capa-3. El demuxer Akasha (Fase 20) +/// vive en el tic del compositor; el faro periodico, en `tarea_akasha_faro`. async fn tarea_red(mac: drivers::red::Mac) { - // Dejar un par de fotogramas para que la cola RX se estabilice. for _ in 0..10 { async_system::reloj::EsperaFrame::nueva().await; } - // Componer y enviar el ARP request: «¿quien tiene 10.0.2.2?». let frame = drivers::red::componer_arp_request( mac, drivers::red::IP_RENASER, @@ -200,9 +203,14 @@ async fn tarea_red(mac: drivers::red::Mac) { let _ = writeln!(baliza::Serie, "red :: envio fallido :: {motivo}"); } } - // La tarea termina aqui. Los apps se encargan del trafico desde ahora. } +/// FASE 20 — el faro Akasha. Cada `INTERVALO_FARO` fotogramas (= 5 s a +/// 100 Hz) difunde por broadcast `AnunciarRaiz(manifiesto)`. La primera +/// difusion se demora unos pocos fotogramas para que el grafo termine de +/// montarse — si `manifiesto()` aun es `None`, el envio es no-op y el +/// siguiente faro lo reintentara. Tarea sin fin, con la misma forma que +/// `tarea_compositor` (probada y latiente). /// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el /// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las /// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el @@ -552,6 +560,9 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], drivers::red::irq(), ); + // FASE 20 :: registrar la MAC en el respondedor Akasha, para que + // pueda firmar sus frames AoE. + akasha::montar(mac); } Err(motivo) => { let _ = writeln!(baliza::Serie, "red :: virtio-net :: {motivo}"); @@ -578,8 +589,9 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { // disco de forma ASINCRONA: la demostracion de que la IRQ del disco // conduce la E/S sin detener a las aplicaciones visuales. ejecutor.spawn(tarea_sonda_disco()); - // FASE 18 :: si la tarjeta de red se monto, una tarea le envia un ARP - // request al gateway y registra por COM1 los paquetes entrantes. + // FASE 18 :: si la tarjeta de red se monto, una tarea corta envia un ARP + // al gateway para anunciarse en capa-3. El oficio AoE (Fase 20) — demuxer + // de entrada + faro periodico — va clavado al tic del compositor. if let Ok(mac) = mac_red { ejecutor.spawn(tarea_red(mac)); } diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs index 66763d2..d6cee68 100644 --- a/renaser/kernel/src/wasm/env.rs +++ b/renaser/kernel/src/wasm/env.rs @@ -18,7 +18,9 @@ // * sys_tono — hacer sonar la bocina del PC (Fase 12); // * sys_net_mac — leer la MAC de la tarjeta de red (Fase 19); // * sys_net_enviar — enviar un frame Ethernet crudo (Fase 19); -// * sys_net_recibir — leer el siguiente frame recibido (Fase 19). +// * sys_net_recibir — leer el siguiente frame recibido (Fase 19; +// desde la Fase 20, los frames Akasha se filtran +// en el kernel y no llegan al userspace). // // GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le // entrega contra los limites reales de su memoria lineal. No se confia en que @@ -530,10 +532,14 @@ pub(crate) fn enlazar_capacidades( )?; // --- CAPACIDAD 14 :: sys_net_recibir(salida, capacidad) -> i32 --- - // Saca el siguiente frame de la cola RX del dispositivo y lo copia en - // `salida`. Devuelve los bytes copiados (>0), 0 si no hay frame pendiente, - // o -1 si no hay red montada. La cola RX es del dispositivo y se comparte - // entre los apps: el primero que pregunte se lleva el paquete. + // Saca el siguiente frame de la cola del USUARIO y lo copia en `salida`. + // Desde la Fase 20, esa cola la rellena el demultiplexor del kernel + // (`akasha::drenar_y_demultiplexar`): los frames Akasha (`0x88B5` con + // payload valido) se procesan en el nucleo y NO llegan aqui; el resto + // del trafico —ARP, IPv4 de QEMU, futuros protocolos— si. Devuelve los + // bytes copiados (>0), 0 si no hay frame pendiente, o -1 si no hay red + // montada. La cola se vacia FIFO; si un app no llama nunca, los frames + // mas antiguos se descartan al desbordar (ver `akasha::COLA_USUARIO`). enlazador.func_wrap( "renaser", "sys_net_recibir", @@ -555,10 +561,10 @@ pub(crate) fn enlazar_capacidades( "WASM :: sys_net_recibir desbordo la memoria lineal", )?; } - // Bufer kernel-side donde el driver vuelca el frame; luego se copia - // a la memoria del app en una sola pasada. + // Bufer kernel-side donde la cola del usuario vuelca el frame; luego + // se copia a la memoria del app en una sola pasada. let mut buf: alloc::vec::Vec = alloc::vec![0u8; capacidad as usize]; - let n = crate::drivers::red::recibir_en(&mut buf); + let n = crate::akasha::pop_usuario(&mut buf); if n == 0 { return Ok(0); }