<!-- source: https://www.secondlayer.tools/docs/verification -->

# Verify

Trust, and verify.

Where Secondlayer can prove an answer, it serves the proof: a hash, a merkle path, a signature, or a replay you run yourself. A check that passes leaves you trusting a smaller, named thing. A check that can't run says so, and is never a pass.

## What you can check

Every layer has a check. Each one leaves you trusting a smaller, named thing.

## Check a block

One command walks every link from the checkpoint to the block's state. It works at any height, including below the checkpoint.

Try it in your browser, no install:

```text ~/secondlayer exit=0
$ secondlayer verify block 9137005
✓ block       9,137,005  2e5962b00b8f243e1a4b5a1b7ff3076618ce5e737e30c019bc61e461c92f57ff
✓ bitcoin     headers 967,680..970,269, proof-of-work and retargets checked
✓ burn        height 970,269  00000000000000000001abf5e92c4e771c041ff3a9fde450c2291775641a801c
✓ cycle       144, signer set proven forward from checkpoint cycle 143
✓ signatures  2,866 / 4,000 weight signed, threshold 2,800
✓ txs         2 transactions match the tx merkle root
✓ state root  8103be4b31c6549fe58dd453d9ea357286c874ac0f4f81f4326791e8191f0cd5 matches the witness
✓ diff        18 written (unnamed), 1 carried, 5 internal
· rows        not checked (pass --rows)
✓ Block 9,137,005 is proven. Trusted: only the checkpoint (Stacks 8,956,304, Bitcoin 967,680).
```

## Prove a transaction

`verifyTransactionProof` recomputes the txid, merkle root, block hash, and signer weight from one proof.

```bash
curl "$SECONDLAYER_API_URL/v1/index/transactions/0x<tx_id>/proof" \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```ts
import { verifyTransactionProof } from "@secondlayer/sdk";

const proof = await fetch(
  `${process.env.SECONDLAYER_API_URL}/v1/index/transactions/${txid}/proof`,
  { headers: { Authorization: `Bearer ${process.env.SECONDLAYER_API_KEY}` } },
).then((response) => response.json());

const result = verifyTransactionProof(proof);
// result.ok === true, result.level === "consensus"
// result.signerWeightBps ~ 7000+, result.rewardSetSource === "embedded"
```

### Proof levels

A `200` returns the raw transaction, raw header, merkle path, and (only when the reward set could be resolved) `consensus`.

| Level | Checks |
| --- | --- |
| `anchored` | `txid` to merkle path to the header's `tx_merkle_root`, plus header self-consistency. Any Stacks node can corroborate it. |
| `consensus` | Also recovers the header's signer signatures and confirms at least 70% of the reward cycle's signer weight signed. |

```ts
type TransactionProofVerifyResult = {
  level: "anchored" | "consensus";
  txidMatches: boolean;
  includedInHeader: boolean;
  headerSelfConsistent: boolean;
  signerWeightBps?: number;
  thresholdMet?: boolean;
  // "embedded" trusts Secondlayer for the signer set.
  rewardSetSource?: "provided" | "embedded";
  ok: boolean;
  errors: string[];
};
```

With no reward set, consensus uses the one embedded in the proof, which trusts Secondlayer for the signer set alone. `fetchRewardSet` resolves it from your own stacks-node (`/v3/stacker_set/{cycle}`) instead:

```ts
import { verifyTransactionProof, fetchRewardSet } from "@secondlayer/sdk";

const rewardSet = await fetchRewardSet({
  nodeUrl: "https://your-stacks-node:20443",
  cycle: proof.consensus.reward_cycle,
});
if (!rewardSet) throw new Error("your node has no reward set for that cycle");

const trustless = verifyTransactionProof(proof, { rewardSet });
// trustless.rewardSetSource === "provided"
```

| Status / code | Meaning |
| --- | --- |
| `404` `PROOF_UNAVAILABLE` | The transaction, or the block containing it, was not found. |
| `503` `PROOF_TX_SET_INCOMPLETE` | The server couldn't reproduce the block's `tx_merkle_root` from its stored transactions, so it refuses to emit a proof that wouldn't verify. |
| `503` `PROOF_NODE_UNAVAILABLE` | The signed-header source (a stacks-node) was unreachable. Retryable. |

The transaction verifier uses Node's crypto (via `@secondlayer/shared`), so it is server-side only.

### Bitcoin transactions

`verifyBitcoinTx` follows a transaction from its bytes to its txid, merkle root and block header, then checks that header against the checkpoint's Bitcoin chain, with no contract call.

```ts
import { BlockVerifier, SecondlayerProofSource } from "@secondlayer/verify";

const sweepTxid = "f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b";
const verifier = new BlockVerifier({ source: new SecondlayerProofSource() });
const result = await verifier.verifyBitcoinTx(sweepTxid, {
  expect: { address: "bc1q9fkjx3s9ufz90k4jzgmdkzlyt22h0tp3x92ua9", amount: 398_000n },
});
// result.ok, result.height, result.matched (the output index)
```

To read the proof from your own Bitcoin node, spread a source over it. The node needs `txindex=1`, and the verifier re-checks every byte, so an Esplora endpoint works the same way.

```ts
import { SecondlayerProofSource } from "@secondlayer/verify";
import { bitcoinRpcSource, buildTxProof } from "@secondlayer/stacks/bitcoin";

const bitcoin = bitcoinRpcSource({
  url: "http://localhost:8332",
  auth: { username: "stacks", password: process.env.BITCOIN_RPC_PASSWORD ?? "" },
});
const source = {
  ...new SecondlayerProofSource(),
  getBitcoinTxProof: (txid: string) => buildTxProof(bitcoin, { txid }),
};
```

Pair it with a state proof of sBTC `completed-withdrawal-sweep` to follow a withdrawal from Stacks state to its Bitcoin output.

## Blocks below the checkpoint

A block below the checkpoint skips the Bitcoin and signer links. The checkpoint's header commits to every ancestor by hash, so an `ancestry` link ties the block to it, by parent links or one MARF proof.

- The skipped links read `not needed: the block is hash-chained to the checkpoint`.
- `txs` reads `not checked: no transaction bytes` for an epoch 2.x block or when the source served the header only.

## Exit codes and cost

| Exit | Meaning |
| --- | --- |
| `0` | Every link holds |
| `1` | A link is broken; the first is named |
| `2` | Could not check |

The tradeoff: a lying source can fail a link but never pass one, and the price is work. The first check syncs Bitcoin headers from the checkpoint, and each block fetches a state witness.

## Flags

| Flag | Effect |
| --- | --- |
| `--node <url>` | Read blocks and MARF proofs from your Stacks node instead of the API |
| `--checkpoint <file>` | Trust your own checkpoint JSON instead of the one built into the release |
| `--rows` | Also check the indexed `vm_events` rows against the diff. Billed as Index rows; everything else is free |
| `--json` | Full result on stdout |

## In your code

`@secondlayer/verify` is the same check as a library.

```bash
bun add @secondlayer/verify
```

`verifyBlock` never throws for bad source data: it returns `ok: false` and the first broken link.

```ts verify-block.ts
import { SecondlayerProofSource, verifyBlock } from "@secondlayer/verify";

// No key needed; add apiKey for higher rate limits.
const source = new SecondlayerProofSource({
  baseUrl: process.env.SECONDLAYER_API_URL!,
});

// Never throws on bad source data.
const result = await verifyBlock(9_137_005, { source });
// The first broken link.
if (!result.ok) throw new Error(result.failures[0]?.message);
console.log(result.blockId, result.stateRoot, result.diff?.writes.length);
```

With your own node, blocks and MARF proofs come from its RPC. Witnesses, burn preimages and Bitcoin headers still come from the API, checked the same way. Use `BlockVerifier` to verify many blocks from one checkpoint and reuse the synced headers and signer sets.

```ts
import {
  BlockVerifier,
  NodeRpcProofSource,
  SecondlayerProofSource,
} from "@secondlayer/verify";

const api = new SecondlayerProofSource({ baseUrl, apiKey });
const source = { ...api, ...new NodeRpcProofSource({ nodeUrl: "http://localhost:20443" }) };

const verifier = new BlockVerifier({ source });
const result = await verifier.verify(9_137_005);
```

Pass `rows: true` to also check the indexed `vm_events` rows (billed as Index rows).

- `result.transactions` holds the block's checked transactions; `result.writes` its named writes.
- The library uses `fetch` and pure-JS crypto (`@noble`), with no Node built-ins.

## Archive assurance levels

The manifest states a level per range. Most of the chain is `db-reconstructive`.

A published `node.json` upgrades only its `coverage` window.

| Level | Means |
| --- | --- |
| `db-reconstructive` | Exports from one repeatable-read snapshot restore deterministically. Does not prove no historical callback was omitted. |
| `node-attested` | Block identities also match an independently synced stacks-node. Transactions and events stay `db-reconstructive`. |
| `observer-attested` | Digests match independently retained raw observer history. |

## Proofs API and limits

The HTTP proofs behind every check, and what can't be proven yet.

### Proofs API

The raw material `verify block` reads. Free, never metered, no key needed.

On the hosted API the limits are per IP, and an account key doubles them:

| Route | Per IP | With an account key |
| --- | --- | --- |
| Any proofs route | 10/s | 20/s |
| Witnesses | 1/s | 2/s |

Answers addressed by a hash never change: they carry an ETag and `Cache-Control: immutable`.

On your own box the witness, burn and Bitcoin-header routes need a proof sidecar: [Self-host](/docs/self-host#what-differs-on-your-box).

| Route | Returns |
| --- | --- |
| [`/v1/proofs/block/height/{height}`](/docs/api/proofs#get-signed-block-by-height) | Signed block by height |
| [`/v1/proofs/block/{index_block_hash}`](/docs/api/proofs#get-signed-block) | Signed block by id |
| [`/v1/proofs/witness/{index_block_hash}`](/docs/api/proofs#get-state-witness) | State witness the header's state root must hash to |
| [`/v1/proofs/writes/{height}`](/docs/api/proofs#get-block-state-writes) | Every written leaf of the block, named |
| [`/v1/proofs/marf/{path}`](/docs/api/proofs#get-marf-proof) | MARF inclusion proof for a key at a block |
| [`/v1/proofs/burn/{consensus_hash}`](/docs/api/proofs#get-burn-block-proof) | Burn block a consensus hash commits to |
| [`/v1/proofs/bitcoin-headers`](/docs/api/proofs#list-bitcoin-headers) | Raw Bitcoin headers from a height |
| [`/v1/proofs/bitcoin-tx/{txid}`](/docs/api/proofs#get-bitcoin-tx-proof) | Bitcoin transaction, merkle branch and header |
| [`/v1/proofs/epoch2-header/{index_block_hash}`](/docs/api/proofs#get-epoch2-header) | Epoch 2.x header by id |

The transaction proof above is [`/v1/index/transactions/{tx_id}/proof`](/docs/api/index#get-transaction-proof).

### What it does not cover yet

| Gap | Effect |
| --- | --- |
| Print events and transaction results | Stacks headers don't commit to them, so no block check covers them. |
| Which transaction made a write | The transaction root binds each transaction's bytes, not which one wrote a leaf or whether it succeeded. |
| `events` and `none` subgraphs | Only `state` subgraphs can be replayed. [Rows anyone can recompute](/docs/subgraphs#rows-anyone-can-recompute). |
| The checkpoint | Trusted as-is. Pass your own to change that. |
| Bitcoin reorgs | Headers are checked for valid work, not against a competing chain, and reorgs past the synced tip are not followed. |
| `__MARF_*` keys from a node alone | A node serves no proof for them, so with `NodeRpcProofSource` alone, blocks more than 16 below a trusted block fail `ancestry` as `unavailable`. |
| Epoch 2.x transactions | Skipped with a note. |
| Unnamed writes | Without `state_writes` for the block, writes are proven, not named. |

## Notes for agents

- 'secondlayer verify block <height>' prints each link (bitcoin, burn, cycle, signatures, txs, state root, diff). Exit 0, 1, 2 as documented. Only the checkpoint stays trusted.
- In code: @secondlayer/verify, verifyBlock(height, { source }) with a SecondlayerProofSource. For own-node proofs use a BlockVerifier with NodeRpcProofSource spread over it. When ok is false, throw result.failures[0].message.
- Checks: webhook delivery (verifySecondlayerSignature, trusts the key); subgraph rows (verify subgraph <name> --replay, state only); transaction in block (/v1/index/transactions/:tx_id/proof, trusts the signer set); Streams frames (SDK, trusts the key); history vs archive (verify raw, trusts the archive key + assurance level); any block (verify block <height>, trusts the release checkpoint).
- Transaction proof: '/v1/index/transactions/<tx_id>/proof' then verifyTransactionProof(proof) from @secondlayer/sdk server-side. For full trustlessness pass fetchRewardSet({ nodeUrl, cycle }) from the user's own node as { rewardSet } until rewardSetSource is 'provided'.
- Handle '404 PROOF_UNAVAILABLE'; retry '503 PROOF_NODE_UNAVAILABLE'.

## Setup

Ensure setup once, skipping any step already done:
- Skill: `bunx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `bun add -g @secondlayer/cli`
- Instance: `secondlayer init` writes `INSTANCE_TOKEN`. Loopback `/v1` reads need no key.
- Hosted/archive: export `SECONDLAYER_API_KEY` (`sk-sl_*`) or buy credits with `secondlayer credits buy` (`balance`, `refill`), then `secondlayer whoami`
