# 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.

curl:

```bash
# 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
```

JavaScript:

```js
// Check an etch yourself: re-hash the published string, compare.
const res = await fetch('https://test.etch.so/api/v1/verify/' + etchId)
const v = await res.json()

const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(v.canonical_string))
const mine = [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')
console.log(mine === v.hash)                       // the words are unchanged
console.log(v.checks.map(c => c.key + ': ' + c.verdict).join(', '))
```

Python:

```python
# Check an etch yourself: re-hash the published string, compare.
import hashlib, json, urllib.request

v = json.load(urllib.request.urlopen(f"https://test.etch.so/api/v1/verify/{etch_id}"))
mine = hashlib.sha256(v["canonical_string"].encode()).hexdigest()
print(mine == v["hash"])                            # the words are unchanged
print(", ".join(f"{c['key']}: {c['verdict']}" for c in v["checks"]))
```
## 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.

JavaScript:

```js
// 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)
```

Python:

```python
# 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.
# tree_version is etch["chain"]["tree_version"]; user_id is etch["author"]["user_id"].
import hashlib

record = hash if tree_version == 1 else user_id.replace("-", "") + hash
acc = hashlib.sha256(b"\x00" + bytes.fromhex(record)).hexdigest()      # leaf
for sibling in proof:
    lo, hi = sorted([acc, sibling])
    acc = hashlib.sha256(b"\x01" + bytes.fromhex(lo) + bytes.fromhex(hi)).hexdigest()  # node
print(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.

JavaScript:

```js
// 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.
```

Python:

```python
# Read a batch manifest from Arweave and rebuild the tree yourself.
# A manifest is sorted, fixed-width records: 32-byte etch hashes under tree
# version 1; from version 2, 48 bytes — the author's user id, then the hash.
import urllib.request

data = urllib.request.urlopen(f"https://arweave.net/{manifest_tx}").read()
width = 32 if tree_version == 1 else 48
records = [data[i:i + width].hex() for i in range(0, len(data), width)]
# Already sorted — that is how the tree was built.
```
## Read the attestation

Finally, read the root's attestation on Arbitrum Sepolia from the EAS contract ([`0x2521021fc8BF070473E1e1801D3c7B4aB701E1dE`](https://sepolia.arbiscan.io/address/0x2521021fc8BF070473E1e1801D3c7B4aB701E1dE)) and check that it holds the root you folded to, that the attester is [`0x25F885Bbef6589819f9B5025D8f962d8aF8dE99f`](https://sepolia.arbiscan.io/address/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](/docs/proof#chain).

## 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](/docs/proof#complete).
