# Open Ledger protocol, version 1

This is what a third-party client needs to read an Orb community back from Monad testnet (chain 10143). It
needs three JSON-RPC methods (`eth_call`, `eth_getLogs`, and the chain id), a Keccak and SHA-256 implementation,
and, for a private ledger, the owner's recovery code. It does not need an Orb server, an account, or any state
kept by Orb. The reference implementation is `lib/ledger-reader.ts`, `lib/ledger-shared.ts` and
`lib/ledger-crypto.ts`; `scripts/read-ledger.mjs` is a command-line client built on them.

## Contract

`contracts/OrbCommunityLedger.sol`, deployed at the address recorded by `npm run ledger:deploy:testnet` (the
deployment record names the contract, the transaction and the block).

- `ledgers(bytes32 id)` returns `(owner, scribe, mode, closed, seq, lastBlock, head)`.
- `openMessage`, `closeMessage` return the exact texts an owner signs (see below). Clients may call them or build
  the same bytes themselves.
- Events: `Opened(id, owner, scribe, mode, name, publicKey, recovery, ownerSignature)`,
  `Entry(id, seq, prevBlock, head, codec, payload)`, `Closed(id, seq, head)`.

The contract is testnet-only and has no upgrade path. Its runtime bytecode is checked against the artifact
before a client trusts it.

## Identifiers

`ledgerId(owner, server, generation) = keccak256(abi.encode("orb.ledger.v1", owner, sha256(utf8(server)), generation))`,
where `owner` is the checksummed address. A community's history restarts under a new id each time it is turned
on again.

## The owner's consent

To open a ledger, the owner signs (EIP-191 `personal_sign`) this text, with one field per line and no trailing
newline. Every hex value is lowercase.

```
Orb community ledger
Save this community on Monad testnet.
mode: private (encrypted)            (or: mode: public (plain text, permanent))
ledger: <id>
owner: <owner address>
scribe: <scribe address>
name: <community name>
key: <keccak256 of the public key bytes, 0x-hex>        (keccak256 of empty bytes for a public ledger)
recovery: <keccak256 of the recovery bytes, 0x-hex>      (keccak256 of empty bytes for a public ledger)
contract: <contract address>
chain: 10143
```

The name is at most 80 bytes and contains no control characters. The contract rebuilds this text and recovers
the signer; a mismatch reverts. A reader checks the same signature with the owner address from `Opened`.

To stop a ledger, the owner signs:

```
Orb community ledger
Stop saving this community on Monad testnet. What was already saved stays on chain.
ledger: <id>
contract: <contract address>
chain: 10143
```

## Entries and the chain of hashes

Each `publish` appends entry `seq`, which must be the previous `seq` plus one.

`head(seq) = keccak256(abi.encode(bytes32 head(seq-1), uint64 seq, uint8 codec, bytes32 keccak256(payload)))`,
with `head(0) = 0x00…00` (32 zero bytes).

Codecs: `1` text (UTF-8 JSON, public ledgers only), `2` raw deflate of UTF-8 JSON (public), `3` sealed (private).
A public ledger refuses codec 3; a private ledger refuses codecs 1 and 2. Payloads are at most 65,536 bytes.

Each `Entry` event carries `prevBlock`: the block of the entry before it, or the block of the opening record for
the first entry. A reader walks back from `ledgers(id).lastBlock`, asking for one block at a time with
`eth_getLogs` filtered to that block and to topics `[Opened or Entry, id]`. Public RPCs answer small windows and
refuse bursts, so the one-block walk is deliberate. Retry on rate limits and on transport errors such as a bare
"Failed to fetch"; do not retry on a revert or a decoding error.

Verify, in order:

1. The contract's code matches the published runtime, and `Opened` exists for `id`.
2. The owner's signature over the opening text recovers to the owner address.
3. For each `seq` from 1 to `ledgers(id).seq`, an entry exists and `head(seq)` equals the head its event carries.
   The last head equals `ledgers(id).head`. A gap, a swap or a reordering fails here.
4. Decode each payload (below). A ledger is complete when every entry is present, chained and decoded.

## Private entries (codec 3)

The plaintext is the raw-deflate form of the entry JSON. It is sealed to the ledger's P-256 public key (65 bytes,
`0x04 || x || y`):

- A fresh P-256 key pair is generated for the entry. ECDH is run between the ephemeral private key and the ledger
  public key, giving 32 bytes of shared secret.
- The AES-256-GCM key is HKDF-SHA256 of that secret, with salt = the 65-byte ephemeral public key and info =
  `orb-ledger-seal-v1`.
- The AES-GCM nonce is 12 random bytes. The additional authenticated data is
  `orb.ledger.entry.v1\n<id, lowercase>\n<seq>\n<codec>` (UTF-8).
- The payload is `0x01 || ephemeral public key (65) || nonce (12) || ciphertext || tag (16)`.

The ledger's private key is recovered from the `recovery` bytes with the owner's code. Those bytes are UTF-8 JSON:

```
{"v":2,"publicKey":"<base64 of the 65-byte public key>","factors":{"passphrase":{...}}}
```

The passphrase envelope is PBKDF2-HMAC-SHA256 (salt 16 bytes, 600,000 iterations, or any count from 210,000 to
3,000,000) producing an AES-256-GCM key over the PKCS#8 private key. Its additional data is the context string
followed by `\npublicKey: <base64>`, where the context is the JSON
`{"v":1,"factor":"orb-ledger-passphrase","ledger":"<id>","owner":"<owner>"}`. The code is the 25-character recovery
code, formatted `XXXXX-XXXXX-XXXXX-XXXXX-XXXXX` over the alphabet `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, normalised to
upper case with separators removed. After decryption, the imported private key must agree with the stored public
key before it is used.

## Entry JSON

An entry is UTF-8 JSON with `"v":1` and one of three types.

```
{"v":1,"type":"messages","channels":[
   {"id":"<room id>","name":"<channel name>",
    "messages":[{"i":<message id>,"t":<ms epoch>,"a":{"r":"<author ref>","n":"<display name>","h":"<handle, private only>"},
                 "b":"<text>","k":"gif"|"file","p":<reply-to id>}],
    "removed":[<message ids deleted later>]}]}

{"v":1,"type":"meta","at":<ms>,"community":{"name":"","bio":"","created":<ms>,"members":<count>},
 "channels":[{"id","name","kind","topic","private","readOnly","sealed","saved"}],
 "roles":[{"name","color","position"}],
 "members":[{"r","n","h","role"}]}          (members present in private ledgers only)
```

`a.r` is `sha256(lowercase(ledger id) + ":" + person)`, the first 16 hex characters, so references do not link
between ledgers. A deletion may appear in a later entry than the message it removes; readers apply deletions
after merging every entry. A message id that appears twice keeps the first copy.

Readers bound everything they accept: at most 5,000 messages per channel block, 500 channels, 4,000 characters per
message, 120 per channel name, 1,000 per bio, and 65,536 bytes in total per entry. Inflated data is capped at
262,144 bytes. Anything over a bound, any unknown type or version, and any invalid UTF-8 is reported as a problem
for that entry, and the other entries are still read.

## Writers

A conforming writer sends only what the owner chose, redacts secrets before sealing or publishing, never
publishes a channel the mode forbids, and writes a deletion only for a message it has already saved. The Orb
keeper is the reference writer. It is not required to be the only one; any scribe the owner's consent names may
write, provided the contract accepts it.

## Versioning

Version 1 is the first published format. A change to any hash, codec, AAD or message text is a new version, and
old ledgers stay readable under their own version. The contract publishes its version through its bytecode hash.
