6.8 KiB
Format Specification (v0)
This document freezes the v0 formats used by civilization-ledger.
Key idea: signing and verification do not depend on CBOR serialization details. The canonical signing bytes are defined below and locked by golden-vector tests.
Entry (v0)
Fields
EntryUnsigned (the data that is signed):
prev_hash: 32 bytes ([u8;32]) — previous entry hash, or00…00for genesists_ms:u64— Unix epoch millisecondsnamespace: UTF-8 stringpayload_cbor: bytes — opaque payload (the ledger never interprets it)author_pubkey: 32 bytes ([u8;32]) — Ed25519 verifying key
Entry adds:
sig: 64 bytes ([u8;64]) — Ed25519 signature oversigning_message_v0
Signing bytes (v0)
signing_message_v0 = (concatenation, in order):
- Domain separator: ASCII
CLv0(4 bytes) prev_hash(32 bytes)ts_msas little-endianu64(8 bytes)namespace_lenas little-endianu32(4 bytes)namespacebytes (namespace_lenbytes)payload_hash = BLAKE3(payload_cbor)(32 bytes)author_pubkey(32 bytes)
This is implemented in civilization-ledger/crates/ledger-core/src/entry.rs:32 and locked by civilization-ledger/crates/ledger-core/tests/golden_vectors.rs:10.
Entry hash (v0)
entry_hash = BLAKE3( … ) over:
- Domain separator
CL-entry-v0 prev_hashts_ms(little-endianu64)namespace_len(little-endianu32)namespacebytespayload_hash = BLAKE3(payload_cbor)author_pubkeysig_len(little-endianu32, always64for v0)sigbytes (64)
This hash is the hash-chain link and the Merkle leaf value.
Merkle (v0)
Leaves are entry hashes ([u8;32]).
- Empty tree root:
BLAKE3("CL-merkle-empty-v0") - Leaf compression:
leaf = BLAKE3("CL-merkle-leaf-v0" || entry_hash) - Node compression:
node = BLAKE3("CL-merkle-node-v0" || left || right) - If a level has an odd number of nodes, the last node is duplicated (
right = left).
The Merkle root is computed over the prefix of entries included in a checkpoint.
Checkpoints file (v0)
log/checkpoints.jsonl is JSON Lines. Each line is a Checkpoint:
ts_ms(u64)entry_count(u64) — number of entries covered by this checkpointmerkle_root_hex(64 hex chars)head_hash_hex(64 hex chars) — the hash of entryentry_count-1(or00…00ifentry_count == 0)- optional witness fields:
witness_pubkey_hexwitness_sig_hex
Checkpoint Attestations (v0, v1)
Checkpoint attestations are independent witness signatures over a checkpoint root.
File: log/checkpoints.attestations.jsonl (JSON Lines). Each line is a CheckpointAttestationV0
or CheckpointAttestationV1, distinguished by the format field.
format: must equalciv-ledger-checkpoint-attest-v0ledger_genesis_hash_hex: 64 hex chars — the first entry hash (ledger identity anchor)checkpoint_entry_count:u64— number of entries covered by the checkpointcheckpoint_merkle_root_hex: 64 hex charscheckpoint_head_hash_hex: 64 hex chars — the hash of entrycheckpoint_entry_count-1(or00…00ifcheckpoint_entry_count == 0)ts_seen_ms:u64— witness observation time (Unix epoch milliseconds)witness_pubkey_hex: 64 hex chars — Ed25519 public keywitness_sig_hex: 128 hex chars — Ed25519 signature over the signing bytes below
Attestation signing bytes (v0)
attestation_signing_message_v0 = (concatenation, in order):
- Domain separator: ASCII
CIV_LEDGER_CHECKPOINT_ATTEST_V0 ledger_genesis_hash(32 bytes)checkpoint_entry_countas little-endianu64(8 bytes)checkpoint_merkle_root(32 bytes)checkpoint_head_hash(32 bytes)ts_seen_msas little-endianu64(8 bytes)
This is implemented in civilization-ledger/crates/ledger-core/src/attestation.rs and locked by
civilization-ledger/crates/ledger-core/tests/attestation_vectors.rs.
Checkpoint Attestation (v1)
CheckpointAttestationV1 adds one field to bind the witness signature to the specific checkpoint
record timestamp:
format: must equalciv-ledger-checkpoint-attest-v1- all fields from v0, plus:
checkpoint_ts_ms:u64— theCheckpoint.ts_msvalue being attested
Policy:
ts_seen_ms >= checkpoint_ts_ms
Attestation signing bytes (v1)
attestation_signing_message_v1 = (concatenation, in order):
- Domain separator: ASCII
CIV_LEDGER_CHECKPOINT_ATTEST_V1 ledger_genesis_hash(32 bytes)checkpoint_entry_countas little-endianu64(8 bytes)checkpoint_merkle_root(32 bytes)checkpoint_head_hash(32 bytes)checkpoint_ts_msas little-endianu64(8 bytes)ts_seen_msas little-endianu64(8 bytes)
ReadProof / Receipt (v0)
Read proofs are self-contained JSON objects of type ReadProofV0:
format: must equalciv-ledger-readproof-v0entry_hash_hex: 64 hex chars (the entry hash being proven)entry_index: 0-based index of the entry within the checkpoint prefixentry_count: number of entries in the checkpoint prefixcheckpoint_merkle_root_hex: 64 hex charspath: array of Merkle steps, each with:sibling_side:"left"or"right"(position of the sibling relative to the current hash)sibling_hash_hex: 64 hex chars (the sibling hash at that Merkle level)
Verification:
current = BLAKE3("CL-merkle-leaf-v0" || entry_hash)- For each path step:
- if
sibling_side == "left":current = BLAKE3("CL-merkle-node-v0" || sibling || current) - if
sibling_side == "right":current = BLAKE3("CL-merkle-node-v0" || current || sibling)
- if
- Accept iff
current == checkpoint_merkle_root
This is implemented in civilization-ledger/crates/ledger-core/src/proof.rs.
Receipt (v0)
Receipts bundle: entry bytes + inclusion proof + (optional) witness attestations.
JSON object ReceiptV0:
format: must equalciv-ledger-receipt-v0entry_cbor_b64: base64 (no padding) of CBOR-encodedEntryentry_hash_hex: 64 hex charsread_proof: aReadProofV0attestations: array ofCheckpointAttestationV0orCheckpointAttestationV1(may be empty)
Verification:
- Decode
entry_cbor_b64→Entry, verify the entry signature, and computeentry_hash. - Verify
read_proofand requireread_proof.entry_hash_hex == entry_hash_hex. - If witness is required: verify at least one included checkpoint attestation (v0 or v1) and
require it matches
read_proof.entry_count+read_proof.checkpoint_merkle_root_hex.
This is implemented in civilization-ledger/crates/ledger-core/src/receipt.rs.
Payload Type: file_anchor.v0
The ledger anchor-file CLI emits a CBOR payload with the following JSON shape:
type:"file_anchor.v0"path: string (preferably repo-relative)hash_blake3_hex: 64 hex charsbytes:u64git:commit: string or nulldirty: boolean or null