@secondlayer/verify 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,13 @@ checkpoint -> Bitcoin headers (proof of work) -> burn block -> signer set (MARF
9
9
  -> header signatures (70% of signer weight) -> state witness (state root) -> state diff
10
10
  ```
11
11
 
12
+ Blocks below the checkpoint take a shorter chain. The checkpoint's header commits to every ancestor by hash, so no signatures are needed:
13
+
14
+ ```
15
+ checkpoint -> block id at that height (parent links, or one MARF proof) -> header hashes to the id
16
+ -> state witness (state root) -> state diff
17
+ ```
18
+
12
19
  Every byte comes from a source you don't have to trust. A lying source can make a link fail, never pass. Powers `secondlayer verify block` in [`@secondlayer/cli`](https://www.npmjs.com/package/@secondlayer/cli).
13
20
 
14
21
  ## Install
@@ -43,6 +50,8 @@ const source = {
43
50
  };
44
51
  ```
45
52
 
53
+ Heights below the checkpoint need no extra options: `verifyBlock(150_000, { source })` derives the direction. Within 16 blocks of a trusted block the verifier follows `parent_block_id` links. Farther down it asks the source for one MARF proof of `__MARF_BLOCK_HEIGHT_TO_HASH::<height>` at the checkpoint: each block writes its parent's id there, so the proof names the ancestor and is checked against the checkpoint's state root. Pre-Nakamoto (2.x) blocks need a source with `getEpoch2Header`, which both sources here implement.
54
+
46
55
  `verifyBlock` never throws for bad source data: it returns `ok: false` with `failures`, the first entry being the first broken link. Use `BlockVerifier` to verify many blocks from one checkpoint and reuse the synced Bitcoin headers and proven signer sets.
47
56
 
48
57
  By default only proofs are read, and proofs are free. Pass `rows: true` to also name every write and check the indexed rows: the source's `state_writes` and `vm_events` reads bill as Index rows on the Secondlayer API.
@@ -62,11 +71,14 @@ await verifyBlock(9_137_005, { source, rows: true });
62
71
  | Its state | the witness recomputes the header's state root |
63
72
  | Every key it wrote | `rows: true` and a source with `state_writes`: each written leaf is named, nothing hidden |
64
73
  | Indexed rows | `rows: true`: each `vm_events` row matches a leaf the block wrote |
74
+ | Below the checkpoint: it is the checkpoint chain's block at that height | parent links or a MARF proof from the checkpoint's state root, then the header (Nakamoto or 2.x) hashes to that id |
65
75
 
66
76
  ## What it trusts, and what it doesn't cover
67
77
 
68
78
  - **The checkpoint.** `MAINNET_CHECKPOINT` (Stacks block 8,956,304, cycle 143 signers, Bitcoin block 967,680) is trusted as-is. Pass `checkpoint` to use your own.
69
- - **Bitcoin heights.** Blocks whose burn height is below the checkpoint's Bitcoin block can't be verified. Headers are checked for valid work, not compared against a competing chain, and reorgs past the synced tip are not followed.
79
+ - **Bitcoin heights.** At or above the checkpoint, a block's burn block must be at or above the checkpoint's Bitcoin block. Below the checkpoint the burn block is checked only when it is (with the baked checkpoint it never is); the hash chain pins those blocks instead. Headers are checked for valid work, not compared against a competing chain, and reorgs past the synced tip are not followed.
80
+ - **MARF proofs of `__MARF_*` keys.** A node's `/v2/clarity/marf` answers only keys with a stored value string, and these have none. The API's `/v1/proofs/marf` falls back to its proof sidecar for them; with `NodeRpcProofSource` alone, blocks more than 16 below a trusted block fail `ancestry` as `unavailable`.
81
+ - **Epoch 2.x consensus hashes.** A 2.x header commits to its parent's block hash, not its id. The id always comes from a trusted descendant (its header, or a MARF proof), and the source's consensus hash must hash with the header to it. State proofs for 2.x heights are against the anchored block's root, which covers the microblocks it confirms.
70
82
  - **Names need `rows: true` and `state_writes`.** Without them every write is still proven, but unnamed; `notes` says so.
71
83
  - **Not covered:** print events (Stacks headers don't commit to them) and transaction contents.
72
84
 
package/dist/index.d.ts CHANGED
@@ -30,8 +30,43 @@ declare function parseNakamotoHeader(raw: Bytes): NakamotoHeader;
30
30
  * except the signer signatures. Also the header's block_hash.
31
31
  */
32
32
  declare const signerSignatureHash: (h: NakamotoHeader) => Bytes;
33
- /** StacksBlockId = sha512/256(block_hash || consensus_hash). */
34
- declare const blockId2: (h: NakamotoHeader) => Bytes;
33
+ /**
34
+ * Epoch 2.x anchored block header. It commits to the parent's block hash, not
35
+ * its index block hash, and carries no consensus hash: the source supplies
36
+ * that, and `blockId` binds it to an id proven elsewhere.
37
+ */
38
+ interface Epoch2Header {
39
+ version: number;
40
+ /** total_work.burn: burn spent on the fork so far. */
41
+ burnSpent: bigint;
42
+ /** total_work.work: the Stacks chain length (one per anchored block). */
43
+ chainLength: bigint;
44
+ vrfProof: Bytes;
45
+ parentBlockHash: Bytes;
46
+ parentMicroblock: Bytes;
47
+ parentMicroblockSequence: number;
48
+ txMerkleRoot: Bytes;
49
+ /** Covers the block and the parent microblock stream it confirms. */
50
+ stateIndexRoot: Bytes;
51
+ microblockPubkeyHash: Bytes;
52
+ /** sha512/256 of the serialized header. */
53
+ blockHash: Bytes;
54
+ consensusHash: Bytes;
55
+ }
56
+ type StacksHeader = NakamotoHeader | Epoch2Header;
57
+ /**
58
+ * Parse the epoch 2.x StacksBlockHeader at the start of `raw` (a header from
59
+ * `/v2/headers`, or block bytes from `/v2/blocks/<id>`), with the 20-byte
60
+ * consensus hash of the sortition that elected it.
61
+ */
62
+ declare function parseEpoch2Header(raw: Bytes, consensusHash: Bytes): Epoch2Header;
63
+ declare const isEpoch2Header: (h: StacksHeader) => h is Epoch2Header;
64
+ /**
65
+ * StacksBlockId (index block hash) = sha512/256(block_hash || consensus_hash).
66
+ * A Nakamoto block hash is its signer signature hash; a 2.x one hashes the
67
+ * whole header.
68
+ */
69
+ declare const blockId2: (h: StacksHeader) => Bytes;
35
70
  /** TrieHash::from_key: sha512/256(utf8(key)). */
36
71
  declare const marfPath: (key: string) => Bytes;
37
72
  /** MARFValue::from_value: sha512/256(utf8(value)) padded to 40 bytes. */
@@ -59,7 +94,7 @@ interface MarfProofInput {
59
94
  * vouched for by header bytes (a bare root->id map would let a forged
60
95
  * ancestor trie through).
61
96
  */
62
- headers: NakamotoHeader[];
97
+ headers: StacksHeader[];
63
98
  }
64
99
  /**
65
100
  * TrieMerkleProof::verify. True iff `value` is stored at `path` in the MARF
@@ -257,8 +292,8 @@ interface VerifyCheckpoint {
257
292
  /**
258
293
  * Mainnet: Stacks block 8,956,304 (`3914ea5f…74bb`) holding the cycle 143
259
294
  * signer set (29 signers, weight 4000), and Bitcoin block 967,680 (a
260
- * retarget-period start inside cycle 143). Verifies blocks whose burn
261
- * height is 967,680 or later.
295
+ * retarget-period start inside cycle 143). Later blocks verify forward
296
+ * (burn height 967,680 or later); earlier ones by hash chain from 8,956,304.
262
297
  */
263
298
  declare const MAINNET_CHECKPOINT: VerifyCheckpoint;
264
299
  /** Sortition preimage behind a consensus hash. */
@@ -297,10 +332,26 @@ interface VmEventRow {
297
332
  raw_key?: string | null;
298
333
  raw_value?: string | null;
299
334
  }
335
+ /** An epoch 2.x header and the consensus hash its index block hash commits to. */
336
+ interface Epoch2HeaderResponse {
337
+ /** Consensus-serialized StacksBlockHeader. */
338
+ header: Bytes;
339
+ consensusHash: Bytes;
340
+ }
300
341
  interface ProofSource {
301
342
  /** Raw Nakamoto block bytes (header first), by block id or height. */
302
343
  getBlock(ref: string | number): Promise<Bytes>;
303
- /** MARF inclusion proof for a 32-byte path at `tipId`; null when the key is absent. */
344
+ /**
345
+ * Epoch 2.x block header by index block hash: blocks below the checkpoint
346
+ * from before Nakamoto. Without it only Nakamoto blocks verify.
347
+ */
348
+ getEpoch2Header?(blockId: string): Promise<Epoch2HeaderResponse>;
349
+ /**
350
+ * MARF inclusion proof for a 32-byte path at `tipId`; null when the key is
351
+ * absent. Blocks below the checkpoint also ask for
352
+ * `__MARF_BLOCK_HEIGHT_TO_HASH::<height>`, whose value is no stored string:
353
+ * only `proof` is read for it.
354
+ */
304
355
  getMarfProof(pathHex: string, tipId: string): Promise<MarfProofResponse | null>;
305
356
  /** State witness (wire v3) for a block. */
306
357
  getWitness(blockId: string): Promise<Bytes>;
@@ -327,14 +378,15 @@ interface SecondlayerSourceOptions {
327
378
  fetch?: Fetch;
328
379
  }
329
380
  /**
330
- * Every proof input from the Secondlayer API: `/v1/proofs/*` for blocks, MARF
331
- * proofs, witnesses, burn preimages and Bitcoin headers; the Index API for
381
+ * Every proof input from the Secondlayer API: `/v1/proofs/*` for blocks, epoch
382
+ * 2.x headers, MARF proofs, witnesses, burn preimages and Bitcoin headers; the Index API for
332
383
  * state_writes and vm_events, read only when verifyBlock gets `rows: true`.
333
384
  * Methods are own properties, so a partial source spreads over it:
334
385
  * `{ ...new SecondlayerProofSource(o), ...new NodeRpcProofSource(n) }`.
335
386
  */
336
387
  declare class SecondlayerProofSource implements ProofSource {
337
388
  getBlock: (ref: string | number) => Promise<Bytes>;
389
+ getEpoch2Header: (blockId: string) => Promise<Epoch2HeaderResponse>;
338
390
  getMarfProof: (pathHex: string, tipId: string) => Promise<MarfProofResponse | null>;
339
391
  getWitness: (blockId: string) => Promise<Bytes>;
340
392
  getBurnPreimage: (consensusHash: string) => Promise<BurnPreimage>;
@@ -349,11 +401,13 @@ interface NodeRpcSourceOptions {
349
401
  fetch?: Fetch;
350
402
  }
351
403
  /**
352
- * Blocks and MARF proofs straight from a Stacks node's RPC. Witnesses, burn
353
- * preimages and Bitcoin headers need another source: spread this over one.
404
+ * Blocks, epoch 2.x headers and MARF proofs straight from a Stacks node's RPC.
405
+ * Witnesses, burn preimages and Bitcoin headers need another source: spread
406
+ * this over one.
354
407
  */
355
- declare class NodeRpcProofSource implements Pick<ProofSource, "getBlock" | "getMarfProof"> {
408
+ declare class NodeRpcProofSource implements Pick<ProofSource, "getBlock" | "getEpoch2Header" | "getMarfProof"> {
356
409
  getBlock: (ref: string | number) => Promise<Bytes>;
410
+ getEpoch2Header: (blockId: string) => Promise<Epoch2HeaderResponse>;
357
411
  getMarfProof: (pathHex: string, tipId: string) => Promise<MarfProofResponse | null>;
358
412
  constructor(opts: NodeRpcSourceOptions);
359
413
  }
@@ -432,8 +486,8 @@ interface BlockStateResult {
432
486
  * `parentLeaves`, and the remaining writes come back unnamed.
433
487
  */
434
488
  declare function verifyBlockState(input: BlockStateInput): Promise<BlockStateResult>;
435
- type VerifyStep = "block" | "bitcoin" | "burn" | "signer-set" | "signatures" | "witness" | "names" | "rows";
436
- type VerifyFailureCode = StateFailureCode | "unavailable" | "id-mismatch" | "height-mismatch" | "invalid-header" | "preimage-mismatch" | "burn-not-in-chain" | "before-checkpoint" | "anchor-not-found" | "anchor-outside-window" | "anchor-unsigned" | "set-proof-invalid" | "below-threshold" | "unknown-signer" | "duplicate-signer" | "bad-signature";
489
+ type VerifyStep = "ancestry" | "block" | "bitcoin" | "burn" | "signer-set" | "signatures" | "witness" | "names" | "rows";
490
+ type VerifyFailureCode = StateFailureCode | "unavailable" | "id-mismatch" | "height-mismatch" | "invalid-header" | "preimage-mismatch" | "burn-not-in-chain" | "before-checkpoint" | "not-ancestor" | "ancestry-proof-invalid" | "anchor-not-found" | "anchor-outside-window" | "anchor-unsigned" | "set-proof-invalid" | "below-threshold" | "unknown-signer" | "duplicate-signer" | "bad-signature";
437
491
  interface VerifyFailure {
438
492
  step: VerifyStep;
439
493
  code: VerifyFailureCode;
@@ -446,13 +500,31 @@ interface VerifyFailure {
446
500
  path?: string;
447
501
  ordinal?: number;
448
502
  }
503
+ /** How a block below the checkpoint was tied to it. */
504
+ interface Ancestry {
505
+ /** Trusted descendant it was proven from: the checkpoint, or a block proven below it earlier. */
506
+ fromHeight: number;
507
+ fromBlockId: string;
508
+ /**
509
+ * `parents`: each header hashes to the parent id its child commits to.
510
+ * `marf`: one MARF proof of `__MARF_BLOCK_HEIGHT_TO_HASH::<height>` against
511
+ * the descendant's state root.
512
+ */
513
+ via: "parents" | "marf";
514
+ }
449
515
  interface BlockVerification {
450
- /** Every step passed: header, burn binding, signatures, state root, names and rows. */
516
+ /**
517
+ * Every step passed. At or above the checkpoint: header, burn binding,
518
+ * signatures, state root, names and rows. Below it: ancestry, header,
519
+ * state root, names and rows (and the burn binding when Bitcoin reaches it).
520
+ */
451
521
  ok: boolean;
452
522
  height?: number;
453
523
  blockId?: string;
454
524
  /** The header's state_index_root, hex: what the witness must hash to. */
455
525
  stateRoot?: string;
526
+ /** Set for blocks below the checkpoint, which need no signatures. */
527
+ ancestry?: Ancestry;
456
528
  cycle?: number;
457
529
  burnHeight?: number;
458
530
  /** Display-order hex. */
@@ -491,7 +563,10 @@ declare class BlockVerifier {
491
563
  private readonly sets;
492
564
  /** Where each proven set was anchored: the lower bound for the next search. */
493
565
  private readonly anchors;
566
+ /** Headers fetched by id, each checked to hash to it. */
494
567
  private readonly headers;
568
+ /** Blocks proven on the checkpoint's chain below it, by height. */
569
+ private readonly below;
495
570
  private readonly burns;
496
571
  /** Unverified source answers by consensus hash. */
497
572
  private readonly preimages;
@@ -500,9 +575,50 @@ declare class BlockVerifier {
500
575
  /** Verify one block by id (hex) or height. Never throws for bad source data. */
501
576
  verify(ref: string | number): Promise<BlockVerification>;
502
577
  private run;
503
- /** Header by id, from the cache or the source; the bytes must hash to `id`. */
578
+ /** At or above the checkpoint: Bitcoin, burn binding, signer set, signatures, state. */
579
+ private runForward;
580
+ /**
581
+ * Below the checkpoint: prove the checkpoint chain's id at `height` (and
582
+ * that it is `claimed`, when the caller asked by id), then the header that
583
+ * hashes to it, then its state. Signatures add nothing to a header pinned
584
+ * by hash from a trusted descendant.
585
+ */
586
+ private runBelow;
587
+ /**
588
+ * The checkpoint chain's block id at `height`, from the nearest trusted
589
+ * block above it: parent links when within MAX_WALK, else one MARF proof.
590
+ */
591
+ private ancestor;
592
+ /** The lowest trusted block above `height`: the checkpoint or one proven below it. */
593
+ private nearestAbove;
594
+ /**
595
+ * Follow parent ids down from `from`: each parent header must hash to the
596
+ * id its child committed to. Null when a 2.x header stops the walk: it
597
+ * commits to its parent's block hash, not the parent's id.
598
+ */
599
+ private walk;
600
+ /**
601
+ * Read the id at `height` from `from`'s state: block `height + 1` wrote it
602
+ * to `__MARF_BLOCK_HEIGHT_TO_HASH::<height>`, and the proof must recompute
603
+ * `from`'s state root through the header of every trie it crosses.
604
+ */
605
+ private jump;
606
+ /**
607
+ * Below the checkpoint the hash chain already pins the block; still bind
608
+ * its burn block when the Bitcoin checkpoint reaches it. The burn height
609
+ * here is the source's hint, so a low one only skips this extra check.
610
+ */
611
+ private bindBurnBelow;
612
+ /** Witness against the authenticated header's root, then the named diff and rows. */
613
+ private verifyState;
614
+ /** Header by id, from the cache or the source; it must hash to `id`. */
504
615
  private header;
505
616
  /**
617
+ * A Nakamoto block by id, or when the source has none (epoch 2.x blocks are
618
+ * not served as Nakamoto blocks) its epoch 2.x header.
619
+ */
620
+ private fetchHeader;
621
+ /**
506
622
  * Prove `value` at `path` in `tip`'s state, fetching the header of every
507
623
  * ancestor trie the proof crosses.
508
624
  */
@@ -515,7 +631,8 @@ declare class BlockVerifier {
515
631
  private parentLeaves;
516
632
  /**
517
633
  * A carried leaf holds the parent's value. The parent header is
518
- * authenticated by hash: its id is committed in the signed child header.
634
+ * authenticated by hash: its id is committed in the authenticated child
635
+ * (its header, or for 2.x its own trie).
519
636
  */
520
637
  private parentHolds;
521
638
  /** Consensus hash -> PoW-verified burn block, via the sortition preimage. */
@@ -544,4 +661,4 @@ declare class BlockVerifier {
544
661
  }
545
662
  /** One-shot: verify a block from a checkpoint (MAINNET_CHECKPOINT by default). */
546
663
  declare const verifyBlock: (ref: string | number, opts: VerifyOptions) => Promise<BlockVerification>;
547
- export { BlockDiff, BlockDiffInput, BlockStateInput, BlockStateResult, BlockVerification, BlockVerifier, BurnPreimage, Bytes, Checkpoint, ClassifiedLeaf, DiffLeaf, DiffWrite, HeaderChain, HeaderRule, HeaderValidationError, MAINNET_CHECKPOINT, MAINNET_FIRST_BURN_HEIGHT, MAINNET_PREPARE_LENGTH, MAINNET_REWARD_CYCLE_LENGTH, MarfProofInput, MarfProofResponse, NakamotoHeader, NodeRpcProofSource, NodeRpcSourceOptions, ProofSource, ProvenDiff, SIGNERS_CONTRACT, SecondlayerProofSource, SecondlayerSourceOptions, SignerCheck, SignerSet, SourceError, StateFailure, StateFailureCode, StateWitness, StateWrite, VerifyCheckpoint, VerifyFailure, VerifyFailureCode, VerifyOptions, VerifyStep, VmEventRow, WitnessLeaf, blockId2 as blockId, classifyBlockDiff, cycleStart, dataVarKey, decodeSignerSet, ftBalanceKey, hex, mapEntryKey, marfPath, marfProofAncestors, marfValue, parseNakamotoHeader, parseWitness, rewardCycle, signerSignatureHash, unhex, verifyBlock, verifyBlockState, verifyConsensusPreimage, verifyMarfProof, verifySignerSignatures };
664
+ export { Ancestry, BlockDiff, BlockDiffInput, BlockStateInput, BlockStateResult, BlockVerification, BlockVerifier, BurnPreimage, Bytes, Checkpoint, ClassifiedLeaf, DiffLeaf, DiffWrite, Epoch2Header, Epoch2HeaderResponse, HeaderChain, HeaderRule, HeaderValidationError, MAINNET_CHECKPOINT, MAINNET_FIRST_BURN_HEIGHT, MAINNET_PREPARE_LENGTH, MAINNET_REWARD_CYCLE_LENGTH, MarfProofInput, MarfProofResponse, NakamotoHeader, NodeRpcProofSource, NodeRpcSourceOptions, ProofSource, ProvenDiff, SIGNERS_CONTRACT, SecondlayerProofSource, SecondlayerSourceOptions, SignerCheck, SignerSet, SourceError, StacksHeader, StateFailure, StateFailureCode, StateWitness, StateWrite, VerifyCheckpoint, VerifyFailure, VerifyFailureCode, VerifyOptions, VerifyStep, VmEventRow, WitnessLeaf, blockId2 as blockId, classifyBlockDiff, cycleStart, dataVarKey, decodeSignerSet, ftBalanceKey, hex, isEpoch2Header, mapEntryKey, marfPath, marfProofAncestors, marfValue, parseEpoch2Header, parseNakamotoHeader, parseWitness, rewardCycle, signerSignatureHash, unhex, verifyBlock, verifyBlockState, verifyConsensusPreimage, verifyMarfProof, verifySignerSignatures };