verba volant, scripta manent

Verify without Etch

Receipts, the script, manifests and folding proofs by hand.

Etch shows its working on every etch's proof page, but you shouldn't need Etch to check Etch. Everything here runs with no Etch code.

Keep a receipt

GET /etches/{id}/receipt returns one self-describing file with every value the three checks need: the line that was hashed, the fingerprint, the proof with its tree version and leaf and node rules, the signature if there is one and the words are public, and what the attestation must contain — including how close its time must be to the etch's. Keep it and the etch stays checkable even if Etch is gone.

Re-hash the words

Once an etch is public, its whole line is published. Hash it and compare with the fingerprint.

# Etch's own verdicts — then check them yourself with the canonical string.
curl https://test.etch.so/api/v1/verify/$ETCH_ID

# Re-hash the published string with any SHA-256 tool:
curl -s https://test.etch.so/api/v1/verify/$ETCH_ID | jq -j .canonical_string | shasum -a 256

Fold the proof

Climb from the fingerprint to the batch root. Tag bytes are prepended as bytes, not text. From tree version 2 the leaf starts with the account's id, so you need etch.author.user_id and etch.chain.tree_version as well as the proof.

// Fold an etch to its root. Tag bytes are prepended as BYTES, not as text.
// From tree version 2 a leaf also names its author: user id, then hash.
// treeVersion is etch.chain.tree_version; userId is etch.author.user_id.
const sha = async (...parts) => {
  const len = parts.reduce((n, p) => n + p.length, 0)
  const buf = new Uint8Array(len)
  let at = 0
  for (const p of parts) { buf.set(p, at); at += p.length }
  return hex(await crypto.subtle.digest('SHA-256', buf))
}
const toBytes = (h) => Uint8Array.from(h.match(/../g).map(b => parseInt(b, 16)))

const record = treeVersion === 1 ? hash : userId.replaceAll('-', '') + hash
let acc = await sha(Uint8Array.of(0x00), toBytes(record))     // leaf
for (const sibling of proof) {
  const [lo, hi] = acc <= sibling ? [acc, sibling] : [sibling, acc]
  acc = await sha(Uint8Array.of(0x01), toBytes(lo), toBytes(hi))  // node
}
console.log(acc === root)

Rebuild the batch

Each batch's sorted list of leaves is on Arweave, at the transaction named in the attestation's manifestTx: fingerprints under tree version 1, and from version 2 each account's id followed by its fingerprint. Decode it and you can rebuild the tree and any etch's proof yourself.

// Read a batch manifest from Arweave and rebuild the tree yourself.
// This is the whole decoder. A manifest is sorted, fixed-width records:
//   tree version 1: the etch hash                        32 bytes
//   tree version 2: the author's user id, then the hash  48 bytes
// treeVersion is in the batch's attestation, beside the manifest's id.

const res = await fetch(`https://arweave.net/${manifestTx}`)
const bytes = new Uint8Array(await res.arrayBuffer())
const width = treeVersion === 1 ? 32 : 48

const records = []
for (let i = 0; i < bytes.length; i += width) {
  records.push([...bytes.subarray(i, i + width)]
    .map(b => b.toString(16).padStart(2, '0')).join(''))
}

// records is already sorted — that is how the tree was built, so this
// rebuilds the exact root and regenerates any etch's proof. From version 2
// it also says whose etch each leaf is: the first 32 characters are the
// author's user id without its dashes.

Read the attestation

Finally, read the root's attestation on Arbitrum Sepolia from the EAS contract (0x2521021fc8BF070473E1e1801D3c7B4aB701E1dE) and check that it holds the root you folded to, that the attester is 0x25F885Bbef6589819f9B5025D8f962d8aF8dE99f, that it isn't revoked, and that its time follows the etch's recorded time by no more than 10 minutes. The proof page's script does all of this in one file. The details are in How proof works.

Check a whole record

/u/USERNAME/audit.mjs is a script, made for each account, that checks the record holds every etch the account has anchored since tree version 2 — using only the chain and Arweave, with Etch's listing as the thing under test. Run it with Node 18 or newer; it keeps a small state file so later runs read only what's new. It exits 0 when the record is complete, 1 when something is missing or wrong, and 2 when some batch couldn't be read yet. What it checks is in How proof works.