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'stx_merkle_root, plus header self-consistency (block_hash,index_block_hashfromraw_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 / 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 transaction set, 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. |
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.