@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 +16 -0
- package/dist/index.d.ts +95 -4
- package/dist/index.js +219 -1
- package/dist/index.js.map +15 -14
- package/package.json +2 -2
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
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
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=
|
|
2441
|
+
//# debugId=6B4D2C6B1BB8C3A564756E2164756E21
|
|
2224
2442
|
//# sourceMappingURL=index.js.map
|