# FTO Protocol Specification — v1 (EXPERIMENTAL, testnet)

> **Status:** testnet · **Spec version:** `FTO/v1` · **Wire version byte:** `0x31` (`'1'`)
> **This document is the normative public spec.** Any third party can implement an indexer
> from it alone and reproduce the same **state digest** and **state root** as the reference
> implementations. Golden vectors: [`fto/spec/vectors.json`](./spec/vectors.json), checked by
> `fto/tools/spec_vectors_check.mjs`.
>
> Network parameters in this doc carry **testnet** values; mainnet values are set at a mainnet
> cutover and this document is versioned for that (see §11). Mainnet is **not** live.

---

## 0. Scope and status

FTO ("Fractal TapeOut") is a **metaprotocol**: there is no VM and no covenant in the asset
layer. Events are ordinary Bitcoin (Fractal) transactions carrying one `OP_RETURN` payload
each. The **truth layer is an event log**: everyone derives the same state by replaying the
same events in the same canonical order. Consensus does not interpret the payload; the
indexer does, deterministically.

- **Layers:** `用` (use: hold/transfer/tapeout/trade), `发` (issue: factory/mint/task/PoD),
  `运营` (operate: indexers, commitments, keeper). See [`FEATURE-MATRIX.md`](../FEATURE-MATRIX.md).
- **Experimental:** the rules are frozen for the deployed networks but the *format* is not
  final. `AUTH_HEIGHT`/`ECON_HEIGHT` are network parameters.
- **Fail-closed writes to mainnet** are enforced by the deployed edge and are outside this
  protocol spec.

## 1. Conventions

- Integers are **little-endian**.
- `id16` = 16 bytes; `owner8` = 8 bytes; `hash32` = 32 bytes.
- `hash16(o) = sha256(canonicalJSON(o))[:16]` (first 16 bytes, shown as 32 hex chars).
- `canonicalJSON(o)` = JSON with object keys sorted ascending recursively, no whitespace.
- `sha256` is SHA-256. Hex is lowercase.
- A **factory** is a content-addressed processor (`factoryId = hash16(core)`, `core = {symbol,
  supplyNAND, supplyLATCH, multiplier}`).
- Two **element kinds**: `NAND`, `LATCH`.

## 2. On-chain wire format

One event = one transaction with one `OP_RETURN` output. The pushed data is:

```
[ typeByte(1) ][ version(1)=0x31 ][ fields… ]
```

`decode` rejects any payload whose second byte is not `0x31`. Type bytes and layouts:

| type | byte | fields (offset from 0) | total |
| --- | --- | --- | --- |
| `factory` | `0x46` | `[2..18) factoryId` · `[18..22) supplyNAND u32` · `[22..26) supplyLATCH u32` · `[26..28) multiplier u16` · `[28] symLen` · `[29..29+symLen) symbol utf8` | `29+symLen` |
| `mint` | `0x4d` | `[2..18) factoryId` · `[18] kind (0=NAND,1=LATCH)` · `[19..23) amount u32` · `[23..31) owner8` | `31` |
| `netlist` | `0x4e` | `[2..) netlist bytes (inline)` | `2+len` |
| `task` | `0x4b` | `[2..18) taskId` · `[18] nIn` · `[19] nOut` · `[20] nameLen` · `[21..21+nameLen) name` · `[21+nameLen] refLen` · `[22+nameLen.. ) ref bytes` | `22+nameLen+refLen` |
| `tapeout` | `0x58` | `[2..18) factoryId` · `[18..34) circuitId(16)` · `[34..50) taskId` · `[50..58) owner8` | `58` |
| `tapeoutDA` | `0x59` | `[2..18) factoryId` · `[18..50) circuitId(32)` · `[50..66) taskId` · `[66..74) owner8` | `74` |
| `transfer` | `0x54` | `[2..18) factoryId` · `[18] kind` · `[19..23) amount u32` · `[23..31) from owner8` · `[31..39) to owner8` | `39` |
| `stateRoot` | `0x52` | `[2..34) root(32)` · `[34..38) height u32` · `[38..42) events u32` · `[42..74) setHash(32)` | `74` |

Every asset payload is ≤ 80 bytes. `stateRoot` is **not** an asset event (§8.3).

## 3. Canonical identity

### 3.1 Content ids

```
factoryId = hash16({ symbol, supplyNAND, supplyLATCH, multiplier })
circuitId = hash16({ netlist: <canonical netlist bytes as hex> })   // inline path
circuitId = hash32(netlist)                                          // DA path (32 bytes)
taskId    = hash16({ name, kind: "comb", nIn, nOut, ref })
```

`circuitId` must match the netlist bytes the event commits to; an indexer re-hashes and
rejects a mismatch. DA circuits publish the full 32-byte `circuitId` and the netlist lives
off-chain, content-addressed (see [`SCALE-SPEC.md`](./SCALE-SPEC.md)).

### 3.2 Owner identity

An owner is always carried explicitly as an 8-byte id. There are two namespaces:

- **address** — a bech32 segwit address. Its id is domain-separated and **script-derived**, so
  every HRP spelling (`tb1…` / `bc1…` / `bcrt1…`) of the same key maps to one id:

  ```
  addressOwnerId(addr) = sha256("FTO/owner/addr/v1" || witnessVersion(1) || program)[:8]
  ```

- **label** — anything else (e.g. `"alice"`). Its id is `sha256(utf8(label))[:8]`. A label is
  reachable only by whoever knows the preimage and is **not** settleable by the wallet market.

`owner:<hex8>` is the canonical textual form and passes through unchanged. For backward
compatibility, two **frozen alias** ids from the pre-#16 era are remapped to their canonical
ids at read time (see `fto/src/owner.js`).

Balance key: `` `${factoryId}|${kind}|owner:${id}` ``.

## 4. Event semantics (state transition)

The indexer maintains: `factories` (id → `{symbol,supplyNAND,supplyLATCH,multiplier}`),
`minted` (`` `${factoryId}|${kind}` → amount ``), `balances` (balance key → amount),
`circuits`, `tasks`, `best` (taskId → best circuitId), `pod`.

| event | effect |
| --- | --- |
| `factory` | register `factoryId`; `factoryId` must equal `hash16(core)` |
| `mint` | `balances[key] += amount` and `minted[key] += amount`; the factory must exist and the mint must not exceed the factory's supply (per kind) |
| `transfer` | `balances[from] -= amount`; `balances[to] += amount`; `from` must hold `amount`; authorized (§6) |
| `tapeout` | burn `nand`/`latch` elements from the factory's supplies at `owner`; record a circuit; debit `owner` is authorized (§6) |
| `tapeoutDA` | same as `tapeout`, DA `circuitId` |
| `netlist` | prerequisites for a `tapeout` (inline netlist bytes) |
| `task` | register a task; `taskId = hash16({name,kind:"comb",nIn,nOut,ref})` |
| `stateRoot` | **ignored** by the asset transition (commitment only, §8.3) |

PoD weight (deterministic, no I/O): `weight = (gates + (isBest ? bonusGates : 0)) * multiplier`
where `gates` is the burn cost of the circuit, and `isBest` iff this circuit is the first
best solution of its task. `bonusGates` is a fixed parameter (`32` in the reference tests).

**Invalid events are skipped and recorded** in `errors` (with index/type/reason); they do not
stop the replay. This is what makes the digest a pure function of the ordered event log.

## 5. Canonical ordering

Events are ordered by:

1. block `height` ascending (unconfirmed = `null`/`Infinity` sorts last, fail-closed),
2. within a block, position (`pos`) ascending,
3. within a tx, a deterministic per-event rank,
4. tie-break by `txid`.

Concrete rule: `rawSort` in `fto/src/logical.js`. Reorg safety follows from the covering
commitment (setHash) in §8.3.

## 6. Authorization (rule "G4")

**Network parameter `AUTH_HEIGHT`** (testnet: `1577499`).

For an event with a finite `height < AUTH_HEIGHT`, the pre-fork rule applies: `from`/`owner`
fields are trusted as carried. For `height >= AUTH_HEIGHT` (and for unconfirmed events), the
debited owner **must be one of the signers of the tx** that carries the event:

- `signers` = the owner ids of `vin[].prevout.scriptpubkey_address` (address scripts only).
- A `transfer`/`tapeout` whose debited owner is not in `signers` is **rejected**
  (`unauthorized transfer` / `unauthorized tapeout`).

Hand-built logical logs with no `height` keep the pre-fork rule (used only in tests).

## 7. Economics (rule "G9")

**Network parameter `ECON_HEIGHT`** (testnet: `1577517`). Gate on the **factory's** created
height (`since`).

1. **Creator binding.** The debited signer of a factory's *first accepted mint*
   (`signers[0]`) becomes that factory's **creator**. Every later mint of the same factory
   must include the creator among its signers, else it is rejected (`unauthorized mint`).
2. **Active rule.** A factory created at/after `ECON_HEIGHT` earns a canonical `F` row (§8.1)
   **only once it has been minted** (its keys appear in `minted`). An empty factory contributes
   **nothing** to the digest/state root, so factory spam cannot bloat state.

A factory with `since < ECON_HEIGHT` (or no height) keeps the pre-fork rule and is resident
from creation. Unconfirmed (`null`/`Infinity`) is bound (fail-closed).

## 8. Canonical state and commitments

### 8.1 Canonical rows

`stateRows(st)` is an ordered list of strings:

```
F|<factoryId>|<symbol>|<supplyNAND>|<supplyLATCH>|<multiplier>       # sorted by factoryId
M|<factoryId>|<kind>|<mintedAmount>                                  # sorted by key
B|<factoryId>|<kind>|owner:<id>|<balance>                            # sorted by key
```

Inactive post-fork factories (no mint) contribute no `F` row (§7).

### 8.2 State digest and state root

```
digest = sha256( utf8( stateRows(st).join("\n") ) )                  # 32 bytes, hex
leaf(row)        = sha256( utf8("FTO/state-root/v1\n" + row) )
stateRoot(rows)  = merkleRoot( [ leaf(r) for r in rows ] )           # VCP layout
EMPTY_STATE_ROOT = sha256("FTO/state-root/v1/empty")
```

The Merkle layout is VCP's (`vcp/src/merkle.js`): parent = `sha256(left ‖ right)`, an odd
node duplicates. The empty state has the fixed `EMPTY_STATE_ROOT`. The digest and the root are
over the **same rows**, so they can never disagree about what "state" is.

### 8.3 On-chain commitment (`stateRoot`)

A `stateRoot` OP_RETURN anchors a root together with the covered `height`, the covered
`events` count, and `setHash = sha256(join("\n", sorted(coveredTxids)))`. To verify:

1. take all asset events with `height <= commitment.height`,
2. recompute `stateRoot` and `setHash` from those events,
3. require root, setHash and count to match.

A reorg that changes the covered prefix changes `setHash`, so it is detected, not silently
accepted. A `stateRoot` event is never applied to the state and never enters the digest.

## 9. Golden vectors

[`fto/spec/vectors.json`](./spec/vectors.json) contains self-contained cases
(`events` → `expect.digest`, `expect.root`). Reproduce with:

```sh
node fto/tools/spec_vectors_check.mjs            # JS + Python + C# must agree with the file
node fto/tools/spec_vectors_check.mjs --emit     # regenerate expectations from JS (maintainers)
```

The Python (`fto/verify_indexer.py`) and C# (`fto/third-indexer/`) reference implementations
are independent and must print the same digest for every vector.

## 10. Reference implementations

| impl | path | role |
| --- | --- | --- |
| JS (canonical) | `fto/src/{wire,logical,indexer,econ,auth,stateroot}.js` | edge + Node CLI/SDK |
| Python | `fto/verify_indexer.py` | independent cross-check |
| C# / .NET | `fto/third-indexer/` | independent cross-check |
| Client | `fto/src/client.js` | chain-direct recompute in a browser/Node |

Run the full suite: `node tools/verify_all.mjs`.

## 11. Governance, freeze, and compatibility

- The wire version byte is `0x31`. A future incompatible format gets a new byte/version and a
  new spec version (`FTO/v2`), not a silent change.
- The rule constants and content ids are **frozen** and guarded by
  `node tools/check_contracts.mjs` (24 contracts). They are **not unilaterally changeable**:
  a change would alter the digest/root that already-committed anchors attest to.
- Fork activation is expressed only as **network parameters** (`AUTH_HEIGHT`, `ECON_HEIGHT`).
  Events below a parameter keep their historical interpretation byte-for-byte, so history is
  never rewritten.
- Testnet values in this document are provisional; mainnet values are set at the mainnet
  cutover and published as `FTO/v1` (mainnet parameters).

## 12. Known limitations (deferred)

- No BTC burn / recipient consent on mint (residual R15; see [`G9-ECONOMICS.md`](../G9-ECONOMICS.md)).
- No on-chain governance; parameters are set at deploy time.
- DA netlist availability is an off-chain obligation (incentivized separately).
- Mainnet is not live; fail-closed write guards remain in force.

---

*Changes to this document are versioned. The machine-checkable truth is `vectors.json` +
`node tools/verify_all.mjs`.*
