Un formato binario para datos que tienen que durar.
Graven es una especificación abierta para un formato de serialización zero-copy y su lenguaje de esquemas. Los mensajes se leen en su lugar, directamente del buffer. El formato está congelado y completamente especificado, y sus vectores de conformidad permiten que una implementación en cualquier lenguaje demuestre que lee cada mensaje correctamente.
Inspirado en FlatBuffers y Cap'n Proto. Partimos de sus ideas, medimos sus compromisos con benchmarks y seguimos explorando hasta llegar a este formato. Cómo llegamos aquí
Formato v1 congelado. Implementación de referencia en Rust. Licencia MIT.
La especificación y la documentación técnica están en inglés.
namespace bank; table Entry { account @0: u64; amount @1: i64; memo @2: string?; }
Hecho para datos que sobreviven al código que los escribió
Los datos de negocio se escriben una vez y se leen durante años: por la próxima versión, por un servicio que se actualiza más tarde, por una auditoría mucho después de que el equipo original haya cambiado. Graven está diseñado para esa realidad.
- Lectura en su lugar
- Los campos se leen directamente de los bytes. Las cadenas y los segmentos de bytes se toman prestados del buffer, así que leer un registro grande no cuesta nada hasta que tocas un campo.
- Evolucionar sin adivinar
- Un lockfile registra cada layout. El compilador rechaza los cambios que harían que el código antiguo y el nuevo no coincidan, y pide confirmación explícita antes de que algo rompa a un lector antiguo.
- Los bytes no confiables se abren con seguridad
- Una sola pasada de
verifyrecorre todo el mensaje con límites fijos de profundidad y de trabajo. Después, una vista verificada lee sin volver a comprobar, y una entrada mal formada solo puede producir un error. - Saber qué le pasó a un archivo
- El sobre opcional lleva un schema id y un CRC32C. Una escritura interrumpida por una caída se informa como incompleta; un bit alterado se informa como corrupción. Nunca se confunden.
Cómo se mantiene fiable el formato
Cada afirmación de la especificación está respaldada por algo que se ejecuta. Esto es lo que se comprueba, y cómo.
| Garantía | Cómo se comprueba |
|---|---|
| El formato no cambia por accidente | Una especificación normativa (lenguaje RFC 2119) y 42 vectores de conformidad congelados, cada uno un .bin
con su resultado esperado. El escritor de referencia debe reproducir los vectores canónicos byte a byte. |
| Dos implementaciones coinciden | Dentro de la implementación de referencia, un intérprete independiente guiado por el esquema como dato y el código generado leen cada vector y cada mutación de un byte. Deben aceptar, rechazar y decodificar igual. Toda nueva implementación se mide con los mismos vectores. |
| La entrada hostil queda acotada | verify limita la profundidad a 64 y el trabajo total a max(len × 8, 1 MiB), así que los ciclos y la
amplificación por datos compartidos se rechazan. Cinco objetivos de fuzzing ejecutaron más de 80 millones de entradas
con comprobación de desbordamiento activada, sin hallazgos. |
| El camino rápido con unsafe es correcto | En la implementación en Rust, el único código unsafe omite la revalidación de UTF-8 después de verify. Sus invariantes
están documentadas, se hacen cumplir con una prueba de mutación, y el runtime y el código generado se ejecutan bajo Miri sin
ningún comportamiento indefinido reportado. |
| Los cambios de esquema no corrompen en silencio | El compilador compara cada cambio con el lockfile: 15 reglas rechazan los cambios que leerían mal datos antiguos, y 2 avisos exigen confirmación en el esquema. Un corpus de cadenas de versiones prueba cada regla. |
| Caídas y corrupción se distinguen | El orden de comprobación del sobre es normativo, así que un final truncado y un registro dañado producen errores distintos y predecibles en todas las implementaciones. |
El compilador revisa los cambios de esquema
Cambia un tipo o añade un campo obligatorio, y gravenc update lo rechaza, indicando la regla y la línea.
Añade el campo con un valor por defecto, y se escribe el nuevo lock. Esta es la salida real.
Rechazado
table Entry {
account @0: u64;
amount @1: i32;
memo @2: string?;
currency @3: u16;
}
$ gravenc update ledger.graven
ledger.graven:5:3: field `Entry.amount` (@1): type changed
from `i64` to `i32` (type-changed)
ledger.graven:7:3: new field `Entry.currency` (@3) is
required: every new reader would fail on old messages;
use `u16?` or a default (new-required)
Aceptado
table Entry {
account @0: u64;
amount @1: i64;
memo @2: string?;
currency @3: u16 = 986;
}
$ gravenc update ledger.graven
ledger.graven.lock: written
Para qué sirve
Graven encaja donde los registros se escriben una vez, se guardan mucho tiempo y los lee software que cambia.
- Libros contables y logs de eventos
- Añade sobres a un archivo y nunca los reescribas. Cada registro lleva su schema id y su checksum, una caída deja un final detectablemente incompleto, y los informes pueden recorrer el archivo sin deserializarlo.
- Protocolos de servicios y SDKs
- Enruta las peticiones por schema id, verifica una vez en la entrada y pasa la vista verificada sin copiar. Las reglas de compatibilidad hacen que un cliente antiguo siga funcionando cuando el servidor avanza.
- Archivos y registros de auditoría
- Un formato congelado con vectores de conformidad públicos es algo que todavía podrás decodificar dentro de diez años, con otra implementación si hace falta.
- Dispositivos y sincronización sin conexión
- El formato no necesita asignador de memoria ni alineación, y el runtime en Rust es
no_stdcon dos dependencias, así que los mismos registros pueden venir de terminales de punto de venta y dispositivos embebidos que los acumulan mientras están sin conexión. - Cachés y valores almacenados
- Guarda registros en un almacén clave-valor y lee solo el campo que necesitas, directamente de los bytes guardados.
- Fronteras con plugins
- Intercambia buffers con extensiones no confiables, por ejemplo módulos WebAssembly, y deja que
verifyproteja al host de lo que devuelvan.
Implementaciones
La especificación es el producto; las implementaciones se miden con ella. Una implementación conforme lee los 42 vectores con exactamente el resultado esperado (spec §10).
| Lenguaje | Estado | Qué incluye |
|---|---|---|
| Rust | Implementación de referencia. Pasa todos los vectores v1. | Runtime (no_std), el compilador gravenc con lockfile y comprobación de
compatibilidad, generación de código Rust y el intérprete de conformidad.
Referencia de la API |
Escribir una implementación
Empieza por el algoritmo de layout (spec §3.3), las reglas de lectura (§4–§6) y verify (§7), y luego ejecuta
los vectores de conformidad. Cada vector es un .bin con un
.json que indica el valor decodificado o el error exacto, y cada vector inválido tiene un único defecto,
así que un fallo apunta a una sola regla.
Generar código para otro lenguaje
El lenguaje de esquemas y sus reglas de compatibilidad son independientes del lenguaje. El lockfile que escribe
gravenc ya registra el layout, el schema id y la presencia de los campos de cada tabla, que es lo que un
generador de código para otro lenguaje necesita para emitir lectores y escritores.
Primeros pasos con Rust
Usar la implementación de referencia solo requiere el toolchain de Rust.
-
Instala el compilador
cargo install --path crates/graven-compiler -
Escribe un esquema y crea su lockfile
gravenc update schemas/bank.graven --init -
Genera el código desde
build.rsgraven_compiler::build::Build::new() .schema("schemas/bank.graven") .compile() .unwrap_or_else(|e| panic!("{e}")); -
Escribe y lee
let bytes = TransactionW::to_bytes(&mut b, |t| { t.id(42).currency(Currency::USD); })?; let tx = Transaction::from_bytes(&bytes)?; println!("{}", tx.id());
La guía de primeros pasos y la referencia del lenguaje de esquemas (en inglés) cubren el flujo completo.