Graven

Um formato binário para dados que precisam durar.

Graven é uma especificação aberta para um formato de serialização zero-copy e sua linguagem de schema. As mensagens são lidas no lugar, direto do buffer. O formato está congelado e totalmente especificado, e seus vetores de conformidade permitem que uma implementação em qualquer linguagem prove que lê cada mensagem corretamente.

Inspirado no FlatBuffers e no Cap'n Proto. Partimos das ideias deles, medimos seus trade-offs em benchmarks e seguimos explorando até chegar a este formato. Como chegamos aqui

Formato v1 congelado. Implementação de referência em Rust. Licença MIT.

A especificação e a documentação técnica estão em inglês.

Uma mensagem, byte a byte
namespace bank;

table Entry {
  account @0: u64;
  amount  @1: i64;
  memo    @2: string?;
}

    Feito para dados que duram mais que o código que os escreveu

    Dados de negócio são escritos uma vez e lidos por anos: pela próxima versão, por um serviço que só atualiza depois, por uma auditoria muito tempo após a equipe original ter seguido em frente. O Graven foi pensado para essa realidade.

    Leitura no lugar
    Os campos são lidos direto dos bytes. Strings e fatias de bytes são emprestadas do buffer, então ler um registro grande não custa nada até você tocar num campo.
    Evoluir sem adivinhar
    Um lockfile registra cada layout. O compilador recusa mudanças que fariam código antigo e novo discordarem, e pede confirmação explícita antes de qualquer coisa quebrar um leitor antigo.
    Bytes não confiáveis podem ser abertos com segurança
    Uma única passada de verify percorre a mensagem inteira com limites fixos de profundidade e de trabalho. Depois disso, a view verificada lê sem checar de novo, e entrada malformada só consegue produzir um erro.
    Saber o que aconteceu com um arquivo
    O envelope opcional carrega um schema id e um CRC32C. Uma escrita interrompida por uma queda é reportada como incompleta; um bit trocado é reportado como corrupção. Os dois nunca se confundem.

    Como o formato se mantém confiável

    Cada afirmação da especificação é garantida por algo que roda. Isto é o que é verificado, e como.

    GarantiaComo é verificada
    O formato não muda por acidente Uma especificação normativa (linguagem RFC 2119) e 42 vetores de conformidade congelados, cada um um .bin com seu resultado esperado. O writer de referência precisa reproduzir os vetores canônicos byte a byte.
    Duas implementações concordam Dentro da implementação de referência, um interpretador independente guiado pelo schema como dado e o código gerado leem cada vetor e cada mutação de um byte dele. Os dois precisam aceitar, recusar e decodificar da mesma forma. Toda nova implementação é cobrada pelos mesmos vetores.
    Entrada hostil fica limitada verify limita a profundidade a 64 e o trabalho total a max(len × 8, 1 MiB), então ciclos e amplificação por dados compartilhados são recusados. Cinco alvos de fuzzing rodaram mais de 80 milhões de entradas com checagem de overflow ligada, sem nenhum achado.
    O caminho rápido com unsafe é correto Na implementação em Rust, o único código unsafe pula a revalidação de UTF-8 depois do verify. Suas invariantes estão documentadas, são cobradas por um teste de mutação, e o runtime e o código gerado rodam sob o Miri sem nenhum comportamento indefinido reportado.
    Mudanças de schema não corrompem em silêncio O compilador compara cada mudança com o lockfile: 15 regras recusam mudanças que leriam dados antigos errado, e 2 avisos exigem confirmação no schema. Um corpus de cadeias de versões testa cada regra.
    Queda e corrupção são diferenciadas A ordem de checagem do envelope é normativa, então um final truncado e um registro danificado produzem erros diferentes e previsíveis em todas as implementações.

    Mudanças de schema são revisadas pelo compilador

    Mude um tipo ou acrescente um campo obrigatório, e o gravenc update recusa, citando a regra e a linha. Acrescente o campo com um default, e o novo lock é escrito. Esta é a saída real.

    Recusado

    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)

    Aceito

    table Entry {
      account  @0: u64;
      amount   @1: i64;
      memo     @2: string?;
      currency @3: u16 = 986;
    }
    $ gravenc update ledger.graven
    ledger.graven.lock: written

    Para que serve

    O Graven se encaixa onde registros são escritos uma vez, guardados por muito tempo e lidos por software que muda.

    Livros-razão e logs de eventos
    Acrescente envelopes a um arquivo e nunca os reescreva. Cada registro carrega seu schema id e checksum, uma queda deixa um final reconhecidamente incompleto, e relatórios podem varrer o arquivo sem desserializá-lo.
    Protocolos de serviços e SDKs
    Roteie requisições pelo schema id, verifique uma vez na entrada e repasse a view verificada sem copiar. As regras de compatibilidade garantem que um cliente antigo continue funcionando quando o servidor avança.
    Arquivos e trilhas de auditoria
    Um formato congelado com vetores de conformidade públicos é algo que você ainda consegue decodificar daqui a dez anos, com outra implementação se for preciso.
    Dispositivos e sincronização offline
    O formato não precisa de alocador nem de alinhamento, e o runtime em Rust é no_std com duas dependências, então os mesmos registros podem vir de terminais de ponto de venda e dispositivos embarcados que os acumulam enquanto estão offline.
    Caches e valores armazenados
    Guarde registros num banco chave-valor e leia só o campo de que precisa, direto dos bytes armazenados.
    Fronteiras com plugins
    Troque buffers com extensões não confiáveis, como módulos WebAssembly, e deixe o verify proteger o host de qualquer coisa que elas devolvam.

    Implementações

    A especificação é o produto; as implementações são medidas por ela. Uma implementação conforme lê os 42 vetores com exatamente o resultado esperado (spec §10).

    LinguagemStatusO que inclui
    Rust Implementação de referência. Passa em todos os vetores v1. Runtime (no_std), o compilador gravenc com lockfile e checagem de compatibilidade, geração de código Rust e o interpretador de conformidade. Referência da API

    Escrevendo uma implementação

    Comece pelo algoritmo de layout (spec §3.3), pelas regras de leitura (§4–§6) e pelo verify (§7), e então rode os vetores de conformidade. Cada vetor é um .bin com um .json que traz o valor decodificado ou o erro exato, e cada vetor inválido tem um único defeito, então uma falha aponta para uma única regra.

    Gerando código para outra linguagem

    A linguagem de schema e suas regras de compatibilidade não dependem de linguagem. O lockfile escrito pelo gravenc já registra o layout, o schema id e a presença dos campos de cada tabela, que é o que um gerador de código para outra linguagem precisa para emitir leitores e writers.

    Primeiros passos com Rust

    Usar a implementação de referência exige só o toolchain do Rust.

    1. Instale o compilador

      cargo install --path crates/graven-compiler
    2. Escreva um schema e crie seu lockfile

      gravenc update schemas/bank.graven --init
    3. Gere o código no build.rs

      graven_compiler::build::Build::new()
          .schema("schemas/bank.graven")
          .compile()
          .unwrap_or_else(|e| panic!("{e}"));
    4. Escreva e leia

      let bytes = TransactionW::to_bytes(&mut b, |t| {
          t.id(42).currency(Currency::USD);
      })?;
      let tx = Transaction::from_bytes(&bytes)?;
      println!("{}", tx.id());

    O guia de primeiros passos e a referência da linguagem de schema (em inglês) cobrem o fluxo completo.