Proof format
The bitgraph/1 proof, field by field, for anyone writing a verifier, a reader or a producer that must interoperate with the reference implementation. The normative text is SPEC.md (rendered at /spec); every tree/1 proof pins its SHA-256. What a holder keeps beside a file is not the bare proof but an export, bitgraph-export/1, which carries the proof with the file’s leaf and the blocks that bound it, and SPEC.md itself.
The proof
A proof is one JSON object. The comments mark what is required; the table after the block says what each field is and whether it is signed, self-authenticating or advisory.
{ "version": "bitgraph/1", // REQUIRED - exact value "artifact": { "hashAlg": "sha256", // REQUIRED - "sha256" only in v1 "digestB64": "<base64>" // REQUIRED - SHA-256, 32 decoded bytes }, "commit": { "nonceB64": "<base64>", // REQUIRED - >=16 decoded bytes "counter": "42", // OPTIONAL - decimal string, monotonic "slotCounter": "41", // OPTIONAL - the reserved position's counter (< commit counter) "slotHashB64": "<base64>", // OPTIONAL - SHA-256 of the canonical position record "time": 1700000000000, // OPTIONAL - Unix ms "prevB64": "<base64>", // OPTIONAL - chain link, 32 bytes "epochId": "<base64>", // OPTIONAL - the epoch identifier, 32 bytes "chainId": "bitgraph:main", // OPTIONAL - the sequence this proof is on "slotFloor": { // OPTIONAL - the Base block bound into the position when it opened (since enclave v10, current) "chain": "base", "evmChainId": 8453, // Base mainnet; the EVM chain id, not commit.chainId "blockNumber": 52271417, "blockHash": "0x<hex>", // 32 bytes, lowercase: keccak-256 of the header the enclave hashed "blockTimestamp": 1791332181 // Unix seconds, read from the header }, "slotAnchor": { // OPTIONAL - EARLIER floor (enclave v7 to v9): the chain's latest Ethereum anchor when the position was allocated; never beside slotFloor "counter": "17", // counter of that anchor proof on this chain "blockNumber": 25921179, "blockHash": "0x<hex>" // 32 bytes, lowercase }, "anchor": { // OPTIONAL - on earlier Ethereum anchor proofs only: the block this proof anchors (enclave v7 to v9) "blockNumber": 25921180, "blockHash": "0x<hex>" } }, "signer": { "publicKeyB64": "<base64>", // REQUIRED - Ed25519, 32 bytes "signatureB64": "<base64>" // REQUIRED - Ed25519, 64 bytes }, "environment": { "enforcement": "measured-tee", // REQUIRED - "stub"|"hw-key"|"measured-tee" "measurement": "<opaque>", // REQUIRED - non-empty string "attestation": { // OPTIONAL "format": "aws-nitro", // REQUIRED when parent present "reportB64": "<base64>" // REQUIRED when parent present } }, "slotAllocation": { // OPTIONAL - the position record "version": "bitgraph/slot/1", "nonceB64": "<base64>", // same as commit.nonceB64 "counter": "41", // same as commit.slotCounter "epochId": "<base64>", // same as commit.epochId "publicKeyB64": "<base64>", // enclave Ed25519 key "chainId": "bitgraph:main", // OPTIONAL - same as commit.chainId "signatureB64": "<base64>" // Ed25519 over the canonical position record (every field above except this one) }, // no floor and no time: the enclave allocates without a clock, and the floor is signed at commit as commit.slotFloor (commit.slotAnchor on earlier proofs) "agency": { ... }, // OPTIONAL - legacy; present on some older proofs "attribution": { // OPTIONAL - signed; creator metadata, or the marker (below): for a tree/1 it names the spec the proof pins "name": "string", "title": "string", "message": "string" }, "metadata": { }, // OPTIONAL - NOT signed, advisory; a tree/1 carries its root document here as metadata["bitgraph-tree/1"], counted only when it hashes to the signed digest "claims": { }, // OPTIONAL - NOT signed, advisory "proofHash": "<base64>" // OPTIONAL - NOT signed; added by the ledger after signing (see commit.prevB64) }
Field by field
| Field | What it is | Class |
|---|---|---|
| version | The schema, exactly bitgraph/1. | Signed |
| artifact.hashAlg | The hash applied to the file’s bytes; only sha256 in v1. | Signed |
| artifact.digestB64 | The file’s fingerprint (SHA-256 digest), 32 bytes, standard base64. Any change to the file changes it. | Signed |
| commit.nonceB64 | The position’s nonce: at least 16 decoded bytes, 32 from the enclave’s hardware random number generator. | Signed |
| commit.counter | The commit’s position in the epoch’s sequence, a decimal string compared as a big integer. | Signed |
| commit.slotCounter | The reserved position, consumed by this commit; always below commit.counter. | Signed |
| commit.slotHashB64 | SHA-256 of the canonical position record: binds the commit to that exact record. | Signed |
| commit.time | Unix milliseconds from the enclave’s clock. Not a trusted clock; order comes from the counters. | Signed, advisory value |
| commit.prevB64 | SHA-256 of the previous proof on the sequence, canonicalized whole, after removing the two top-level fields added outside the enclave after signing (proofHash and ethereum): the link that makes the order checkable. | Signed |
| commit.epochId | The enclave lifetime that signed this proof, hex SHA-256. Changes at every restart. | Signed |
| commit.slotFloor | The floor: the Base block the enclave bound into the position when it opened, from a header it hashed and checked itself. | Signed |
| commit.slotAnchor | The earlier floor (enclave v7 to v9): the chain’s latest Ethereum anchor when the position was allocated. A proof carries one floor, never both. | Signed |
| commit.anchor | On earlier anchor proofs only: the Ethereum block this proof anchors. | Signed |
| signer.publicKeyB64 | The enclave’s Ed25519 key, 32 bytes. | Signed |
| signer.signatureB64 | Ed25519 signature over the canonical signed body, 64 bytes. | Self-authenticating |
| environment.enforcement | The tier the producer reports; measured-tee in production. Not evidence of the tier on its own. | Signed |
| environment.measurement | The enclave image’s PCR0. A verifier pins it with an allowlist. | Signed |
| environment.attestation.format | The attestation document format, aws-nitro. | Signed |
| environment.attestation.reportB64 | The hardware attestation, whose user data is the hash of this proof’s signed body. | Self-authenticating (vendor-signed) |
| slotAllocation | The position record, signed by the enclave before any digest was received; bound to the commit through commit.slotHashB64. | Self-authenticating |
| agency | Legacy actor envelope on some older proofs; its actor summary is signed, its authorization carries its own signature. | Legacy |
| attribution | A claim the submitter made (name, title, message), or the signed marker below: for a tree/1, the commitment version, the word tree/1, and the SHA-256 of the SPEC.md the proof was made under. | Signed |
| timestamps | Optional RFC 3161 tokens. Never evidence of position. | Advisory |
| metadata | Caller-supplied. A tree/1 proof carries its committed artifact here, the 84-byte root document as metadata["bitgraph-tree/1"] in hex, counted only when it hashes to the signed digest; earlier set proofs carried theirs the same way. | Advisory |
| claims | Caller-supplied. | Advisory |
Signed body
The Ed25519 signature covers the canonical serialization of a SignedBody object:
{ version: proof.version, artifact: proof.artifact, actor: proof.agency?.actor, // legacy; when present policy: proof.policy, // when present attribution: proof.attribution, // when present commit: proof.commit, // ALL fields verbatim publicKeyB64: proof.signer.publicKeyB64, enforcement: proof.environment.enforcement, measurement: proof.environment.measurement, attestationFormat: proof.environment.attestation?.format // when present }
Everything in that object is detectably invalid if altered. The attestation report carries a hash of the same body, so the hardware vouches for exactly the bytes the key signed.
proofHash, which the ledger adds after signing, is a different and narrower value: the SHA-256 of the canonical JSON of the signed body without actor and policy (that is, version, artifact, commit, publicKeyB64, enforcement, measurement, and attribution and attestationFormat when present), base64 encoded. It names the proof in the ledger and in a ceiling file. For a proof with no actor and no policy, which is every proof made today, it equals the hash of the full signed body and the attestation’s user_data; for older proofs that carry either, it does not, so the signature and the attestation are always checked against the full body.
What is not signed
| Field | Reason |
|---|---|
| signatureB64 | The signature cannot cover itself |
| attestation.reportB64 | Vendor-signed, self-authenticating separately |
| slotAllocation | Self-authenticating (own Ed25519 signature); bound via commit.slotHashB64 |
| metadata | Advisory, never trusted as a field. A tree/1 proof carries its committed artifact here, the root document under bitgraph-tree/1 in hex; a reader counts it only if it hashes to the signed artifact.digestB64. Earlier set proofs carried their manifest (set/1) or root document (set/2) the same way, and a set/2 member’s evidence may ride under bitgraph-fuse/1/member. A tree member’s own evidence, its leaf and path, never rides in the proof: it is in the export. |
| claims | Advisory, not trusted |
The position and its binding
Every proof is bound to a position allocated before it. The position record is created and signed before the file’s digest reaches the enclave, so the enclave committed to a nonce and a counter without having received the file’s digest. A verifier checks four bindings between the record and the commit:
| Binding | How |
|---|---|
| Nonce binding | commit.nonceB64 === slotAllocation.nonceB64 |
| Counter ordering | commit.slotCounter < commit.counter |
| Hash binding | commit.slotHashB64 === SHA-256(canonicalize(slotBody)) |
| Same enclave | slotAllocation.publicKeyB64 === signer.publicKeyB64 |
The position record has its own Ed25519 signature, so the enclave created it. The commit signature includes slotHashB64, which binds the proof to that exact record; a swapped position record breaks the commit signature.
Base floor
Since enclave v10 the enclave binds a public floor into every position when it opens it. The host hands it the header of the newest Base block (Coinbase’s Ethereum layer-2, a block every 2 seconds). The enclave hashes the header itself and checks it: the hash is the block hash; the block’s time is Base mainnet’s schedule for its number; the time is not after the enclave’s own clock; and the number is not below the last floor fixed on the chain, so floors never go backwards. It keeps the block beside the position and signs its number, hash and time into the proof at commit as commit.slotFloor, naming the chain, so a verifier never reads it as an Ethereum floor. It refuses to open a position on bitgraph:main without one. The position record itself (slotAllocation) does not carry the floor; the bitgraph-fuse/3 commitment derived from it does (below). Whoever presents the proof cannot move the floor, because it is inside the commit signature. The record was made after that block. Offline, a reader checks that the header hashes to slotFloor.blockHash and reads the time from it; that the block is Base’s own is one lookup on Base, by block number, comparing the hash. Coinbase runs Base’s sequencer.
Earlier: Ethereum anchor floor
Proofs made before the switch to the Base floor carry an Ethereum floor instead, and verify exactly as before. Since enclave v7 (2026-09-06) the enclave noted the chain’s latest Ethereum anchor at the moment it allocated each position, kept it beside the position inside the enclave, and signed it into the proof at commit as commit.slotAnchor. The position record itself (slotAllocation) does not carry it, and neither does a bitgraph-fuse/1 commitment derived from that record (a bitgraph-fuse/2 commitment does, below). Whoever presents the proof cannot move the floor, because it is inside the commit signature. That it was the latest anchor when the position was allocated rests on the measured enclave, the same trust root as the counter: slotAnchor.counter is below commit.slotCounter, and the anchor proof at that counter carries the same block. The commit itself is placed after the block by hash alone, since its prevB64 chain runs back through that anchor proof; bytes that carry the position commitment are placed after it through the enclave’s counter. A reader checks the time offline from the Ethereum block header: the header’s keccak must equal slotAnchor.blockHash, and the time in the block header is then a lower bound on the proof. From enclave v8 (2026-09-07) to v9 the enclave refused to sign a proof on the anchored chain without a floor, so on those proofs the field is absent only on unanchored chains.
Anchor proofs themselves carry commit.anchor, holding the block number and hash the enclave signed. The enclave wrote it only after verifying the anchor service’s Ed25519 signature over the claim against a public key baked into the enclave image, and refused the attribution name Ethereum Anchor without it. So a v7 or v8 proof whose attribution says anchor but lacks commit.anchor is not an anchor.
commit.anchor is what identifies an anchor, and where its block should be read from. The attribution name is the older test and remains valid for proofs written before v7, which carry nothing else; it is not a requirement, and an anchor is free to spend its signed attribution on something else, such as the fuse marker below.
Fused artifacts
A fused artifact is a file that carries its position commitment, written into the bytes before the file was finished. The proof is an ordinary bitgraph/1 proof: slotAllocation is the position record the producer held, commit.slotCounter its counter, commit.counter the commit position, and artifact.digestB64 the digest of the fused bytes. The signed attribution is the marker:
{ "name": "bitgraph-fuse/3", // the commitment version: bitgraph-fuse/3 now; bitgraph-fuse/1 or bitgraph-fuse/2 on earlier proofs "title": "tree/1", // "tree/1" for every BitGraph made now; a placement id or encoding id on earlier single-file proofs "message": "<base64>" // tree/1: SHA-256 of SPEC.md, standard base64, the spec this proof pins; earlier proofs: the origin digest }
The position commitment is derived from the signed position record. The raw nonce never enters the artifact. The allocation also returns the floor the enclave will sign at commit, and the producer binds that block’s hash into the commitment. Since enclave v10 the floor is a Base block and the commitment is marked bitgraph-fuse/3; from 2026-09-30 (enclave v9) until then it was an Ethereum anchor, marked bitgraph-fuse/2:
slotRecordHash = SHA-256(canonicalize(slotBody)) // = commit.slotHashB64 commitment/1 = SHA-256("bitgraph-fuse/1" || 0x00 || slotRecordHash || nonce) // nonce: 32 raw bytes commitment/2 = SHA-256("bitgraph-fuse/2" || 0x00 || slotRecordHash || nonce || floorBlockHash) // earlier: the 32 raw bytes of commit.slotAnchor.blockHash commitment/3 = SHA-256("bitgraph-fuse/3" || 0x00 || slotRecordHash || nonce || floorBlockHash) // the 32 raw bytes of commit.slotFloor.blockHash (a Base block)
A verifier chooses the formula by the signed marker name and recomputes commitment/3 from the proof’s own signed commit.slotFloor.blockHash (commitment/2 from commit.slotAnchor.blockHash), so a producer can neither tighten nor loosen the floor. A proof carrying both floors is ambiguous and binds nothing. What /2 and /3 add: a block’s hash exists only once the block does, so bytes carrying the commitment were finished after that block by the hash alone; with /1, that step rested on the enclave’s counter order (the anchor before the position). Placements, payloads and set documents are unchanged between the versions; a set’s documents keep the bitgraph-fuse/1 type and metadata key, and the commitment version is the signed attribution name. A verifier that knows only /1 sees a /2 or /3 proof as an ordinary valid proof and says nothing about the file. tree/1 under SPEC.md version 2 uses /3; under version 1, /2.
Every BitGraph made now is a tree/1, whether it holds one file or a hundred thousand: one position, each file a leaf. A leaf is the hash of the file’s committed bytes with its placement code (0x00 kept as is, 0x01 trailer/1, 0x02 container/1, 0x03 container/2), the leaves form an RFC 9162 tree, and the committed artifact is an 84-byte root document naming the root and the count; artifact.digestB64 is its hash. The proof alone commits the root. A file proves it is in the tree with its export, which carries its leaf and path (SPEC.md sections 8 and 12). A verifier that does not know the pinned spec hash answers “undetermined”, never true. Before tree/1, two or more files were a set under one position (set/1, set/2 below); those positions still verify. Registered placements say, byte for byte, where a file’s commitment sits:
| Placement | Bytes | Used for |
|---|---|---|
| trailer/1 | the original bytes, then the 8-byte magic BGFUSE01, 8 zero bytes, the 32-byte commitment | formats whose decoders ignore trailing bytes: JPEG, PNG, GIF, TIFF and TIFF-based raws, BMP, RIFF such as WebP |
| container/1 | an uncompressed ustar archive: bitgraph-fuse/manifest.json, then bitgraph-fuse/original | older artifacts; readable, no longer made |
| container/2 | the same archive with bitgraph-fuse/original first, then bitgraph-fuse/manifest.json, so the file is hashed once and the digest finished later | everything else |
| produced/1 | a canonical JSON payload naming the commitment and an optional origin digest | artifacts produced without a source file; SDK and CLI only |
| set/1 | a canonical JSON manifest listing every member’s fused digest, origin digest and placement; the manifest is the committed artifact | older sets; readable, no longer made |
| set/2 | a Merkle root document over the member rows; each member keeps its row, leaf index and inclusion path | older sets of two or more files; readable, no longer made |
| tree/1 | an 84-byte root document over the leaves; each file’s leaf and path travel in its export, never in the proof | every BitGraph made now, one file or many |
The fused bytes are transient. The original plus the proof rebuilds them byte for byte with the declared placement, and verifying that reconstruction against the leaf, and the leaf against the signed root, is the evidence. What travels with the file is its export, bitgraph-export/1: the proof, the file’s leaf and path (the owner’s export holds every leaf and name), the floor block’s header, and the Base ceiling and its Ethereum settlement once they exist. An export holds no copy of the file and no anchor proofs (earlier proofs’ anchors are read from their headers); SPEC.md travels beside it. Earlier single-file proofs traveled as a Frame file, <name>.bitgraph-fuse.json, with an advisory manifest; those still verify.
What this bound reaches: the fused bytes could not have been finished before the position was allocated. What it does not reach: the original, which can be any age; the proof says only that it existed no later than the commit.
Ceiling file
A ceiling in time is carried beside the proof, in its own file (bitgraph-ceiling/1), never inside it. Seconds after a commit, a writer process on BitGraph’s host puts a Merkle root over the new records’ proofHash values into one Base transaction. The file lets anyone check, offline, that one record is under that root and that the transaction is in a particular Base block, whose time the record existed by.
{ "version": "bitgraph-ceiling/1", "proofHash": "<base64>", // the record's proofHash (above) "leafIndex": 0, // this record's place in the batch "leafCount": 1, // the batch size "merklePath": ["0x<hex>", ...], // sibling hashes, leaf level up "root": "0x<hex>", // the batch root "anchor": { // null until the transaction is included "chainId": 8453, // Base mainnet "writer": "0x<address>", "txHash": "0x<hex>", "rawTx": "0x<hex>", // the signed EIP-1559 transaction "payload": "0x<hex>", // its data, 84 bytes (below) "blockNumber": 51979918, "blockHash": "0x<hex>", "blockTimestamp": 1790749183, "blockHeader": "0x<hex>", // RLP; hashes to blockHash "txIndex": 39, "txInclusionProof": ["0x<hex>", ...] // Merkle-Patricia proof into the header's transactionsRoot }, "status": "safe", // pending | included | safe | finalized: reported, not proven (below) "statusObserved": { ... }, // when BitGraph's Base node reported each status "floor": { // the proof's signed floor block, with its header: commit.slotFloor (Base), // or commit.slotAnchor (Ethereum) on earlier proofs, as in this example "blockNumber": 26088457, "blockHash": "0x<hex>", "blockTimestamp": 1790749163, "blockHeader": "0x<hex>" }, "settlement": null // filled in later, when Ethereum carries the block (below) }
The payload is the transaction’s data, 84 bytes: the ASCII magic BGC1 (4 bytes); the Merkle root (32); prev, the SHA-256 of the writer’s previous payload, all zeros for the first (32); then the first and the last position in the batch, each an unsigned 64-bit big-endian integer (8 and 8). The root is the binding. The positions are an index for people: they carry no epoch, and a verifier does not check them. prev chains the writes, so a write missing from a copy of the list shows as a break.
The tree hashes as RFC 9162 does: a leaf is SHA-256 of the byte 0x00 followed by the 32 raw bytes of the record’s proofHash, and an inner node is SHA-256 of 0x01, the left child and the right child. Because proofHash leaves out actor and policy, a verifier also checks the proof’s own signature, which covers them.
A verifier checks, offline: the proof’s signature; that the record’s leaf and path reach the root; that the payload carries that root; that the raw transaction is signed by the writer the verifier names, sent to that same address, on the expected chain, carrying the payload, and hashes to txHash; that the header hashes to blockHash and the transaction is in it; and that the floor header hashes to the proof’s signed floor block (commit.slotFloor, or commit.slotAnchor on earlier proofs). The block time is read from the header, never from the file’s own field. Offline, that shows the header is the one with that hash; that it is Base’s own block is one lookup on any Base node or explorer, at any time.
What the file does not prove. status is what BitGraph’s Base node reported when the file was written: included by Base’s sequencer, safe once Base had posted the block’s data to Ethereum, finalized once that Ethereum block was final. Ask any Base node for its safe or finalized block to check it yourself.
Settlement is the proof of that step: Ethereum’s own record of the Base block, filled in once it exists. The current form, bitgraph-output-root/1, is Base’s output root: Base posts to Ethereum, about once an hour, a root over its state that covers a run of blocks, and the settlement carries that root’s claim on Ethereum (the dispute game that proposed it, with the Ethereum block and transaction), together with the proof from Base’s block header to the root. Offline, a verifier recomputes the output root from the Base header it already holds and matches it to the claim. That shows the Base block existed by that Ethereum block, whatever happens to the block’s bytes later; a tree/1 export carries exactly this. The earlier form, bitgraph-settlement/1, pointed at the Ethereum block that committed the Base batch data, its raw header, the batcher’s blob transaction with its inclusion proof, and the KZG commitments of the blobs. Ethereum prunes blob bytes after about 18 days, so the older proof packages kept them beside the ceiling file and bitgraph-audit checks each blob against its commitment, decodes the batch and locates the ceiling transaction inside it; exports carry no blob bytes. What neither form proves: that Base’s derivation accepted the batch, and that the Ethereum header is canonical, which is one lookup on any node, said and never implied.
The floor it names is always the proof’s signed floor block: commit.slotFloor, or commit.slotAnchor on earlier proofs. A record whose own bytes quote a later block, as the earlier demonstration file on the home page quotes the anchor recorded right after its position opened, has a tighter floor of its own, which a reader checks from that quote.
The writer. The key is an ordinary Ethereum key generated on BitGraph’s host and held by the operator, outside the enclave. Holding it lets the operator make a ceiling late, or skip one, but never make one early: a proofHash does not exist before its commit. The batch’s position range is the operator’s statement; the root is not. The writer address sends nothing but ceiling writes, each a zero-value transaction to itself carrying one payload, and account nonces have no gaps, so reading its transactions from nonce 0 lists every ceiling ever written. That is an operating rule, not something the chain enforces: the chain shows whether it has held.
Canonical serialization
The signed body is serialized to bytes using a deterministic algorithm:
- Recursively sort all object keys in Unicode code-point order
- Serialize with
JSON.stringify(), no whitespace - Encode the resulting string as UTF-8 (no BOM)
Top-level key order after sort:
actor? → artifact → attestationFormat? → attribution? → commit → enforcement → measurement → publicKeyB64 → version
Field classification
Signed (security-critical)
These fields are in the SignedBody. Tampering invalidates the signature:
version, artifact.*, attribution.* (when present), commit.*, signer.publicKeyB64, environment.enforcement, environment.measurement, attestation.format
Self-authenticating
Not in the signed body, but independently verifiable:
signatureB64 (Ed25519), attestation.reportB64 (vendor-signed), slotAllocation (own Ed25519 signature)
Advisory (unsigned)
Not signed. Must not be used for security decisions: timestamps, metadata, claims.
Algorithms
| Purpose | Algorithm | Details |
|---|---|---|
| Proof signature | Ed25519 (RFC 8032) | 32-byte key, 64-byte signature |
| Hash | SHA-256 (FIPS 180-4) | 32 bytes, Base64 encoded |
| Encoding | Base64 (RFC 4648 §4) | Standard, with = padding |
| Counter | Decimal string | BigInt-safe, no leading zeros |
Where next
- SPEC.mdThe normative text, rendered, with the hash every tree/1 proof pins. The bytes are at /spec/SPEC.md.
- VerificationWhat a verifier checks, in order, and what each result means.
- API referenceEvery endpoint that produces or reads these proofs.
- Integration guideMake a proof from the CLI, the SDK or two HTTP calls, and keep it.
- The protocolThe same account in formal terms, with the invariants.