# How to Recover a Sealed Prayer

**Suite:** `v1`
**Status:** Final for suite `v1`. The chains, keys and contract in §2 were pinned on 22 Oct 2026 and must not change. The drand endpoints and Arweave gateways listed there are only examples: any that serve the same data will do.
**Last updated:** 2026-10-27. Where the five Path B cards are kept, and how three of them sign a round (§2, §4a); drand endpoints and Arweave gateways rechecked (§2). 2026-10-23: spoken prayers up to 3 minutes at 16 kbps (§5a). 2026-10-22: mainnet values pinned: drand quicknet, the Bhutan chain, `PrayerAnchor`

---

## Read this first

This document exists so that a person with **no connection to 108.bt, no account, and no help from anyone** can open a sealed prayer. It is written for a stranger, possibly decades from now, who has only this text and an internet connection.

It deliberately avoids depending on our website, our software, or our continued existence. If any instruction here requires contacting us, that is a bug in this document.

§7 says how to check that this file is genuine.

---

## 1. What was done

Each prayer was encrypted with `AES-256-GCM` using a random 256-bit key. That key was then encrypted a second time using **drand timelock encryption**, addressed to a specific future round of the drand `quicknet` randomness beacon.

The drand network publishes a signature for every round on a fixed 3-second schedule. Once the target round is reached, its signature becomes public and permanently available, and that signature is the key that unwraps the prayer's key.

Nothing else is required. No password was ever set. No account was ever created.

## 2. Constants

| Name | Value |
|---|---|
| drand chain | `quicknet` |
| drand chain hash (Path A, quicknet) | `52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971` |
| drand quicknet public key | `83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a` (scheme `bls-unchained-g1-rfc9380`) |
| Bhutan chain hash (Path B) | `2066b61a2e7cab03dba438f85cfdeed30fe148276baa732d5d171d1fd254a2d4` (beacon id `108-pathb`, group hash `ca2fe1e94952df4911f230ba0db9c40c9f8d97de8efc17982b5540edfa4d3b9a`) |
| Bhutan chain public key (Path B) | `9941efa4e60cbe4f37c7b8bdec1b218e1e6752b782d0f8810185a48dd6528c085bcda9dff428ed62cfb2738bbd2a930601e5c09050e2a6736dde3309ead6789a25b402d8ebc076c929c8fc7cc7effe5fe0dc875a5593ce8c03ff6eadffeb9f80` (scheme `bls-unchained-g1-rfc9380`, the same as quicknet) |
| Bhutan chain round schedule | One round every 3 seconds from genesis time `1792666680` (22 Oct 2026, 10:58:00 UTC): round `r` is at `1792666680 + (r − 1) × 3` |
| Bhutan chain threshold | 3 of 5 |
| Bhutan chain files (Path B) | `https://108.bt/pathb` (drand's HTTP format; also `https://api.108.bt/api/v1/pathb`), and on Arweave by tag: `App-Name` = `108bt`, `Type` = `pathb-info` or `pathb-round`, `Chain-Hash`, `Round` |
| Path B shares | Five paper cards, each sealed in a tamper-evident envelope, kept in four places in Bhutan so that no one place holds three: 108.bt (two cards) · GovTech Agency (one) · Zhung Dratshang, the Central Monastic Body (one) · Gelephu Mindfulness City Authority, GMCA (one) |
| drand period | 3 seconds |
| drand genesis time (quicknet) | `1692803367` (23 Aug 2023, 15:09:27 UTC): round `r` is at `1692803367 + (r − 1) × 3` |
| drand endpoints | `https://api.drand.sh`, `https://api2.drand.sh`, `https://api3.drand.sh`, `https://drand.cloudflare.com`, or any other that serves the chain hash above |
| Ethereum network | Mainnet (chain id 1) |
| `PrayerAnchor` contract (Ethereum mainnet) | `0x24BbCE6122224a8135019c496830BD66e785B95a` (deployed in block 26247503, source verified on Etherscan) |
| `Chorten108` contract (Ethereum mainnet) | Not deployed as of this version. **Not needed to open or verify a prayer.** |
| Arweave gateways | `https://arweave.net`, `https://turbo-gateway.com`, `https://ardrive.net`, `https://vilenarios.com`, `https://frostor.xyz`, or **any** [AR.IO gateway](https://ar.io/). All are interchangeable; a transaction id fetched from one is identical from all. |
| ArNS convenience name | None registered as of this version. **Not required:** use the transaction ids. |
| Cipher | `AES-256-GCM`, 96-bit IV, 128-bit tag |
| Timelock | `tlock` over BLS12-381 (drand IBE), in the binary age format (not PEM-armoured) |

## 3. Finding the data

You need the Arweave transaction that contains the prayer.

**If you have the receipt**, it names the Arweave transaction id directly. Skip to §4.

**If you have nothing but this document:**

1. Read the `PrayerAnchor` contract on Ethereum mainnet at the address in §2. Any Ethereum node, block explorer, or public RPC will serve it. No API key is required to read contract state.
2. Call `batchCount()` to learn how many batches exist, then `batches(i)` for each `i` from 0 to `batchCount() - 1`. `i` is the batch id. Every entry returns `{ root, arTxId, count, timestamp }`, where `timestamp` is the block time it was anchored.
3. `arTxId` is the Arweave transaction id as 32 raw bytes. Encode them as **base64url without padding** to get the usual 43-character id, then fetch it from any Arweave gateway: `https://arweave.net/<43-char id>`.
4. `count` is the number of prayers in that bundle.

Contract *state* is used rather than event logs precisely so this step still works after Ethereum clients prune historical transaction data (EIP-4444).

**Or search Arweave directly.** Every bundle is tagged `App-Name` = `108bt`, `Type` = `batch`, `Batch-Id`, and `Anchor-Chain-Id` = `1` with `Anchor-Contract` = the `PrayerAnchor` address in §2. Search any gateway's GraphQL for those tags. Batch ids start again at 0 for every contract, so a bundle with any other chain id or contract belongs to a test deployment: ignore it. Whatever you find this way, check it against the contract (§6) before trusting it.

## 4. The bundle format

Each Arweave transaction contains UTF-8 JSON: `{ "type": "batch", "batchId", "root", "prayers": [...] }`. Each entry in `prayers`:

```json
{
  "id": "01JB7X...",
  "suite": "v1",
  "ciphertext": "<base64 iv>.<base64 ciphertext‖tag>",
  "wrappedK": "<base64 of the binary age file (tlock recipient), not PEM-armoured>",
  "unlockRound": 18235000,
  "wrappedKB": "<base64 of the binary age file, Bhutan chain>",
  "unlockRoundB": 87600,
  "drandChainB": "<Bhutan chain hash>",
  "unlockDate": "2034-02-21T00:00:00+06:00",
  "displayDate": "2034-02-21T00:00:00+06:00",
  "drandChain": "<chain hash>",
  "createdAt": "2026-10-14T09:22:41Z",
  "kind": "text"
}
```

A prayer is written or spoken. A spoken one has `"kind": "voice"` and one more field, pointing to its recording, which is its own
Arweave item (see §5a):

```json
  "kind": "voice",
  "voice": { "arweaveId": "<43-character Arweave id>", "hash": "0x<keccak256 of that item>" }
```

`unlockDate` is when the prayer can be decrypted. `displayDate` is the date its author chose, and 108.bt waited for it before showing the prayer. The two are the same unless the author chose more than 10 years. Encryption was capped at 10 years, so for those prayers you can decrypt from `unlockDate`, before the author's chosen date. Please consider honouring `displayDate` anyway.

The bundle's top level also carries `batchId` and `root`, which you need in §6. A prayer is placed in no chorten: it belongs to all 108.

## 4a. Two ways in; you only need one

Every record carries **two** wrapped copies of the same content key:

- **`wrappedK`**: addressed to drand quicknet round `unlockRound`
- **`wrappedKB`**: addressed to the Bhutan chain round `unlockRoundB`

They unwrap to the identical key. **Try Path A first**; it is automatic and public. If quicknet no longer exists, use Path B. The Bhutan chain's round signatures are made with any 3 of its 5 key cards (§2) and published once quicknet has stopped: every overdue round together, then later rounds as they come due. Your round is `unlockRoundB` in the bundle.

Each signature, and the chain's info, is kept on Arweave (tags in §2), so it outlives 108.bt. To use them:

1. Search any Arweave gateway for `App-Name` = `108bt`, `Type` = `pathb-info`, `Chain-Hash` = `drandChainB` from the bundle, and for `Type` = `pathb-round` with `Round` = your `unlockRoundB`.
2. Save them as `<chain hash>/info` and `<chain hash>/public/<round>` in a folder, and serve it with any static web server.
3. `tle -d -n http://<that server> -c <chain hash> kb.age` (with `wrappedKB`, base64-decoded). While 108.bt exists, `-n https://108.bt/pathb` does the same.

`tle` checks the signature against the chain's public key before using it, so a copy from anywhere is safe to use.

If Path A is gone and Path B's signature has not yet been published for your round, **any three of the five cards can together produce it.** No one place in §2 holds three, so no one place, 108.bt included, can sign alone. 108.bt's two cards and any one other envelope are enough; so are the three other envelopes together, without 108.bt.

**How three cards sign a round.** A card's text is `PB1-<first 8 hex digits of the chain hash>-<card number>-<share>-<checksum>`; spaces and case don't matter. `<share>` is the card's secret: a 32-byte big-endian BLS12-381 scalar, in base32 (RFC 4648, upper case, no padding). The five shares are the values p(1) to p(5) of a degree-2 polynomial p whose p(0) is the chain's secret key (Shamir secret sharing); the card number is x. To sign round `r` with any three cards: let m = SHA-256 of `r` as an 8-byte big-endian integer, and H(m) its hash to the BLS12-381 G1 curve per RFC 9380 with the domain tag `BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_` (drand's `bls-unchained-g1-rfc9380` scheme, the same as quicknet). Each card's partial signature is its share times H(m); add the three, each multiplied by its Lagrange coefficient at x = 0. The result is the round's signature (48 bytes, compressed G1). It must verify against the chain's public key in §2: if it doesn't, a card was misread. Publish it as `{"round": r, "randomness": "<hex SHA-256 of the signature>", "signature": "<hex>"}` at `<chain hash>/public/<r>`. Sign only rounds whose moment has passed; signing a later round would open its prayers early. 108.bt's own tool (`pathb sign`, then `pathb combine`) does exactly this.

If a place named in §2 no longer exists, ask the organisation that took over its records. If fewer than three cards can be found, Path B cannot be reconstructed: use Path A.

## 5. Decrypting

### The easy way

Install the reference tool [`tle`](https://github.com/drand/tlock) (Go: `go install github.com/drand/tlock/cmd/tle@latest`) and Python's `cryptography` package (`pip install cryptography`), then:

```sh
# 1. unwrap the content key (only works once unlockRound has passed)
echo "$wrappedK" | base64 -d > wrapped.age
tle --decrypt --chain <chain hash> wrapped.age > K.bin

# 2. decrypt the prayer  (openssl's command line cannot do AES-GCM; use Python)
#    ciphertext is "<base64 iv>.<base64 ciphertext‖tag>"
python3 -c '
import sys, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
iv, ct = sys.argv[1].split(".")
key = open("K.bin", "rb").read()
print(AESGCM(key).decrypt(base64.b64decode(iv), base64.b64decode(ct), None).decode())
' "$ciphertext"
```

### From first principles

If no tooling survives, the scheme is fully specified in public literature:

1. **Fetch the round signature.** `GET <drand endpoint>/<chain hash>/public/<unlockRound>` returns a JSON object with a `signature` field: a BLS12-381 signature on the round number, verifiable against the chain's public key from `GET <chain hash>/info`.
2. **Unwrap `K`.** `wrappedK` is an age-encrypted file whose recipient stanza uses the `tlock` identity-based encryption scheme of Gailly, Melissaris and Wesolowski (2023), where the IBE identity is the round number and the round signature is the IBE private key. The age format is publicly specified; the `tlock` stanza is documented in the drand repository.
3. **Decrypt the prayer.** Split `ciphertext` on `.`: the first part is a base64 96-bit IV, the second is the base64 AES-256-GCM ciphertext with its 128-bit tag appended. Decrypt with `K` and verify the tag.

The result is UTF-8 JSON: `{"v":1,"name":"<author>","text":"<the prayer>"}` (a spoken prayer: `"text":""` and `"kind":"voice"`), followed by spaces up to a fixed size (256, 512, 1024, 2048, 4096, 8192, 16384, 32768 or 65536 bytes), so a ciphertext doesn't reveal how long its prayer is. JSON readers ignore trailing whitespace; trim it if yours doesn't. The author's name was sealed together with the prayer.

## 5a. A spoken prayer

A prayer with `"kind": "voice"` was spoken, up to 3 minutes (#65, 23 Oct 2026; never longer in a sealed prayer). Its name is in the ciphertext as for any prayer; its recording is a
separate Arweave item, sealed with **the same key `K`**, so whatever opened the prayer opens its recording too.

1. **Find the item:** its id is the bundle entry's `voice.arweaveId`. Or search Arweave for the tags `App-Name` = `108bt`,
   `Type` = `prayer-voice` and `Prayer-Id` = the prayer's `id`. Fetch it from any gateway: 131,100, 262,172 or 393,244 bytes, raw binary (one size per length band: up to 1, 2 or 3 minutes at 16 kbps); a recording made at 24 kbps on 23 Oct 2026 may instead be 589,852 bytes.
2. **Check it:** `keccak256` of the item must equal the entry's `voice.hash`. That hash is inside the prayer's leaf (§6), so a match
   proves the recording is the one sealed.
3. **Get `K`** exactly as in §5, from `wrappedK` or `wrappedKB`.
4. **Decrypt it** with AES-256-GCM: the first 12 bytes are the IV, the last 16 the tag, and the associated data is the UTF-8 text
   `108bt/prayer-voice/v1|<prayer id>`.
5. **Unpad it:** the result is 131,072, 262,144, 393,216 or 589,824 bytes (the item less its 28). Its first 4 bytes are a big-endian length `n`; the next `n` bytes are the recording,
   and the rest are zeros.
6. **Play it:** those `n` bytes are an Ogg Opus file (RFC 7845), mono, 48 kHz. Save it as `voice.ogg`; VLC, ffmpeg and any modern
   browser play it.

```sh
python3 -c '
import sys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = open("K.bin", "rb").read(); item = open("voice.bin", "rb").read()
p = AESGCM(key).decrypt(item[:12], item[12:], ("108bt/prayer-voice/v1|" + sys.argv[1]).encode())
n = int.from_bytes(p[:4], "big"); assert p[4 + n:] == bytes(len(p) - 4 - n)
open("voice.ogg", "wb").write(p[4:4 + n])
' "$prayerId"
```

## 6. Verifying it is authentic

To confirm a prayer was in the original sealed batch and has not been altered:

The tree is an OpenZeppelin `StandardMerkleTree` ([`@openzeppelin/merkle-tree`](https://github.com/OpenZeppelin/merkle-tree)). Every hash is Ethereum's `keccak256`.

1. ABI-encode `(ciphertext, wrappedK, wrappedKB, voiceHash)`, exactly as they appear in the record, as Solidity `abi.encode(string, string, string, bytes32)`. `voiceHash` is the entry's `voice.hash` for a spoken prayer, and 32 zero bytes for a written one. Because `voiceHash` is inside the leaf, the anchor also proves which recording was sealed with the prayer. Then compute `leaf = keccak256(keccak256(encoded))`. The hash is applied twice.
2. Rebuild the whole tree from every record in the bundle's `prayers` array, or use the record's proof if you have its receipt. Each pair of nodes is hashed as `keccak256(a ‖ b)`, with the smaller 32-byte value first. The result must equal the bundle's `root`.
3. Compare it with `root` for that batch in the `PrayerAnchor` contract. The anchored root is the prayer tree's root itself.

Because the contract entry is write-once and was published *before* the unlock date, a match proves the ciphertext, both wrapped keys and any recording are exactly what was sealed.

## 7. Integrity of this document

This file is stored on Arweave, tagged `App-Name` = `108bt` and `Type` = `recovery-doc`, so it can be found the same way as the bundles. (A file can't contain its own Arweave id; while 108.bt exists, `https://108.bt/keep` links to it.) Each version is stored the same way; the newest is the one to follow, and a record's `suite` (the Changelog below) says which rules it was sealed under. If `Chorten108` is deployed, its metadata manifest lists this file's `keccak256` hash. If you obtained this text elsewhere, hash it and compare. (It can't be stored in a contract, because it contains the contracts' own addresses.) Nothing in §5 or §6 depends on trusting this file: a record that opens with drand's public signature and matches the root anchored on Ethereum is genuine whoever told you how to read it.

## 8. If something is missing

| Problem | What to do |
|---|---|
| `arweave.net` is gone | Use any other AR.IO gateway from §2. **No gateway is special**: any of them returns identical bytes. Each batch is an ANS-104 data item carried inside a bundle transaction; AR.IO gateways index bundles, so they serve the item by the id anchored on Ethereum. |
| No gateway resolves, or ArNS is dead | A plain Arweave node knows only the bundle transaction, not the items inside it. Either run an AR.IO gateway yourself (open source), which indexes bundles and then serves the item by its id, or find the bundle transaction (a gateway's GraphQL `bundledIn` field for the item id, recorded while any gateway existed), fetch it from any Arweave node or archive, and read the item out of it (the ANS-104 format is public). Either way, check what you got: the item's id is the base64url SHA-256 of its signature, and its data must rebuild the root anchored on Ethereum (§6). |
| An Ethereum RPC refuses you | Contract state is readable from any node, explorer, or archive. Run your own client if necessary. |
| drand `quicknet` has shut down | **Use Path B** (§4a). This is exactly what the second wrap exists for: drand has already retired one earlier chain, `fastnet`, in favour of quicknet, which is why this design never relies on a single chain. |
| The Bhutan chain signature is missing | Use Path A. If both are unavailable, bring together any three of the five cards from the places in §2 (§4a): together they can produce the round signature. |
| The unlock date has not arrived | Then it cannot be opened, by anyone, including us. That was the point. |

---

## Changelog

| Suite | Date | Change |
|---|---|---|
| `v1` | 22 Oct 2026 (values pinned) | Initial. AES-256-GCM + tlock, dual-wrapped (drand quicknet + Bhutan chain). Payload `{v,name,text}`, padded with trailing spaces to a fixed size (7 Oct 2026). keccak256 `StandardMerkleTree`; the anchored root is the prayer tree's root, and the bundle holds prayers only (19 Oct 2026); spoken prayers: a separate voice item under the same K and the leaf's `bytes32 voiceHash` (§5a, 19 Oct 2026); no chorten: the record's `chortenId` and the leaf's leading `uint8 chortenId` were removed (20 Oct 2026); a recording is up to 3 minutes at 16 kbps, padded to 128, 256 or 384 KiB (§5a, 23 Oct 2026; earlier test items on Sepolia are 192, 384 or 576 KiB, or larger). All of these came before any prayer was sealed in production. Test batches anchored on Sepolia before 20 Oct carry `chortenId` in each record, and their leaf is `abi.encode(uint8 chortenId, string, string, string, bytes32 voiceHash)` (before 19 Oct, without `voiceHash`). |

Any change to the cipher, encoding, bundle format, or drand chain requires a new suite identifier and a new row here. Records must remain decryptable using only their own `suite` value and this document, never using current application code, which will not exist.
