Reference / Verification

Verification

Prove a transaction is in a Stacks (Nakamoto) block, and that the cycle's signers attested to it, without trusting Secondlayer.

  • Anchored: txid → merkle path → the header's tx_merkle_root, plus header self-consistency (block_hash, index_block_hash from raw_header). Any Stacks node can corroborate it.
  • Consensus: also recovers the header's signer signatures and confirms ≥70% of the reward cycle's signer weight signed the block. Fully trustless when you supply the reward set yourself (see Fully trustless).
curl "https://api.secondlayer.tools/v1/index/transactions/0x<tx_id>/proof"

Open during beta, no key required. A 200 returns the raw transaction, raw header, merkle path, and — only when the reward set could be resolved — consensus:

{
  "txid": "<hex>",
  "index_block_hash": "<hex>",
  "block_height": 8199502,
  "tx_index": 0,
  "raw_tx": "<hex>",
  "raw_header": "<hex>",
  "tx_merkle_path": [{ "position": "left", "hash": "<hex>" }],
  "consensus": {
    "reward_cycle": 136,
    "reward_set": {
      "signers": [{ "signing_key": "<hex>", "weight": 51 }],
      "total_weight": 3862
    }
  }
}

Without consensus the proof is anchored-only.

Errors

Status / codeMeaning
404 PROOF_UNAVAILABLEThe transaction, or the block containing it, was not found.
503 PROOF_TX_SET_INCOMPLETEThe server couldn't reproduce the block's tx_merkle_root from its stored transaction set, so it refuses to emit a proof that wouldn't verify.
503 PROOF_NODE_UNAVAILABLEThe signed-header source (a stacks-node) was unreachable. Retryable.

verifyTransactionProof recomputes the txid, merkle root, block hash, and signer weight client-side.

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

const proof = await fetch(
  `https://api.secondlayer.tools/v1/index/transactions/${txid}/proof`,
).then((r) => r.json());

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

The result describes exactly what was checked:

type TransactionProofVerifyResult = {
  level: "anchored" | "consensus";
  txidMatches: boolean;
  includedInHeader: boolean;
  headerSelfConsistent: boolean;
  signerWeightBps?: number;
  thresholdMet?: boolean;
  rewardSetSource?: "provided" | "embedded";
  ok: boolean;
  errors: string[];
};

Pass no reward set and consensus uses the one embedded in the proof (rewardSetSource: "embedded"), which still trusts Secondlayer for the signer set alone.

fetchRewardSet resolves the reward set from your own stacks-node (/v3/stacker_set/{cycle}):

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

const rewardSet = await fetchRewardSet({
  nodeUrl: "https://your-stacks-node:20443",
  cycle: proof.consensus.reward_cycle,
});

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

Verification uses Node's crypto (via @secondlayer/shared) — server-side only.

SDK for verifyTransactionProof and fetchRewardSet; REST API for the proof endpoint.