Graven

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.

Un mensaje, byte a byte
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 verify recorre 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íaCó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_std con 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 verify proteja 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).

    LenguajeEstadoQué 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.

    1. Instala el compilador

      cargo install --path crates/graven-compiler
    2. Escribe un esquema y crea su lockfile

      gravenc update schemas/bank.graven --init
    3. Genera el código desde build.rs

      graven_compiler::build::Build::new()
          .schema("schemas/bank.graven")
          .compile()
          .unwrap_or_else(|e| panic!("{e}"));
    4. 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.