Tinylayer home

Tinylayer Enclavia signer

tinylayer-enclave is the only trusted stateful component in protocol 2. It holds one global BIP340 key and a compact rollback-protected row per activated coin. It signs BIP446 TemplateHash messages; it does not parse Bitcoin update transactions.

Read the protocol specification before deployment. The complete compiled signer/workload source is rendered directly from src/lib.rs.

Responsibilities

The workload:

The wallet remains responsible for the funding outpoint and for reconstructing and validating every Bitcoin transaction, Taproot tree, amount, signature, locktime, history entry, confirmation, and outspend.

Protected state

global signing secret

coin ID → {
    current capability hash,
    state number,
    latest template hash,
    activation request,
    exact last request and response,
}

The default capacity is 21,000 active rows. The workload must run with Enclavia’s rollback/fork protection. Restoring an older state while retaining the same global key would allow sibling signatures and is outside the protocol.

HTTP API

The workload listens on port 8080:

GET  /health
POST /v2

All typed calls use one strict JSON endpoint.

Service info

{"method":"info"}

Response:

{
  "method": "info",
  "result": {
    "protocol_version": 2,
    "signing_pubkey": "<x-only key>"
  }
}

info is stateless. Wallets call it before constructing a funding address.

Activation

{
  "method": "activate",
  "params": {
    "coin_id": [0, 1],
    "initial_capability_hash": [0, 1],
    "initial_template_hash": [0, 1]
  }
}

Arrays above are abbreviated; wire values contain exactly 32 integers.

The activation request is intentionally Bitcoin-opaque. The workload receives no transaction, outpoint, amount, script, proof, network name, explorer URL, or RPC configuration. It performs no network I/O.

Exact activation replay returns the existing status. A different activation for the same coin ID fails. Because the signer cannot distinguish a real coin from an invented ID, external admission or rate limiting is required before exposing activation to an adversarial public network.

Status

{"method":"status","params":{"coin_id":[0,1]}}

Status exposes the global key, current capability hash, state number, and latest template hash. It never exposes capabilities or private keys.

Sign

{
  "method": "sign",
  "params": {
    "coin_id": [0, 1],
    "current_capability": [0, 1],
    "next_capability_hash": [0, 1],
    "next_state_number": 2,
    "template_hash": [0, 1]
  }
}

The signer requires exactly stored state + 1, a changed capability, and a changed template hash. It signs the raw 32-byte BIP446 hash with BIP340 and persists the new state before returning.

A dropped response is handled by resending the exact request. A different request with the old capability is rejected.

Why one global key

Protocol 1 generated a key before funding. Protocol 2 publishes one global key so the wallet can derive its address locally. The BIP448 bootstrap leaf guarantees a safe initial state without a server signature, allowing allocation to move to the first transfer.

A global key increases blast radius if enclave isolation fails, but no valid Bitcoin update can spend without the separate client signature.

Build

cargo build --locked --release \
  -p tinylayer-enclave \
  --features workload \
  --bin tinylayer-workload

The Docker image builds the same workload. /health only proves the process is reachable; it does not prove attestation, admission policy, or coin state.

Local debug

Start the Bitcoin-opaque workload directly:

cargo run -p tinylayer-enclave --features workload --bin tinylayer-workload

The CLI may connect directly only when initialized with --unsafe-plaintext. Never use that mode for secrets or public deployment.

Deployment invariants

Wallets independently verify funding and chain state. External activation admission protects capacity but is deliberately outside the measured signer.