chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):
- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial
Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.
cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
//! Almacén persistente de atestaciones firmadas.
|
||||
//!
|
||||
//! Layout: una sola `sled::Tree` cuya clave es la concatenación
|
||||
//! `content_hash || author_did` (64 bytes) y cuyo valor es la
|
||||
//! `Attestation` serializada. Esto permite:
|
||||
//! - Idempotencia natural: misma `(autor, contenido)` = misma clave.
|
||||
//! - Listar todas las atestaciones de un contenido vía `scan_prefix`
|
||||
//! con los primeros 32 bytes (el `ContentHash`).
|
||||
//!
|
||||
//! `add` re-verifica criptográficamente cada atestación antes de
|
||||
//! persistirla — el contrato es idéntico al de `AttestationStore` en
|
||||
//! memoria: jamás se almacenan firmas inválidas.
|
||||
|
||||
use minga_core::{Attestation, AttestationError, ContentHash, Did};
|
||||
use sled::{Db, Tree};
|
||||
|
||||
use crate::error::StoreError;
|
||||
|
||||
pub struct SledAttestationStore {
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl SledAttestationStore {
|
||||
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
|
||||
Ok(Self {
|
||||
tree: db.open_tree(name)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add(&self, att: Attestation) -> Result<(), StoreError> {
|
||||
if !att.verify() {
|
||||
return Err(StoreError::Attestation(AttestationError::InvalidSignature));
|
||||
}
|
||||
let key = compose_key(&att.content, &att.author);
|
||||
let bytes = postcard::to_allocvec(&att)?;
|
||||
self.tree.insert(&key, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Devuelve todas las atestaciones para `content` (vacío si
|
||||
/// ninguna). Orden no especificado.
|
||||
pub fn get(&self, content: &ContentHash) -> Result<Vec<Attestation>, StoreError> {
|
||||
let mut out = Vec::new();
|
||||
for kv in self.tree.scan_prefix(&content.0) {
|
||||
let (_k, v) = kv?;
|
||||
out.push(postcard::from_bytes(&v)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn authors_of(&self, content: &ContentHash) -> Result<Vec<Did>, StoreError> {
|
||||
Ok(self.get(content)?.into_iter().map(|a| a.author).collect())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.tree.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn flush(&self) -> Result<(), StoreError> {
|
||||
self.tree.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Itera todas las atestaciones persistidas. Cargando un peer al
|
||||
/// arrancar, esto repuebla el `AttestationStore` en memoria.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<Attestation, StoreError>> + '_ {
|
||||
self.tree.iter().map(|kv| {
|
||||
let (_k, v) = kv?;
|
||||
Ok(postcard::from_bytes(&v)?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_key(content: &ContentHash, author: &Did) -> [u8; 64] {
|
||||
let mut k = [0u8; 64];
|
||||
k[..32].copy_from_slice(&content.0);
|
||||
k[32..].copy_from_slice(&author.0);
|
||||
k
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use minga_core::AttestationError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StoreError {
|
||||
#[error("sled: {0}")]
|
||||
Sled(#[from] sled::Error),
|
||||
|
||||
#[error("postcard: {0}")]
|
||||
Postcard(#[from] postcard::Error),
|
||||
|
||||
#[error("attestation: {0}")]
|
||||
Attestation(#[from] AttestationError),
|
||||
|
||||
#[error("hash inconsistente con el contenido del nodo")]
|
||||
HashMismatch,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Persistencia en disco de keypairs cifrados.
|
||||
//!
|
||||
//! El cifrado en sí (AES-GCM + Argon2id) vive en `minga-core`, que es
|
||||
//! pure logic. Aquí solo se monta la parte de IO: leer/escribir
|
||||
//! bytes a un archivo.
|
||||
//!
|
||||
//! Layout del archivo: el blob crudo que produce
|
||||
//! `Keypair::encrypt(passphrase)`. 85 bytes total.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use minga_core::{Keypair, KeypairCryptoError};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KeypairFileError {
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("crypto: {0}")]
|
||||
Crypto(#[from] KeypairCryptoError),
|
||||
}
|
||||
|
||||
/// Guarda un keypair cifrado con la passphrase en `path`. Si el
|
||||
/// archivo ya existe, lo sobrescribe.
|
||||
pub fn save<P: AsRef<Path>>(
|
||||
keypair: &Keypair,
|
||||
path: P,
|
||||
passphrase: &str,
|
||||
) -> Result<(), KeypairFileError> {
|
||||
let blob = keypair.encrypt(passphrase)?;
|
||||
fs::write(path, blob)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Carga un keypair desde un archivo cifrado.
|
||||
pub fn load<P: AsRef<Path>>(path: P, passphrase: &str) -> Result<Keypair, KeypairFileError> {
|
||||
let blob = fs::read(path)?;
|
||||
Ok(Keypair::decrypt(&blob, passphrase)?)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! `minga-store`: backing persistente con `sled` para los stores de Minga.
|
||||
//!
|
||||
//! Tres stores paralelos a los de `minga-core`:
|
||||
//! - [`SledNodeStore`]: hashes → `StoredNode`s, equivalente persistente
|
||||
//! de `MemStore`.
|
||||
//! - [`SledAttestationStore`]: pruebas criptográficas de autoría
|
||||
//! indexadas por content hash.
|
||||
//! - [`SledMstStore`]: conjunto de claves del MST. La estructura
|
||||
//! probabilística del MST se reconstruye en memoria al cargar
|
||||
//! ([`SledMstStore::to_in_memory`]) — solo persistimos las claves
|
||||
//! porque el árbol es deterministicamente derivable de ellas.
|
||||
//!
|
||||
//! Una `PersistentRepo` agrupa los tres sobre una única `sled::Db`
|
||||
//! (tres trees con namespaces separados).
|
||||
//!
|
||||
//! El núcleo (`minga-core`) sigue siendo agnóstico de IO: estos tipos
|
||||
//! tienen APIs paralelas (devuelven `Result`, deserializan vía
|
||||
//! postcard) y los protocolos de sync se quedan operando sobre los
|
||||
//! tipos in-memory. La integración con `MingaPeer` (que hoy usa
|
||||
//! `MemStore` concreto) llegará tras un trait genérico — esta
|
||||
//! iteración se centra en que la capa de persistencia esté correcta
|
||||
//! y testeada.
|
||||
|
||||
pub mod attestation_store;
|
||||
pub mod error;
|
||||
pub mod keypair_file;
|
||||
pub mod mst_store;
|
||||
pub mod node_store;
|
||||
pub mod repo;
|
||||
|
||||
pub use attestation_store::SledAttestationStore;
|
||||
pub use error::StoreError;
|
||||
pub use keypair_file::KeypairFileError;
|
||||
pub use mst_store::SledMstStore;
|
||||
pub use node_store::SledNodeStore;
|
||||
pub use repo::PersistentRepo;
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Persistencia del MST.
|
||||
//!
|
||||
//! Solo persistimos las **claves** (los `ContentHash`es del conjunto).
|
||||
//! La estructura probabilística del MST (niveles, separadores,
|
||||
//! árbol de Merkle) es derivable determinísticamente de las claves,
|
||||
//! así que reconstruirla en memoria al cargar es trivial.
|
||||
//!
|
||||
//! Layout: una `sled::Tree` cuyas claves son los 32 bytes del hash y
|
||||
//! cuyos valores son vacíos. Los hashes se ordenan automáticamente
|
||||
//! por sled (orden lexicográfico = orden por bytes), lo que coincide
|
||||
//! con el orden que `Mst::iter` produce.
|
||||
|
||||
use minga_core::{ContentHash, Mst};
|
||||
use sled::{Db, Tree};
|
||||
|
||||
use crate::error::StoreError;
|
||||
|
||||
pub struct SledMstStore {
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl SledMstStore {
|
||||
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
|
||||
Ok(Self {
|
||||
tree: db.open_tree(name)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert(&self, h: ContentHash) -> Result<bool, StoreError> {
|
||||
let prev = self.tree.insert(h.0, &[])?;
|
||||
Ok(prev.is_none())
|
||||
}
|
||||
|
||||
pub fn contains(&self, h: &ContentHash) -> Result<bool, StoreError> {
|
||||
Ok(self.tree.contains_key(h.0)?)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.tree.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
/// Itera todas las claves del MST en orden ascendente por hash.
|
||||
pub fn iter(&self) -> impl Iterator<Item = Result<ContentHash, StoreError>> + '_ {
|
||||
self.tree.iter().map(|kv| {
|
||||
let (k, _) = kv?;
|
||||
if k.len() != 32 {
|
||||
return Err(StoreError::HashMismatch);
|
||||
}
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes.copy_from_slice(&k);
|
||||
Ok(ContentHash(bytes))
|
||||
})
|
||||
}
|
||||
|
||||
/// Reconstruye un `Mst` en memoria a partir de las claves
|
||||
/// persistidas. Útil al arrancar un peer: cargamos las claves
|
||||
/// del disco y rehacemos la estructura para operaciones rápidas.
|
||||
pub fn to_in_memory(&self) -> Result<Mst, StoreError> {
|
||||
let mut mst = Mst::new();
|
||||
for h in self.iter() {
|
||||
mst.insert(h?);
|
||||
}
|
||||
Ok(mst)
|
||||
}
|
||||
|
||||
pub fn flush(&self) -> Result<(), StoreError> {
|
||||
self.tree.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//! Almacén persistente de `StoredNode`s indexados por `ContentHash`.
|
||||
//!
|
||||
//! Cada nodo se serializa con postcard y se inserta en una `sled::Tree`
|
||||
//! cuya clave son los 32 bytes del hash. La operación `put` es
|
||||
//! recursiva sobre los hijos (igual que `MemStore::put`): cada
|
||||
//! subárbol se hashea y persiste exactamente una vez.
|
||||
|
||||
use minga_core::{cas, hash_stored, ContentHash, SemanticNode, StoredNode};
|
||||
use sled::{Db, Tree};
|
||||
|
||||
use crate::error::StoreError;
|
||||
|
||||
pub struct SledNodeStore {
|
||||
tree: Tree,
|
||||
}
|
||||
|
||||
impl SledNodeStore {
|
||||
pub fn open_tree(db: &Db, name: &str) -> Result<Self, StoreError> {
|
||||
Ok(Self {
|
||||
tree: db.open_tree(name)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserta un árbol completo. Recursivamente desempaqueta hijos.
|
||||
/// Devuelve el hash de la raíz. Idempotente: insertar el mismo
|
||||
/// árbol dos veces no añade entradas nuevas.
|
||||
pub fn put(&self, node: &SemanticNode) -> Result<ContentHash, StoreError> {
|
||||
let mut child_hashes = Vec::with_capacity(node.children.len());
|
||||
for c in &node.children {
|
||||
child_hashes.push(self.put(c)?);
|
||||
}
|
||||
let h = cas::hash_components(
|
||||
&node.kind,
|
||||
node.field_name.as_deref(),
|
||||
node.leaf_text.as_deref(),
|
||||
&child_hashes,
|
||||
);
|
||||
if !self.tree.contains_key(h.0)? {
|
||||
let stored = StoredNode {
|
||||
kind: node.kind.clone(),
|
||||
field_name: node.field_name.clone(),
|
||||
leaf_text: node.leaf_text.clone(),
|
||||
children: child_hashes,
|
||||
};
|
||||
let bytes = postcard::to_allocvec(&stored)?;
|
||||
self.tree.insert(h.0, bytes)?;
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
/// Inserta un nodo ya troceado por hash. Verifica que el hash
|
||||
/// coincida con `hash_stored(stored)` antes de insertar — sin
|
||||
/// esa verificación no podemos confiar en la integridad de lo
|
||||
/// que viene del wire.
|
||||
pub fn put_chunked(
|
||||
&self,
|
||||
hash: ContentHash,
|
||||
stored: &StoredNode,
|
||||
) -> Result<(), StoreError> {
|
||||
if hash_stored(stored) != hash {
|
||||
return Err(StoreError::HashMismatch);
|
||||
}
|
||||
if !self.tree.contains_key(hash.0)? {
|
||||
let bytes = postcard::to_allocvec(stored)?;
|
||||
self.tree.insert(hash.0, bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, h: &ContentHash) -> Result<Option<StoredNode>, StoreError> {
|
||||
match self.tree.get(h.0)? {
|
||||
Some(bytes) => Ok(Some(postcard::from_bytes(&bytes)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, h: &ContentHash) -> Result<bool, StoreError> {
|
||||
Ok(self.tree.contains_key(h.0)?)
|
||||
}
|
||||
|
||||
/// Reconstruye un `SemanticNode` resolviendo recursivamente todos
|
||||
/// los hijos. `Ok(None)` si algún hash no está en el store
|
||||
/// (almacén incompleto).
|
||||
pub fn reconstruct(&self, h: &ContentHash) -> Result<Option<SemanticNode>, StoreError> {
|
||||
let stored = match self.get(h)? {
|
||||
Some(s) => s,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let mut children = Vec::with_capacity(stored.children.len());
|
||||
for ch in &stored.children {
|
||||
match self.reconstruct(ch)? {
|
||||
Some(n) => children.push(n),
|
||||
None => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok(Some(SemanticNode {
|
||||
kind: stored.kind,
|
||||
field_name: stored.field_name,
|
||||
leaf_text: stored.leaf_text,
|
||||
children,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.tree.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn flush(&self) -> Result<(), StoreError> {
|
||||
self.tree.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Itera todos los pares `(hash, stored_node)` persistidos. Sin
|
||||
/// orden garantizado más allá del lexicográfico de sled. Usado al
|
||||
/// arrancar para volcar el contenido a un `MemStore` en memoria.
|
||||
pub fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<(ContentHash, StoredNode), StoreError>> + '_ {
|
||||
self.tree.iter().map(|kv| {
|
||||
let (k, v) = kv?;
|
||||
if k.len() != 32 {
|
||||
return Err(StoreError::HashMismatch);
|
||||
}
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes.copy_from_slice(&k);
|
||||
let stored: StoredNode = postcard::from_bytes(&v)?;
|
||||
Ok((ContentHash(bytes), stored))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! `PersistentRepo`: agrupa los tres stores (nodos, atestaciones, MST)
|
||||
//! sobre una única `sled::Db`. Cada store ocupa su propio tree
|
||||
//! (namespace lógico) dentro del mismo directorio en disco.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use sled::Db;
|
||||
|
||||
use crate::{
|
||||
attestation_store::SledAttestationStore, error::StoreError, mst_store::SledMstStore,
|
||||
node_store::SledNodeStore,
|
||||
};
|
||||
|
||||
pub struct PersistentRepo {
|
||||
db: Db,
|
||||
pub nodes: SledNodeStore,
|
||||
pub attestations: SledAttestationStore,
|
||||
pub mst: SledMstStore,
|
||||
}
|
||||
|
||||
impl PersistentRepo {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, StoreError> {
|
||||
let db = sled::open(path)?;
|
||||
let nodes = SledNodeStore::open_tree(&db, "nodes")?;
|
||||
let attestations = SledAttestationStore::open_tree(&db, "attestations")?;
|
||||
let mst = SledMstStore::open_tree(&db, "mst")?;
|
||||
Ok(Self {
|
||||
db,
|
||||
nodes,
|
||||
attestations,
|
||||
mst,
|
||||
})
|
||||
}
|
||||
|
||||
/// Flushea los tres trees a disco. Llamar en puntos de
|
||||
/// checkpoint o antes de cerrar para garantizar durabilidad.
|
||||
pub fn flush(&self) -> Result<(), StoreError> {
|
||||
self.db.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user