@secondlayer/verify 0.5.3 → 0.6.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
@@ -61,6 +61,22 @@ const keyed = new SecondlayerProofSource({ apiKey: process.env.SECONDLAYER_API_K
61
61
  await verifyBlock(9_137_005, { source: keyed, rows: true });
62
62
  ```
63
63
 
64
+ ## Bitcoin transactions
65
+
66
+ `verifyBitcoinTx` proves a Bitcoin transaction is in the checkpoint's Bitcoin chain, and optionally that one output pays an address and amount. No contract call.
67
+
68
+ ```ts
69
+ import { BlockVerifier, SecondlayerProofSource } from "@secondlayer/verify";
70
+
71
+ const verifier = new BlockVerifier({ source: new SecondlayerProofSource() });
72
+ const result = await verifier.verifyBitcoinTx(txid, {
73
+ expect: { address: "bc1q9fkjx3s9ufz90k4jzgmdkzlyt22h0tp3x92ua9", amount: 398_000n },
74
+ });
75
+ // result.ok, result.height, result.matched (the output index)
76
+ ```
77
+
78
+ To read from your own Bitcoin node (`txindex=1`), spread `getBitcoinTxProof: (txid) => buildTxProof(bitcoinRpcSource({ url, auth }), { txid })` over the source, with both imported from `@secondlayer/stacks/bitcoin`. Any Esplora endpoint works too: the verifier re-checks every byte.
79
+
64
80
  ## What it proves
65
81
 
66
82
  | Proven | How |
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ /** A byte string, held as a Uint8Array. */
1
2
  type Bytes = Uint8Array;
2
3
  /** Encodes bytes as a lowercase hex string. */
3
4
  declare const hex: (b: Bytes) => string;
4
5
  /** Hex to bytes; tolerates a 0x prefix. */
5
6
  declare const unhex: (s: string) => Bytes;
7
+ /** A Nakamoto-era Stacks block header. */
6
8
  interface NakamotoHeader {
7
9
  /** Low 7 bits = header version; high bit = shadow block. */
8
10
  version: number;
@@ -56,6 +58,7 @@ interface Epoch2Header {
56
58
  blockHash: Bytes;
57
59
  consensusHash: Bytes;
58
60
  }
61
+ /** A Stacks block header from either era. */
59
62
  type StacksHeader = NakamotoHeader | Epoch2Header;
60
63
  /**
61
64
  * Parses the epoch 2.x block header at the start of `raw`.
@@ -86,6 +89,7 @@ declare function marfValue(value: string): Bytes;
86
89
  * header hashes to the id the trie committed. Malformed proofs yield no ids.
87
90
  */
88
91
  declare function marfProofAncestors(proof: Bytes): string[];
92
+ /** A MARF proof with the leaf path and value it covers. */
89
93
  interface MarfProofInput {
90
94
  /** Consensus-serialized TrieMerkleProof (`/v2/clarity/marf/<path>?proof=1`). */
91
95
  proof: Bytes;
@@ -117,6 +121,7 @@ interface WitnessLeaf {
117
121
  /** 32-byte value hash (first 32 bytes of the MARFValue; the tail is zero). */
118
122
  valueHash: Bytes;
119
123
  }
124
+ /** A recomputed trie root, its ancestor roots, and leaves. */
120
125
  interface StateWitness {
121
126
  /** Recomputed trie root; compare to the authenticated header's state_index_root. */
122
127
  root: Bytes;
@@ -130,6 +135,7 @@ interface StateWitness {
130
135
  * The result proves nothing until `root` equals a header's state_index_root.
131
136
  */
132
137
  declare function parseWitness(bytes: Bytes): StateWitness;
138
+ /** The witness leaves, claimed writes, and bookkeeping keys to classify. */
133
139
  interface BlockDiffInput {
134
140
  /** `parseWitness(bytes).leaves`, from a witness whose root was checked. */
135
141
  leaves: WitnessLeaf[];
@@ -143,6 +149,7 @@ interface BlockDiffInput {
143
149
  interface ClassifiedLeaf extends WitnessLeaf {
144
150
  key: string;
145
151
  }
152
+ /** A block's witnessed leaves, split into writes, carried, and internal. */
146
153
  interface BlockDiff {
147
154
  /** Leaves matching a claimed write (path and value). */
148
155
  writes: ClassifiedLeaf[];
@@ -174,6 +181,7 @@ type SignerSet = Map<string, bigint>;
174
181
  * `(some (list (tuple (signer principal) (weight uint) ...)))`.
175
182
  */
176
183
  declare function decodeSignerSet(value: string | Bytes): SignerSet;
184
+ /** Whether the signed weight meets the threshold with known signers. */
177
185
  interface SignerCheck {
178
186
  /** Signed weight meets the threshold, and every signature is a distinct known signer. */
179
187
  valid: boolean;
@@ -238,6 +246,7 @@ interface BlockHeader {
238
246
  bits: number;
239
247
  nonce: number;
240
248
  }
249
+ /** A Bitcoin header rule a chain check can fail on. */
241
250
  type HeaderRule = "checkpoint" | "format" | "prev-link" | "bits" | "retarget" | "median-time-past" | "version" | "proof-of-work";
242
251
  /**
243
252
  * Thrown when a Bitcoin header fails a validation rule.
@@ -250,6 +259,7 @@ declare class HeaderValidationError extends Error {
250
259
  readonly name = "HeaderValidationError";
251
260
  constructor(height: number, rule: HeaderRule, detail: string);
252
261
  }
262
+ /** A trusted Bitcoin header that anchors header validation. */
253
263
  interface Checkpoint {
254
264
  height: number;
255
265
  /** Raw 80-byte header, bytes or hex. Trusted without validation. */
@@ -261,6 +271,7 @@ interface Checkpoint {
261
271
  */
262
272
  periodStartTime?: number;
263
273
  }
274
+ /** A Bitcoin header with its height, hash, and raw bytes. */
264
275
  interface ChainEntry {
265
276
  height: number;
266
277
  /** Display-order hex. */
@@ -307,6 +318,7 @@ declare class HeaderChain {
307
318
  * (burn height 967,680 or later); earlier ones by hash chain from 9,055,251.
308
319
  */
309
320
  declare const MAINNET_CHECKPOINT: VerifyCheckpoint;
321
+ /** A trusted Stacks header and signer set to start verification from. */
310
322
  interface VerifyCheckpoint {
311
323
  stacks: {
312
324
  /** Raw Nakamoto header (hex) of a trusted block whose state holds `signerSet`. */
@@ -319,6 +331,7 @@ interface VerifyCheckpoint {
319
331
  /** Retarget-boundary Bitcoin header at or below the first burn block of `stacks.cycle`. */
320
332
  bitcoin: Checkpoint;
321
333
  }
334
+ import { SpvProof } from "@secondlayer/stacks/bitcoin";
322
335
  /** Sortition preimage behind a consensus hash. */
323
336
  interface BurnPreimage {
324
337
  preimage: Bytes;
@@ -355,7 +368,7 @@ interface VmEventRow {
355
368
  raw_key?: string | null;
356
369
  raw_value?: string | null;
357
370
  }
358
- /** An epoch 2.x header and the consensus hash its index block hash commits to. */
371
+ /** An epoch 2.x header and the consensus hash it commits to. */
359
372
  interface Epoch2HeaderResponse {
360
373
  /** Consensus-serialized StacksBlockHeader. */
361
374
  header: Bytes;
@@ -388,6 +401,11 @@ interface ProofSource {
388
401
  getStateWrites?(height: number): Promise<StateWrite[] | null>;
389
402
  /** Indexed vm_events write rows of the block, checked against its proven diff. */
390
403
  getVmEvents?(height: number): Promise<VmEventRow[]>;
404
+ /**
405
+ * A Bitcoin tx, its merkle branch and block header, by display-order txid.
406
+ * Untrusted: `verifyBitcoinTx` re-checks every byte.
407
+ */
408
+ getBitcoinTxProof?(txid: string): Promise<SpvProof>;
391
409
  }
392
410
  /**
393
411
  * Thrown when a proof source request fails.
@@ -404,6 +422,7 @@ declare class SourceError extends Error {
404
422
  type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
405
423
  /** The hosted API. */
406
424
  declare const DEFAULT_API_URL = "https://api.secondlayer.tools";
425
+ /** Options for reading proofs from the Secondlayer API. */
407
426
  interface SecondlayerSourceOptions {
408
427
  /** API origin. Defaults to {@link DEFAULT_API_URL}. */
409
428
  baseUrl?: string;
@@ -449,8 +468,15 @@ declare class SecondlayerProofSource implements ProofSource {
449
468
  getStateWrites: (height: number) => Promise<StateWrite[] | null>;
450
469
  /** The indexed `vm_events` write rows of a block, from the Index API. */
451
470
  getVmEvents: (height: number) => Promise<VmEventRow[]>;
471
+ /**
472
+ * A Bitcoin tx, its merkle branch and header, by txid.
473
+ *
474
+ * The txid is display-order hex.
475
+ */
476
+ getBitcoinTxProof: (txid: string) => Promise<SpvProof>;
452
477
  constructor(opts?: SecondlayerSourceOptions);
453
478
  }
479
+ /** Options for reading proofs from a Stacks node. */
454
480
  interface NodeRpcSourceOptions {
455
481
  /** Stacks node RPC origin, e.g. `http://localhost:20443`. */
456
482
  nodeUrl: string;
@@ -512,6 +538,7 @@ interface StateFailure {
512
538
  path?: string;
513
539
  ordinal?: number;
514
540
  }
541
+ /** A block's height, parent, and state root to check rows against. */
515
542
  interface BlockStateInput {
516
543
  /** MARF height of the block (its Stacks chain length). */
517
544
  height: number;
@@ -537,6 +564,7 @@ interface BlockStateInput {
537
564
  */
538
565
  parentHolds?: (leaf: WitnessLeaf) => Promise<boolean>;
539
566
  }
567
+ /** The proven diff, rows checked, notes, and failures. */
540
568
  interface BlockStateResult {
541
569
  diff?: ProvenDiff;
542
570
  rowsChecked: number;
@@ -553,6 +581,7 @@ interface BlockStateResult {
553
581
  * `parentLeaves`, and the remaining writes come back unnamed.
554
582
  */
555
583
  declare function verifyBlockState(input: BlockStateInput): Promise<BlockStateResult>;
584
+ /** A transaction's id and its consensus-serialized bytes. */
556
585
  interface BlockTransaction {
557
586
  /** sha512/256 of the transaction bytes, hex. */
558
587
  txid: string;
@@ -583,8 +612,31 @@ declare class TransactionsError extends Error {
583
612
  * mismatch.
584
613
  */
585
614
  declare function blockTransactions(raw: Bytes, header: NakamotoHeader): BlockTransaction[] | null;
586
- type VerifyStep = "ancestry" | "block" | "bitcoin" | "burn" | "signer-set" | "signatures" | "txs" | "witness" | "names" | "rows";
587
- type VerifyFailureCode = StateFailureCode | "unavailable" | "id-mismatch" | "tx-root-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";
615
+ import { SpvProof as SpvProof3 } from "@secondlayer/stacks/bitcoin";
616
+ import { OutputScriptType } from "@secondlayer/stacks/bitcoin";
617
+ /** One output of a proven Bitcoin transaction. */
618
+ interface BitcoinTxOutputView {
619
+ vout: number;
620
+ /** Satoshis. */
621
+ amount: bigint;
622
+ script: Bytes;
623
+ type: OutputScriptType;
624
+ /** Mainnet address, when the script has one. */
625
+ address?: string;
626
+ }
627
+ /**
628
+ * The output fields a proven transaction must match.
629
+ *
630
+ * Every field given has to be equal. `address` compares as a string, `script`
631
+ * byte for byte, `amount` in satoshis.
632
+ */
633
+ interface BitcoinTxExpect {
634
+ address?: string;
635
+ script?: Bytes;
636
+ amount?: bigint;
637
+ }
638
+ type VerifyStep = "ancestry" | "block" | "bitcoin" | "bitcoin-tx" | "burn" | "signer-set" | "signatures" | "txs" | "witness" | "names" | "rows";
639
+ type VerifyFailureCode = StateFailureCode | "unavailable" | "id-mismatch" | "tx-root-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" | "tx-parse" | "txid-mismatch" | "merkle-mismatch" | "bitcoin-header-not-in-chain" | "output-not-found";
588
640
  interface VerifyFailure {
589
641
  step: VerifyStep;
590
642
  code: VerifyFailureCode;
@@ -609,6 +661,7 @@ interface Ancestry {
609
661
  */
610
662
  via: "parents" | "marf";
611
663
  }
664
+ /** The result of verifying a block, step by step. */
612
665
  interface BlockVerification {
613
666
  /**
614
667
  * Every step passed. At or above the checkpoint: header, burn binding,
@@ -649,6 +702,7 @@ interface BlockVerification {
649
702
  /** Empty when ok. The first entry is the first broken link. */
650
703
  failures: VerifyFailure[];
651
704
  }
705
+ /** The proof source, checkpoint, and row checking for a block verify. */
652
706
  interface VerifyOptions {
653
707
  source: ProofSource;
654
708
  /** Defaults to MAINNET_CHECKPOINT. */
@@ -666,6 +720,21 @@ interface VerifyOptions {
666
720
  */
667
721
  onStep?: (step: VerifyStep) => void;
668
722
  }
723
+ /** The result of proving a Bitcoin transaction against the checkpoint's chain. */
724
+ interface BitcoinTxVerification {
725
+ /** The tx is in a block of the checkpoint's Bitcoin chain, and any `expect` matched. */
726
+ ok: boolean;
727
+ /** Display-order hex. */
728
+ txid?: string;
729
+ height?: number;
730
+ /** Display-order hex. */
731
+ blockHash?: string;
732
+ outputs?: BitcoinTxOutputView[];
733
+ /** Index of the first output matching `expect`, when given. */
734
+ matched?: number;
735
+ /** Empty when ok. */
736
+ failures: VerifyFailure[];
737
+ }
669
738
  /**
670
739
  * Verifies blocks from one checkpoint against one source.
671
740
  *
@@ -686,6 +755,8 @@ declare class BlockVerifier {
686
755
  private readonly headers;
687
756
  /** Blocks proven on the checkpoint's chain below it, by height. */
688
757
  private readonly below;
758
+ /** Bitcoin block hashes proven below the checkpoint by prev-hash links, by height. */
759
+ private readonly bitcoinBelow;
689
760
  private readonly burns;
690
761
  /** Unverified source answers by consensus hash. */
691
762
  private readonly preimages;
@@ -696,6 +767,24 @@ declare class BlockVerifier {
696
767
  constructor(opts: VerifyOptions);
697
768
  /** Verify one block by id (hex) or height. Never throws for bad source data. */
698
769
  verify(ref: string | number): Promise<BlockVerification>;
770
+ /**
771
+ * Proves a Bitcoin tx is in the checkpoint's Bitcoin chain.
772
+ *
773
+ * It can also prove that one output pays `expect`. Pass an `SpvProof`, or a
774
+ * txid for the source's `getBitcoinTxProof` to fetch. It never throws for bad
775
+ * source data.
776
+ */
777
+ verifyBitcoinTx(proof: SpvProof3 | string, opts?: {
778
+ expect?: BitcoinTxExpect
779
+ }): Promise<BitcoinTxVerification>;
780
+ private runBitcoinTx;
781
+ /**
782
+ * Fail unless the Bitcoin block `hash` is the checkpoint chain's block at
783
+ * `height`. At or above the checkpoint that is the PoW-validated chain; below
784
+ * it, prev-hash links back from the checkpoint header, which commits to every
785
+ * ancestor.
786
+ */
787
+ private bitcoinHeaderInChain;
699
788
  /** Report a passed link. A throwing callback must not break verification. */
700
789
  private passed;
701
790
  private run;
@@ -799,6 +888,7 @@ declare class BlockVerifier {
799
888
  /** One-shot: verify a block from a checkpoint (MAINNET_CHECKPOINT by default). */
800
889
  declare const verifyBlock: (ref: string | number, opts: VerifyOptions) => Promise<BlockVerification>;
801
890
  type LinkStatus = "ok" | "failed" | "skipped";
891
+ /** One verification step with its status and what it proved. */
802
892
  interface LinkLine {
803
893
  step: VerifyStep;
804
894
  label: string;
@@ -827,4 +917,5 @@ declare function describeLinks(r: BlockVerification, cp: VerifyCheckpoint, opts?
827
917
  text: string
828
918
  }
829
919
  };
830
- export { Ancestry, BlockDiff, BlockDiffInput, BlockStateInput, BlockStateResult, BlockTransaction, BlockVerification, BlockVerifier, BurnPreimage, Bytes, Checkpoint, ClassifiedLeaf, DEFAULT_API_URL, DiffLeaf, DiffWrite, Epoch2Header, Epoch2HeaderResponse, HeaderChain, HeaderRule, HeaderValidationError, LINKS, LinkLine, LinkStatus, 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, TransactionsError, VerifyCheckpoint, VerifyFailure, VerifyFailureCode, VerifyOptions, VerifyStep, VmEventRow, WitnessLeaf, blockId2 as blockId, blockTransactions, classifyBlockDiff, cycleStart, dataVarKey, decodeSignerSet, describeLinks, ftBalanceKey, hex, isEpoch2Header, mapEntryKey, marfPath, marfProofAncestors, marfValue, parseEpoch2Header, parseNakamotoHeader, parseWitness, rewardCycle, signerSignatureHash, txMerkleRoot2 as txMerkleRoot, unhex, verifyBlock, verifyBlockState, verifyConsensusPreimage, verifyMarfProof, verifySignerSignatures };
920
+ import { SpvProof as SpvProof4 } from "@secondlayer/stacks/bitcoin";
921
+ export { Ancestry, BitcoinTxExpect, BitcoinTxOutputView, BitcoinTxVerification, BlockDiff, BlockDiffInput, BlockStateInput, BlockStateResult, BlockTransaction, BlockVerification, BlockVerifier, BurnPreimage, Bytes, Checkpoint, ClassifiedLeaf, DEFAULT_API_URL, DiffLeaf, DiffWrite, Epoch2Header, Epoch2HeaderResponse, HeaderChain, HeaderRule, HeaderValidationError, LINKS, LinkLine, LinkStatus, 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, SpvProof4 as SpvProof, StacksHeader, StateFailure, StateFailureCode, StateWitness, StateWrite, TransactionsError, VerifyCheckpoint, VerifyFailure, VerifyFailureCode, VerifyOptions, VerifyStep, VmEventRow, WitnessLeaf, blockId2 as blockId, blockTransactions, classifyBlockDiff, cycleStart, dataVarKey, decodeSignerSet, describeLinks, ftBalanceKey, hex, isEpoch2Header, mapEntryKey, marfPath, marfProofAncestors, marfValue, parseEpoch2Header, parseNakamotoHeader, parseWitness, rewardCycle, signerSignatureHash, txMerkleRoot2 as txMerkleRoot, unhex, verifyBlock, verifyBlockState, verifyConsensusPreimage, verifyMarfProof, verifySignerSignatures };
package/dist/index.js CHANGED
@@ -690,6 +690,9 @@ function headerHash(raw) {
690
690
  }
691
691
  return sha2562(sha2562(bytes));
692
692
  }
693
+ function headerHashHex(raw) {
694
+ return bytesToHex2(reverseBytes(headerHash(raw)));
695
+ }
693
696
 
694
697
  // src/bitcoin/pow.ts
695
698
  var POW_LIMIT = 0xffffn << 208n;
@@ -1050,6 +1053,24 @@ var toMarf = (url, j) => {
1050
1053
  throw new SourceError(url, 200, "MARF answer needs data and proof");
1051
1054
  return { data: j.data, proof: unhex(proof) };
1052
1055
  };
1056
+ var isHex = (v) => typeof v === "string" && /^(0x)?([0-9a-f]{2})+$/i.test(v);
1057
+ var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
1058
+ var toBitcoinTxProof = (url, j) => {
1059
+ const w = j;
1060
+ if (!w || !isHex(w.txid) || !isHex(w.raw_tx) || !isHex(w.header) || !isCount(w.block_height) || !isCount(w.tx_index) || !isCount(w.tx_count) || !Array.isArray(w.siblings) || !w.siblings.every(isHex))
1061
+ throw new SourceError(url, 200, "bitcoin tx proof needs txid, raw_tx, header, block_height, tx_index, tx_count and siblings");
1062
+ return {
1063
+ rawTx: unhex(w.raw_tx),
1064
+ txidInternal: unhex(w.txid).reverse(),
1065
+ header: unhex(w.header),
1066
+ height: w.block_height,
1067
+ merkle: {
1068
+ siblings: w.siblings.map(unhex),
1069
+ txIndex: w.tx_index,
1070
+ txCount: w.tx_count
1071
+ }
1072
+ };
1073
+ };
1053
1074
  var DEFAULT_API_URL = "https://api.secondlayer.tools";
1054
1075
  var VM_WRITE_TYPES = [
1055
1076
  "var_set",
@@ -1086,6 +1107,7 @@ class SecondlayerProofSource {
1086
1107
  getBitcoinHeaders;
1087
1108
  getStateWrites;
1088
1109
  getVmEvents;
1110
+ getBitcoinTxProof;
1089
1111
  constructor(opts = {}) {
1090
1112
  const base = trimSlash(opts.baseUrl ?? DEFAULT_API_URL);
1091
1113
  const http = {
@@ -1120,6 +1142,10 @@ class SecondlayerProofSource {
1120
1142
  throw new SourceError(url, 200, `headers start at ${j.from}`);
1121
1143
  return j.headers;
1122
1144
  };
1145
+ this.getBitcoinTxProof = async (txid) => {
1146
+ const url = `${base}/v1/proofs/bitcoin-tx/${txid}`;
1147
+ return toBitcoinTxProof(url, required(url, await getJson(url, http)));
1148
+ };
1123
1149
  this.getStateWrites = async (height) => {
1124
1150
  const url = `${base}/v1/proofs/writes/${height}`;
1125
1151
  const j = await getJson(url, http);
@@ -1437,6 +1463,8 @@ function linkDetail(step, r, cp) {
1437
1463
  return `${n(r.height ?? 0)} ${r.blockId}`;
1438
1464
  case "bitcoin":
1439
1465
  return `headers ${n(cp.bitcoin.height)}..${n(r.burnHeight ?? 0)}, proof-of-work and retargets checked`;
1466
+ case "bitcoin-tx":
1467
+ return "";
1440
1468
  case "burn":
1441
1469
  return `height ${n(r.burnHeight ?? 0)} ${r.bitcoinBlockHash}`;
1442
1470
  case "signer-set":
@@ -1522,6 +1550,86 @@ function describeLinks(r, cp, opts = {}) {
1522
1550
  }
1523
1551
  // src/verify-block.ts
1524
1552
  import { uintCV } from "@secondlayer/stacks/clarity";
1553
+
1554
+ // src/bitcoin-tx.ts
1555
+ import {
1556
+ formatBitcoinAddress,
1557
+ parseBitcoinTx,
1558
+ parseBlockHeader,
1559
+ parseOutputScript,
1560
+ reverseBytes as reverseBytes2,
1561
+ rootFromProof,
1562
+ stripWitness
1563
+ } from "@secondlayer/stacks/bitcoin";
1564
+ var INNER_NODE_SIZE = 64;
1565
+ function branchLength(txCount) {
1566
+ return txCount <= 1 ? 0 : Math.ceil(Math.log2(txCount));
1567
+ }
1568
+ function checkBranch(merkle) {
1569
+ const { txIndex, txCount, siblings } = merkle;
1570
+ if (!Number.isSafeInteger(txCount) || txCount < 1)
1571
+ return `tx count ${txCount} is not a positive integer`;
1572
+ if (!Number.isSafeInteger(txIndex) || txIndex < 0 || txIndex >= txCount)
1573
+ return `tx index ${txIndex} is outside ${txCount} txs`;
1574
+ const want = branchLength(txCount);
1575
+ if (siblings.length !== want)
1576
+ return `branch has ${siblings.length} siblings, a ${txCount}-tx block needs ${want}`;
1577
+ if (siblings.some((s) => s.length !== 32))
1578
+ return "every sibling must be 32 bytes";
1579
+ return;
1580
+ }
1581
+ function checkTxInclusion(proof) {
1582
+ const bad = (code, message) => ({
1583
+ code,
1584
+ message
1585
+ });
1586
+ if (proof.header.length !== HEADER_SIZE)
1587
+ return bad("tx-parse", `header is ${proof.header.length} bytes, not 80`);
1588
+ let parsed;
1589
+ let strippedSize;
1590
+ try {
1591
+ parsed = parseBitcoinTx(proof.rawTx);
1592
+ strippedSize = stripWitness(proof.rawTx).length;
1593
+ } catch (err) {
1594
+ return bad("tx-parse", `raw tx does not parse: ${err.message}`);
1595
+ }
1596
+ if (strippedSize === INNER_NODE_SIZE)
1597
+ return bad("tx-parse", "a 64-byte transaction is indistinguishable from a merkle inner node");
1598
+ if (!bytesEqual(parsed.txidInternal, proof.txidInternal))
1599
+ return bad("txid-mismatch", `raw tx hashes to ${hex(reverseBytes2(parsed.txidInternal))}, not ${hex(reverseBytes2(proof.txidInternal))}`);
1600
+ const branch = checkBranch(proof.merkle);
1601
+ if (branch)
1602
+ return bad("merkle-mismatch", branch);
1603
+ const root = parseBlockHeader(proof.header).merkleRoot;
1604
+ if (!bytesEqual(rootFromProof(parsed.txidInternal, proof.merkle), root))
1605
+ return bad("merkle-mismatch", "the branch does not fold to the header's merkle root");
1606
+ return {
1607
+ txid: hex(reverseBytes2(parsed.txidInternal)),
1608
+ blockHash: headerHashHex(proof.header),
1609
+ outputs: parsed.outputs.map((o, vout) => {
1610
+ const script = o.scriptPubKey;
1611
+ const parsedScript = parseOutputScript(script);
1612
+ return {
1613
+ vout,
1614
+ amount: o.value,
1615
+ script,
1616
+ type: parsedScript.type,
1617
+ address: formatBitcoinAddress(parsedScript, "mainnet")
1618
+ };
1619
+ })
1620
+ };
1621
+ }
1622
+ function outputMatches(output, expect) {
1623
+ if (expect.address !== undefined && output.address !== expect.address)
1624
+ return false;
1625
+ if (expect.script !== undefined && !bytesEqual(output.script, expect.script))
1626
+ return false;
1627
+ if (expect.amount !== undefined && output.amount !== expect.amount)
1628
+ return false;
1629
+ return true;
1630
+ }
1631
+
1632
+ // src/verify-block.ts
1525
1633
  class Broken extends Error {
1526
1634
  failure;
1527
1635
  constructor(failure) {
@@ -1547,6 +1655,7 @@ async function fetchFor(step, what, call, extra = {}) {
1547
1655
  }
1548
1656
  }
1549
1657
  var BITCOIN_BATCH = 2016;
1658
+ var MAX_BITCOIN_WALK = 52560;
1550
1659
  var MAX_WALK = 16;
1551
1660
  async function findAnchor(low, high, window, probe, maxProbes = 32) {
1552
1661
  let [lo, hi] = [low, high];
@@ -1584,6 +1693,7 @@ class BlockVerifier {
1584
1693
  anchors = new Map;
1585
1694
  headers = new Map;
1586
1695
  below = new Map;
1696
+ bitcoinBelow = new Map;
1587
1697
  burns = new Map;
1588
1698
  preimages = new Map;
1589
1699
  raws = new Map;
@@ -1612,6 +1722,114 @@ class BlockVerifier {
1612
1722
  out.ok = out.failures.length === 0;
1613
1723
  return out;
1614
1724
  }
1725
+ async verifyBitcoinTx(proof, opts = {}) {
1726
+ const out = { ok: false, failures: [] };
1727
+ try {
1728
+ await this.runBitcoinTx(proof, opts.expect, out);
1729
+ } catch (err) {
1730
+ if (!(err instanceof Broken))
1731
+ throw err;
1732
+ out.failures.unshift(err.failure);
1733
+ }
1734
+ out.ok = out.failures.length === 0;
1735
+ return out;
1736
+ }
1737
+ async runBitcoinTx(input, expect, out) {
1738
+ let proof;
1739
+ if (typeof input === "string") {
1740
+ const fetchProof = this.source.getBitcoinTxProof;
1741
+ if (!fetchProof)
1742
+ return fail({
1743
+ step: "bitcoin-tx",
1744
+ code: "unavailable",
1745
+ message: "source has no getBitcoinTxProof; pass an SpvProof"
1746
+ });
1747
+ proof = await fetchFor("bitcoin-tx", `bitcoin tx ${input}`, () => fetchProof.call(this.source, input));
1748
+ } else
1749
+ proof = input;
1750
+ const included = checkTxInclusion(proof);
1751
+ if ("code" in included)
1752
+ return fail({ step: "bitcoin-tx", ...included });
1753
+ out.txid = included.txid;
1754
+ out.height = proof.height;
1755
+ out.blockHash = included.blockHash;
1756
+ out.outputs = included.outputs;
1757
+ await this.bitcoinHeaderInChain(proof.height, included.blockHash);
1758
+ if (expect) {
1759
+ const candidates = proof.vout === undefined ? included.outputs : included.outputs.filter((o) => o.vout === proof.vout);
1760
+ const hit = candidates.find((o) => outputMatches(o, expect));
1761
+ if (!hit)
1762
+ return fail({
1763
+ step: "bitcoin-tx",
1764
+ code: "output-not-found",
1765
+ message: proof.vout === undefined ? "no output of the transaction matches what was expected" : `output ${proof.vout} does not match what was expected`
1766
+ });
1767
+ out.matched = hit.vout;
1768
+ }
1769
+ this.passed("bitcoin-tx");
1770
+ }
1771
+ async bitcoinHeaderInChain(height, hash) {
1772
+ const notInChain = (message) => fail({
1773
+ step: "bitcoin-tx",
1774
+ code: "bitcoin-header-not-in-chain",
1775
+ message
1776
+ });
1777
+ const base = this.chain.checkpointHeight;
1778
+ if (!Number.isSafeInteger(height) || height < 0)
1779
+ return notInChain(`block height ${height} is not valid`);
1780
+ if (height >= base) {
1781
+ await this.syncBitcoin(height);
1782
+ if (this.chain.headerAt(height)?.hash !== hash)
1783
+ notInChain(`block ${hash} is not the verified Bitcoin chain's block at ${height}`);
1784
+ return;
1785
+ }
1786
+ if (this.bitcoinBelow.get(height) === hash)
1787
+ return;
1788
+ if (base - height > MAX_BITCOIN_WALK)
1789
+ return fail({
1790
+ step: "bitcoin-tx",
1791
+ code: "before-checkpoint",
1792
+ message: `block ${height} is more than ${MAX_BITCOIN_WALK} blocks below the Bitcoin checkpoint ${base}`
1793
+ });
1794
+ const proven = new Map;
1795
+ let prev;
1796
+ for (let from = height;from < base; ) {
1797
+ const count = Math.min(BITCOIN_BATCH, base - from);
1798
+ const batch = await fetchFor("bitcoin-tx", `bitcoin headers ${from}`, () => this.source.getBitcoinHeaders(from, count));
1799
+ if (batch.length !== count)
1800
+ fail({
1801
+ step: "bitcoin-tx",
1802
+ code: "unavailable",
1803
+ message: `source returned ${batch.length} Bitcoin headers from ${from}, wanted ${count}`
1804
+ });
1805
+ for (const [i, raw] of batch.entries()) {
1806
+ let parsed;
1807
+ try {
1808
+ parsed = parseHeader(raw);
1809
+ } catch (err) {
1810
+ return fail({
1811
+ step: "bitcoin-tx",
1812
+ code: "invalid-header",
1813
+ message: `Bitcoin header ${from + i}: ${err.message}`
1814
+ });
1815
+ }
1816
+ const own = headerHashHex(raw);
1817
+ if (prev === undefined) {
1818
+ if (own !== hash)
1819
+ notInChain(`header at ${height} is not block ${hash}`);
1820
+ } else if (parsed.prevHash !== prev)
1821
+ notInChain(`header at ${from + i} does not link to the header before it`);
1822
+ prev = own;
1823
+ proven.set(from + i, own);
1824
+ }
1825
+ from += count;
1826
+ }
1827
+ const anchor = this.chain.headerAt(base)?.header.prevHash;
1828
+ if (prev === undefined || anchor !== prev)
1829
+ notInChain(`block ${hash} does not link to the Bitcoin checkpoint at ${base}`);
1830
+ for (const [h, v] of proven)
1831
+ this.bitcoinBelow.set(h, v);
1832
+ }
1615
1833
  passed(step) {
1616
1834
  try {
1617
1835
  this.onStep?.(step);
@@ -2220,5 +2438,5 @@ export {
2220
2438
  verifySignerSignatures
2221
2439
  };
2222
2440
 
2223
- //# debugId=C611E693588430B164756E2164756E21
2441
+ //# debugId=6B4D2C6B1BB8C3A564756E2164756E21
2224
2442
  //# sourceMappingURL=index.js.map