# Proof of existence Source: https://selfstore.dev/docs/proof Show that a document existed on a given date without handing it over. selfstore/proof hashes the file on the device, sends only its SHA-256 to a notary, and keeps the receipt next to the file in the user's own store. Sometimes a user needs to show that a file existed on a date: an inventory before a move, an invoice before a dispute, a draft before someone else published the same idea. The usual answer is to upload the file to a service that dates it, and a local-first app is built so that the file never leaves the device. `selfstore/proof` keeps that promise. The bytes are hashed on the device, only the SHA-256 goes to a notary, and the receipt comes back into the user's own store, next to the file and inside the same backup. ```ts import { prove, verify, rfc3161Notary, PROOFS } from 'selfstore/proof'; const notary = rfc3161Notary({ url: 'https://your-tsa.example/tsr' }); const file = await store.putFile({ bytes, name: 'inventory.pdf', mime: 'application/pdf' }); const proof = await prove(bytes, { notary, label: 'inventory', store, file }); store.get(PROOFS, file); // the receipt, next to its file const { matches } = await verify(bytes, proof); // false the moment one byte moves ``` ## What leaves the device The digest, and the label if you pass one. Nothing else: not the name, not the size, not a byte of the content. The label is sent with the digest, so keep it a type such as `invoice` or `inventory`, never a file name or a phrase from the document. `prove(blob, options)` takes `ProveOptions` and returns a `Proof`. It is the notary's `Receipt` plus what the device measured: the algorithm, the size in bytes and, when you pass `file`, the id of the store file it belongs to. With `store` in the options, the proof is also written to the `PROOFS` collection, keyed by the file id or by the digest. Any store with a `put(collection, record)` method fits, which the `ProofShelf` type names, so the simple store works as it is. ## Checking a file later `verify(blob, receipt)` hashes the bytes again and compares them with the receipt. It returns a `ProofCheck` with three fields. `matches` says whether the bytes are the ones the receipt names, `hash` is what they hash to now, and `complete` says whether the notary had finished with that receipt. That check proves the file in hand is the dated one. It does not prove that the receipt itself is genuine. That is what the notary's evidence is for, and the notary's own verifier checks it, without this library. A receipt that carries no digest at all throws `BAD_FORMAT`, because a damaged receipt is not a document that changed. ## The notary is an interface The services that date a digest differ in almost everything except one shape, so a notary is two methods: ```ts interface Notary { readonly name: string; deposit(hash: string, label?: string): Promise; // a receipt now complete(receipt: Receipt): Promise; // what it adds later } ``` `deposit` sees the digest and the label, never the bytes. Some notaries sign at once and return a complete receipt. Others anchor their digests later, in a daily tree or a public ledger. They answer `complete: false`, and `completeProof(proof, { notary, store })` asks them for the rest the next day or at the next launch. A proof that is already complete comes back as it is, without a call. A notary that answers for another digest, or in another notary's name, is refused with `NOTARY_REFUSED`, because a receipt that does not name the bytes is worse than none. One that cannot be reached, or answers 429 or a 5xx, throws `NOTARY_UNAVAILABLE`, and the deposit can be tried again later. ## RFC 3161 time-stamping authorities `rfc3161Notary` works with any RFC 3161 authority. Its options are `Rfc3161Options`, which take the authority's `url`, an optional `name` for the receipt and an optional `fetch`. The authority signs the digest and its own time in one token, so the receipt is complete on deposit. The reply is checked to stamp this digest and to echo this request's nonce before anything is kept. The token travels in the receipt as `evidence.tsr`, in base64. `timestampReplyOf(receipt)` gives the `.tsr` bytes back, to offer as a download. Anyone can then check it with `openssl ts -verify`, given the file and the authority's certificate. `readTimestampReply(bytes)` reads the digest, the time and the nonce out of a reply as a `TimestampToken`. It does not check the authority's signature. Its job is to tell a token for another document apart before anything is kept. Most public authorities send no CORS headers. In a browser, point `url` at a relay you run, or pass your own `fetch`. ## Public digest journals `digestJournalNotary` works with a public digest journal. Such a journal takes a SHA-256 with no account, writes it as one line of a public file, folds the file into a Merkle tree and anchors the root in Bitcoin with OpenTimestamps. Its options are `JournalOptions`: the same three fields as above, and an optional `type` (since 1.8.32), sent as `type` with every deposit to a journal that issues typed receipts and keys them on it, such as a permit display where the label is the permit number. Without it the deposit is the journal's ordinary one. ```ts import { prove, completeProof, digestJournalNotary } from 'selfstore/proof'; const notary = digestJournalNotary({ url: 'https://journal.example' }); const proof = await prove(bytes, { notary, label: 'inventory', store, file }); // the next day, or at the next launch const done = await completeProof(proof, { notary, store }); ``` The deposit returns the journal's own receipt at once, with `complete: false`. Once the first tour covering the line is in Bitcoin, `completeProof` adds the tour, its root, the block, the `.ots` proof and, when the journal also stamped that root under RFC 3161, the `.tsr`. Checking the path from the line to the root is the job of the journal's own verifier. ## At launch, finishing what is pending A journal finishes a receipt hours after the deposit, so an app that offers proofs needs the same round at every launch. It finds the incomplete proofs, asks the notary that issued each one, and writes back those that are done. `completePending({ store, notaries })` is that round. ```ts import { completePending, proofsIn, digestJournalNotary } from 'selfstore/proof'; const notary = digestJournalNotary({ url: 'https://journal.example' }); const { completed, pending, refused } = await completePending({ store, notaries: [notary] }); for (const proof of proofsIn(store)) show(proof.file, proof.depositedAt, proof.complete); ``` It returns a `PendingRound` with three lists. `completed` holds the proofs that came back complete, and only those are written to the store. `pending` holds the ones to ask again on a later launch: the notary had nothing to add yet, could not be reached, or is not among the `notaries` given. A notary that cannot be reached is not asked again in the same round, so one dead service costs one request, not one per proof. `refused` holds the proofs whose completion answered for another digest. They are set apart and the receipt in the store is never overwritten by that answer. `proofsIn(store)` reads every proof back, oldest deposit first, to list a series or rebuild a screen on another device. It skips any record of the `PROOFS` collection that is not a proof rather than trust it. The store it reads from is a `ProofLibrary`: a `ProofShelf` that can also list a collection with `all()`. The simple store fits as it is. ## Writing your own notary Any object with a `name`, a `deposit` and a `complete` is a notary. Return a `Receipt` that names the same digest, a `depositedAt` instant from the notary's clock rather than the device's, and `complete: true` once nothing is left to add. Keep `evidence` as strings, base64 for bytes, so the receipt stays plain JSON and survives every destination a store can write to. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt