@secondlayer/verify 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/index.d.ts +664 -0
- package/dist/index.js +1949 -0
- package/dist/index.js.map +23 -0
- package/package.json +40 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1949 @@
|
|
|
1
|
+
// src/bytes.ts
|
|
2
|
+
import { sha512_256 } from "@noble/hashes/sha2.js";
|
|
3
|
+
import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils.js";
|
|
4
|
+
var hex = (b) => bytesToHex(b);
|
|
5
|
+
var unhex = (s) => hexToBytes(s.startsWith("0x") ? s.slice(2) : s);
|
|
6
|
+
var concat = (parts) => concatBytes(...parts);
|
|
7
|
+
var bytesEqual = (a, b) => {
|
|
8
|
+
if (a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
for (let i = 0;i < a.length; i++)
|
|
11
|
+
if (a[i] !== b[i])
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
var hashAll = (parts) => {
|
|
16
|
+
const h = sha512_256.create();
|
|
17
|
+
for (const p of parts)
|
|
18
|
+
h.update(p);
|
|
19
|
+
return h.digest();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class Reader {
|
|
23
|
+
buf;
|
|
24
|
+
pos = 0;
|
|
25
|
+
constructor(buf) {
|
|
26
|
+
this.buf = buf;
|
|
27
|
+
}
|
|
28
|
+
bytes(n) {
|
|
29
|
+
if (n < 0 || this.pos + n > this.buf.length)
|
|
30
|
+
throw new Error(`unexpected end of input at ${this.pos} reading ${n}`);
|
|
31
|
+
const out = this.buf.subarray(this.pos, this.pos + n);
|
|
32
|
+
this.pos += n;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
view(n) {
|
|
36
|
+
const b = this.bytes(n);
|
|
37
|
+
return new DataView(b.buffer, b.byteOffset, n);
|
|
38
|
+
}
|
|
39
|
+
u8 = () => this.bytes(1)[0];
|
|
40
|
+
u16 = () => this.view(2).getUint16(0);
|
|
41
|
+
u32 = () => this.view(4).getUint32(0);
|
|
42
|
+
u64 = () => this.view(8).getBigUint64(0);
|
|
43
|
+
i64 = () => this.view(8).getBigInt64(0);
|
|
44
|
+
vec(item, max) {
|
|
45
|
+
const n = this.u32();
|
|
46
|
+
if (n > max)
|
|
47
|
+
throw new Error(`vec length ${n} exceeds ${max}`);
|
|
48
|
+
return Array.from({ length: n }, item);
|
|
49
|
+
}
|
|
50
|
+
get done() {
|
|
51
|
+
return this.pos === this.buf.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// src/header.ts
|
|
55
|
+
import { sha512_256 as sha512_2562 } from "@noble/hashes/sha2.js";
|
|
56
|
+
var NAKAMOTO_BLOCK_VERSION_EPOCH_4 = 1;
|
|
57
|
+
var MAX_SIGNERS = 4000;
|
|
58
|
+
var MAX_PROBLEMATIC_TXS = 1 << 16;
|
|
59
|
+
function parseNakamotoHeader(raw) {
|
|
60
|
+
const r = new Reader(raw);
|
|
61
|
+
const version = r.u8();
|
|
62
|
+
const chainLength = r.u64();
|
|
63
|
+
const burnSpent = r.u64();
|
|
64
|
+
const consensusHash = r.bytes(20);
|
|
65
|
+
const parentBlockId = r.bytes(32);
|
|
66
|
+
const txMerkleRoot = r.bytes(32);
|
|
67
|
+
const stateIndexRoot = r.bytes(32);
|
|
68
|
+
const timestamp = r.u64();
|
|
69
|
+
const minerSignature = r.bytes(65);
|
|
70
|
+
const sigStart = r.pos;
|
|
71
|
+
const signerSignatures = r.vec(() => r.bytes(65), MAX_SIGNERS);
|
|
72
|
+
const sigEnd = r.pos;
|
|
73
|
+
const bits = r.u16();
|
|
74
|
+
if (bits === 0 || bits > MAX_SIGNERS)
|
|
75
|
+
throw new Error(`bad pox_treatment length ${bits}`);
|
|
76
|
+
const bitBytes = Math.ceil(bits / 8);
|
|
77
|
+
if (r.u32() !== bitBytes)
|
|
78
|
+
throw new Error("bad pox_treatment data length");
|
|
79
|
+
r.bytes(bitBytes);
|
|
80
|
+
if ((version & 127) >= NAKAMOTO_BLOCK_VERSION_EPOCH_4) {
|
|
81
|
+
r.vec(() => r.bytes(5), MAX_PROBLEMATIC_TXS);
|
|
82
|
+
}
|
|
83
|
+
const end = r.pos;
|
|
84
|
+
return {
|
|
85
|
+
version,
|
|
86
|
+
chainLength,
|
|
87
|
+
burnSpent,
|
|
88
|
+
consensusHash,
|
|
89
|
+
parentBlockId,
|
|
90
|
+
txMerkleRoot,
|
|
91
|
+
stateIndexRoot,
|
|
92
|
+
timestamp,
|
|
93
|
+
minerSignature,
|
|
94
|
+
signerSignatures,
|
|
95
|
+
signerSighashPreimage: concat([
|
|
96
|
+
raw.subarray(0, sigStart),
|
|
97
|
+
raw.subarray(sigEnd, end)
|
|
98
|
+
]),
|
|
99
|
+
byteLength: end
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var signerSignatureHash = (h) => sha512_2562(h.signerSighashPreimage);
|
|
103
|
+
var EPOCH2_HEADER_LENGTH = 247;
|
|
104
|
+
function parseEpoch2Header(raw, consensusHash) {
|
|
105
|
+
if (consensusHash.length !== 20)
|
|
106
|
+
throw new Error(`consensus hash is ${consensusHash.length} bytes, not 20`);
|
|
107
|
+
const r = new Reader(raw);
|
|
108
|
+
const header = {
|
|
109
|
+
version: r.u8(),
|
|
110
|
+
burnSpent: r.u64(),
|
|
111
|
+
chainLength: r.u64(),
|
|
112
|
+
vrfProof: r.bytes(80),
|
|
113
|
+
parentBlockHash: r.bytes(32),
|
|
114
|
+
parentMicroblock: r.bytes(32),
|
|
115
|
+
parentMicroblockSequence: r.u16(),
|
|
116
|
+
txMerkleRoot: r.bytes(32),
|
|
117
|
+
stateIndexRoot: r.bytes(32),
|
|
118
|
+
microblockPubkeyHash: r.bytes(20)
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
...header,
|
|
122
|
+
blockHash: sha512_2562(raw.subarray(0, EPOCH2_HEADER_LENGTH)),
|
|
123
|
+
consensusHash
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
var isEpoch2Header = (h) => ("parentBlockHash" in h);
|
|
127
|
+
var blockId = (h) => hashAll([
|
|
128
|
+
isEpoch2Header(h) ? h.blockHash : signerSignatureHash(h),
|
|
129
|
+
h.consensusHash
|
|
130
|
+
]);
|
|
131
|
+
// src/marf.ts
|
|
132
|
+
import { sha512_256 as sha512_2563 } from "@noble/hashes/sha2.js";
|
|
133
|
+
var MARF_VALUE_SIZE = 40;
|
|
134
|
+
var NodeId = {
|
|
135
|
+
Empty: 0,
|
|
136
|
+
Leaf: 1,
|
|
137
|
+
Node4: 2,
|
|
138
|
+
Node16: 3,
|
|
139
|
+
Node48: 4,
|
|
140
|
+
Node256: 5
|
|
141
|
+
};
|
|
142
|
+
var NODE_PTR_COUNT = {
|
|
143
|
+
[NodeId.Node4]: 4,
|
|
144
|
+
[NodeId.Node16]: 16,
|
|
145
|
+
[NodeId.Node48]: 48,
|
|
146
|
+
[NodeId.Node256]: 256
|
|
147
|
+
};
|
|
148
|
+
var isBackptr = (id) => (id & 128) !== 0;
|
|
149
|
+
var marfPath = (key) => sha512_2563(new TextEncoder().encode(key));
|
|
150
|
+
var heightToHashKey = (height) => `__MARF_BLOCK_HEIGHT_TO_HASH::${height}`;
|
|
151
|
+
function marfValue(value) {
|
|
152
|
+
const v = new Uint8Array(MARF_VALUE_SIZE);
|
|
153
|
+
v.set(sha512_2563(new TextEncoder().encode(value)));
|
|
154
|
+
return v;
|
|
155
|
+
}
|
|
156
|
+
function pathPreimage(path) {
|
|
157
|
+
if (path.length > 32)
|
|
158
|
+
throw new Error("trie path longer than 32 bytes");
|
|
159
|
+
return concat([Uint8Array.of(path.length), path]);
|
|
160
|
+
}
|
|
161
|
+
var leafHash = (path, value) => hashAll([Uint8Array.of(NodeId.Leaf), pathPreimage(path), value]);
|
|
162
|
+
function nodeHash(node, children) {
|
|
163
|
+
const parts = [Uint8Array.of(node.id)];
|
|
164
|
+
for (const p of node.ptrs)
|
|
165
|
+
parts.push(Uint8Array.of(p.id, p.chr), p.backBlock);
|
|
166
|
+
parts.push(pathPreimage(node.path), ...children);
|
|
167
|
+
return hashAll(parts);
|
|
168
|
+
}
|
|
169
|
+
var PROOF_KIND = [
|
|
170
|
+
"Node4",
|
|
171
|
+
"Node16",
|
|
172
|
+
"Node48",
|
|
173
|
+
"Node256",
|
|
174
|
+
"Leaf",
|
|
175
|
+
"Shunt"
|
|
176
|
+
];
|
|
177
|
+
var KIND_PTRS = {
|
|
178
|
+
Node4: 4,
|
|
179
|
+
Node16: 16,
|
|
180
|
+
Node48: 48,
|
|
181
|
+
Node256: 256
|
|
182
|
+
};
|
|
183
|
+
function decodeProof(bytes) {
|
|
184
|
+
const r = new Reader(bytes);
|
|
185
|
+
const hash = () => r.bytes(32);
|
|
186
|
+
const steps = r.vec(() => {
|
|
187
|
+
const kind = PROOF_KIND[r.u8()];
|
|
188
|
+
if (!kind)
|
|
189
|
+
throw new Error("bad proof step type");
|
|
190
|
+
if (kind === "Shunt")
|
|
191
|
+
return { kind, idx: r.i64(), hashes: r.vec(hash, 1 << 20) };
|
|
192
|
+
const chr = r.u8();
|
|
193
|
+
if (kind === "Leaf")
|
|
194
|
+
return {
|
|
195
|
+
kind,
|
|
196
|
+
chr,
|
|
197
|
+
path: r.bytes(r.u32()),
|
|
198
|
+
value: r.bytes(MARF_VALUE_SIZE)
|
|
199
|
+
};
|
|
200
|
+
const node = {
|
|
201
|
+
id: r.u8(),
|
|
202
|
+
path: r.bytes(r.u32()),
|
|
203
|
+
ptrs: r.vec(() => ({ id: r.u8(), chr: r.u8(), backBlock: r.bytes(32) }), 256)
|
|
204
|
+
};
|
|
205
|
+
const hashes = Array.from({ length: KIND_PTRS[kind] - 1 }, hash);
|
|
206
|
+
return { kind, chr, node, hashes };
|
|
207
|
+
}, 1 << 20);
|
|
208
|
+
if (!r.done)
|
|
209
|
+
throw new Error("trailing bytes after proof");
|
|
210
|
+
return steps;
|
|
211
|
+
}
|
|
212
|
+
var isShunt = (s) => s?.kind === "Shunt";
|
|
213
|
+
var segmentEnd = (p, i) => {
|
|
214
|
+
let j = i + 1;
|
|
215
|
+
while (j < p.length && !isShunt(p[j]))
|
|
216
|
+
j++;
|
|
217
|
+
return j;
|
|
218
|
+
};
|
|
219
|
+
function segmentProofHash(step, childHash) {
|
|
220
|
+
const count = KIND_PTRS[step.kind];
|
|
221
|
+
if (step.node.ptrs.length !== count)
|
|
222
|
+
return null;
|
|
223
|
+
const all = [];
|
|
224
|
+
let ih = 0;
|
|
225
|
+
for (const p of step.node.ptrs) {
|
|
226
|
+
if (p.id !== NodeId.Empty && p.chr === step.chr)
|
|
227
|
+
all.push(childHash);
|
|
228
|
+
else {
|
|
229
|
+
const h = step.hashes[ih++];
|
|
230
|
+
if (!h)
|
|
231
|
+
return null;
|
|
232
|
+
all.push(h);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return all.length === count ? nodeHash(step.node, all) : null;
|
|
236
|
+
}
|
|
237
|
+
function verifySegment(seg, start) {
|
|
238
|
+
let h = start;
|
|
239
|
+
for (const s of seg) {
|
|
240
|
+
if (s.kind === "Shunt" || !h)
|
|
241
|
+
return null;
|
|
242
|
+
h = s.kind === "Leaf" ? leafHash(s.path, s.value) : segmentProofHash(s, h);
|
|
243
|
+
}
|
|
244
|
+
return h;
|
|
245
|
+
}
|
|
246
|
+
function segmentPathPrefix(seg) {
|
|
247
|
+
const parts = [];
|
|
248
|
+
for (const s of seg) {
|
|
249
|
+
if (s.kind === "Shunt")
|
|
250
|
+
return null;
|
|
251
|
+
if (s.kind === "Leaf")
|
|
252
|
+
parts.push(s.path);
|
|
253
|
+
else
|
|
254
|
+
parts.push(Uint8Array.of(s.chr), s.node.path);
|
|
255
|
+
}
|
|
256
|
+
return concat(parts.reverse());
|
|
257
|
+
}
|
|
258
|
+
function isWellFormed(p, expectedPath) {
|
|
259
|
+
if (p[0]?.kind !== "Leaf")
|
|
260
|
+
return false;
|
|
261
|
+
let i = 0;
|
|
262
|
+
let path = new Uint8Array;
|
|
263
|
+
while (i < p.length) {
|
|
264
|
+
const j = segmentEnd(p, i);
|
|
265
|
+
const prefix = segmentPathPrefix(p.slice(i, j));
|
|
266
|
+
if (!prefix)
|
|
267
|
+
return false;
|
|
268
|
+
if (i === 0) {
|
|
269
|
+
if (!bytesEqual(prefix, expectedPath))
|
|
270
|
+
return false;
|
|
271
|
+
path = prefix;
|
|
272
|
+
} else if (prefix.length > path.length || !bytesEqual(path.subarray(0, prefix.length), prefix))
|
|
273
|
+
return false;
|
|
274
|
+
i = j;
|
|
275
|
+
if (i >= p.length)
|
|
276
|
+
return false;
|
|
277
|
+
let k = i + 1;
|
|
278
|
+
while (k < p.length && isShunt(p[k]))
|
|
279
|
+
k++;
|
|
280
|
+
i = k;
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
function shuntHash(lead, idx, inserted, hashes) {
|
|
285
|
+
if (idx === 0n)
|
|
286
|
+
return null;
|
|
287
|
+
const all = lead ? [lead] : [];
|
|
288
|
+
let hi = 0;
|
|
289
|
+
for (let i = 0;i < hashes.length + 1; i++) {
|
|
290
|
+
if (idx - 1n === BigInt(i))
|
|
291
|
+
all.push(inserted);
|
|
292
|
+
else {
|
|
293
|
+
const h = hashes[hi++];
|
|
294
|
+
if (!h)
|
|
295
|
+
return null;
|
|
296
|
+
all.push(h);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return hashAll(all);
|
|
300
|
+
}
|
|
301
|
+
function verifySteps(proof, path, value, root, rootToBlock) {
|
|
302
|
+
if (!isWellFormed(proof, path))
|
|
303
|
+
return false;
|
|
304
|
+
const first = proof[0];
|
|
305
|
+
if (first?.kind !== "Leaf" || !bytesEqual(first.value, value))
|
|
306
|
+
return false;
|
|
307
|
+
let i = 0;
|
|
308
|
+
let j = segmentEnd(proof, i);
|
|
309
|
+
const nodeRoot = verifySegment(proof.slice(i, j), leafHash(first.path, first.value));
|
|
310
|
+
if (!nodeRoot)
|
|
311
|
+
return false;
|
|
312
|
+
i = j;
|
|
313
|
+
const head = proof[i];
|
|
314
|
+
if (!isShunt(head) || head.idx !== 0n)
|
|
315
|
+
return false;
|
|
316
|
+
let trieHash = head.hashes.length === 0 ? nodeRoot : hashAll([nodeRoot, ...head.hashes]);
|
|
317
|
+
i += 1;
|
|
318
|
+
if (i >= proof.length)
|
|
319
|
+
return bytesEqual(root, trieHash);
|
|
320
|
+
let backBlock = rootToBlock.get(hex(trieHash));
|
|
321
|
+
if (!backBlock || isShunt(proof[i]))
|
|
322
|
+
return false;
|
|
323
|
+
while (i < proof.length) {
|
|
324
|
+
j = segmentEnd(proof, i);
|
|
325
|
+
const nextNodeRoot = verifySegment(proof.slice(i, j), backBlock);
|
|
326
|
+
if (!nextNodeRoot)
|
|
327
|
+
return false;
|
|
328
|
+
i = j;
|
|
329
|
+
if (i >= proof.length)
|
|
330
|
+
return false;
|
|
331
|
+
j = i;
|
|
332
|
+
while (j < proof.length) {
|
|
333
|
+
const s = proof[j];
|
|
334
|
+
if (!isShunt(s) || s.idx === 0n)
|
|
335
|
+
break;
|
|
336
|
+
j++;
|
|
337
|
+
}
|
|
338
|
+
j -= 1;
|
|
339
|
+
if (j < i)
|
|
340
|
+
return false;
|
|
341
|
+
let penultimate = trieHash;
|
|
342
|
+
for (const s of proof.slice(i, j)) {
|
|
343
|
+
if (!isShunt(s))
|
|
344
|
+
return false;
|
|
345
|
+
const h = shuntHash(null, s.idx, penultimate, s.hashes);
|
|
346
|
+
if (!h)
|
|
347
|
+
return false;
|
|
348
|
+
penultimate = h;
|
|
349
|
+
}
|
|
350
|
+
const junction = proof[j];
|
|
351
|
+
if (!isShunt(junction))
|
|
352
|
+
return false;
|
|
353
|
+
const next = shuntHash(nextNodeRoot, junction.idx, penultimate, junction.hashes);
|
|
354
|
+
if (!next)
|
|
355
|
+
return false;
|
|
356
|
+
trieHash = next;
|
|
357
|
+
backBlock = rootToBlock.get(hex(trieHash));
|
|
358
|
+
if (!backBlock)
|
|
359
|
+
return false;
|
|
360
|
+
i = j + 1;
|
|
361
|
+
if (bytesEqual(trieHash, root))
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
return bytesEqual(root, trieHash);
|
|
365
|
+
}
|
|
366
|
+
function marfProofAncestors(proof) {
|
|
367
|
+
let steps;
|
|
368
|
+
try {
|
|
369
|
+
steps = decodeProof(proof);
|
|
370
|
+
} catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const ids = new Set;
|
|
374
|
+
for (const s of steps) {
|
|
375
|
+
if (s.kind === "Leaf" || s.kind === "Shunt")
|
|
376
|
+
continue;
|
|
377
|
+
const ptr = s.node.ptrs.find((p) => p.id !== NodeId.Empty && p.chr === s.chr);
|
|
378
|
+
if (ptr && isBackptr(ptr.id))
|
|
379
|
+
ids.add(hex(ptr.backBlock));
|
|
380
|
+
}
|
|
381
|
+
return [...ids];
|
|
382
|
+
}
|
|
383
|
+
function marfProofValue(proof) {
|
|
384
|
+
try {
|
|
385
|
+
const leaf = decodeProof(proof)[0];
|
|
386
|
+
return leaf?.kind === "Leaf" ? leaf.value : null;
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function verifyMarfProof(input) {
|
|
392
|
+
const rootToBlock = new Map;
|
|
393
|
+
for (const h of input.headers)
|
|
394
|
+
rootToBlock.set(hex(h.stateIndexRoot), blockId(h));
|
|
395
|
+
try {
|
|
396
|
+
const steps = decodeProof(input.proof);
|
|
397
|
+
return verifySteps(steps, input.path, input.value, input.root, rootToBlock);
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// src/witness.ts
|
|
403
|
+
var WITNESS_VERSION = 3;
|
|
404
|
+
var PATH_LEN = 32;
|
|
405
|
+
var TRIE_HASH_EMPTY = unhex("c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a");
|
|
406
|
+
var ZERO_BLOCK = new Uint8Array(32);
|
|
407
|
+
function parseWitness(bytes) {
|
|
408
|
+
const r = new Reader(bytes);
|
|
409
|
+
const version = r.u8();
|
|
410
|
+
if (version !== WITNESS_VERSION)
|
|
411
|
+
throw new Error(`unsupported witness version ${version}`);
|
|
412
|
+
const ancestorRoots = Array.from({ length: r.u32() }, () => r.bytes(32));
|
|
413
|
+
const blockTable = Array.from({ length: r.u32() }, () => r.bytes(32));
|
|
414
|
+
const leaves = [];
|
|
415
|
+
const seen = new Set;
|
|
416
|
+
let nodes = 0;
|
|
417
|
+
const readNode = (prefix, expectedId) => {
|
|
418
|
+
nodes++;
|
|
419
|
+
const id = r.u8();
|
|
420
|
+
if (expectedId !== null && id !== expectedId)
|
|
421
|
+
throw new Error(`child id ${id} does not match its pointer id ${expectedId}`);
|
|
422
|
+
const path = r.bytes(r.u8());
|
|
423
|
+
const depth = prefix.length + path.length;
|
|
424
|
+
if (id === NodeId.Leaf) {
|
|
425
|
+
if (depth !== PATH_LEN)
|
|
426
|
+
throw new Error("leaf path does not complete 32 bytes");
|
|
427
|
+
const valueHash = r.bytes(32);
|
|
428
|
+
const full = Uint8Array.from([...prefix, ...path]);
|
|
429
|
+
const key = hex(full);
|
|
430
|
+
if (seen.has(key))
|
|
431
|
+
throw new Error(`duplicate leaf ${key}`);
|
|
432
|
+
seen.add(key);
|
|
433
|
+
leaves.push({ path: full, valueHash });
|
|
434
|
+
return leafHash(path, padValue(valueHash));
|
|
435
|
+
}
|
|
436
|
+
const count = NODE_PTR_COUNT[id];
|
|
437
|
+
if (count === undefined)
|
|
438
|
+
throw new Error(`bad node id ${id}`);
|
|
439
|
+
if (depth >= PATH_LEN)
|
|
440
|
+
throw new Error("interior node at full path depth");
|
|
441
|
+
const n = r.u16();
|
|
442
|
+
if (n !== count)
|
|
443
|
+
throw new Error(`node id ${id} has ${n} ptrs, expected ${count}`);
|
|
444
|
+
const ptrs = [];
|
|
445
|
+
for (let i = 0;i < n; i++) {
|
|
446
|
+
const pid = r.u8();
|
|
447
|
+
if (pid === NodeId.Empty) {
|
|
448
|
+
ptrs.push({ id: pid, chr: 0, backBlock: ZERO_BLOCK });
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const chr = r.u8();
|
|
452
|
+
if (!isBackptr(pid)) {
|
|
453
|
+
ptrs.push({ id: pid, chr, backBlock: ZERO_BLOCK });
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const block = blockTable[r.u32()];
|
|
457
|
+
if (!block)
|
|
458
|
+
throw new Error("backptr table index out of range");
|
|
459
|
+
ptrs.push({ id: pid, chr, backBlock: block });
|
|
460
|
+
}
|
|
461
|
+
const childPrefix = [...prefix, ...path];
|
|
462
|
+
const children = ptrs.map((p) => {
|
|
463
|
+
if (p.id === NodeId.Empty)
|
|
464
|
+
return TRIE_HASH_EMPTY;
|
|
465
|
+
if (isBackptr(p.id))
|
|
466
|
+
return p.backBlock;
|
|
467
|
+
return readNode([...childPrefix, p.chr], p.id);
|
|
468
|
+
});
|
|
469
|
+
return nodeHash({ id, path, ptrs }, children);
|
|
470
|
+
};
|
|
471
|
+
const nodeRoot = readNode([], null);
|
|
472
|
+
if (!r.done)
|
|
473
|
+
throw new Error("trailing bytes after witness");
|
|
474
|
+
const root = ancestorRoots.length === 0 ? nodeRoot : hashAll([nodeRoot, ...ancestorRoots]);
|
|
475
|
+
return { root, ancestorRoots, leaves, nodes };
|
|
476
|
+
}
|
|
477
|
+
var padValue = (valueHash) => {
|
|
478
|
+
const v = new Uint8Array(MARF_VALUE_SIZE);
|
|
479
|
+
v.set(valueHash);
|
|
480
|
+
return v;
|
|
481
|
+
};
|
|
482
|
+
var leafHolds = (leaf, value) => value.length === MARF_VALUE_SIZE && bytesEqual(padValue(leaf.valueHash), value);
|
|
483
|
+
var INTERNAL_PREFIX = "__MARF_";
|
|
484
|
+
function classifyBlockDiff(input) {
|
|
485
|
+
const writes = indexClaims(input.writes, (v) => marfValue(v));
|
|
486
|
+
const internal = indexClaims(input.internal, (v) => {
|
|
487
|
+
const value = unhex(v);
|
|
488
|
+
if (value.length !== MARF_VALUE_SIZE)
|
|
489
|
+
throw new Error(`internal value must be ${MARF_VALUE_SIZE} bytes`);
|
|
490
|
+
return value;
|
|
491
|
+
});
|
|
492
|
+
for (const { key } of internal.values())
|
|
493
|
+
if (!key.startsWith(INTERNAL_PREFIX))
|
|
494
|
+
throw new Error(`internal key must start with ${INTERNAL_PREFIX}: ${key}`);
|
|
495
|
+
const out = {
|
|
496
|
+
writes: [],
|
|
497
|
+
carried: [],
|
|
498
|
+
internal: [],
|
|
499
|
+
hidden: [],
|
|
500
|
+
missing: [],
|
|
501
|
+
complete: false
|
|
502
|
+
};
|
|
503
|
+
const matched = new Set;
|
|
504
|
+
for (const leaf of input.leaves) {
|
|
505
|
+
const pathHex = hex(leaf.path);
|
|
506
|
+
const write = writes.get(pathHex);
|
|
507
|
+
if (write && leafHolds(leaf, write.value)) {
|
|
508
|
+
out.writes.push({ ...leaf, key: write.key });
|
|
509
|
+
matched.add(pathHex);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const bookkeeping = internal.get(pathHex);
|
|
513
|
+
if (bookkeeping && leafHolds(leaf, bookkeeping.value)) {
|
|
514
|
+
out.internal.push({ ...leaf, key: bookkeeping.key });
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const parent = write ? undefined : input.parentValue(pathHex);
|
|
518
|
+
if (parent !== undefined && leafHolds(leaf, unhex(parent)))
|
|
519
|
+
out.carried.push(leaf);
|
|
520
|
+
else
|
|
521
|
+
out.hidden.push(leaf);
|
|
522
|
+
}
|
|
523
|
+
for (const [pathHex, { key }] of writes)
|
|
524
|
+
if (!matched.has(pathHex))
|
|
525
|
+
out.missing.push(key);
|
|
526
|
+
out.complete = out.hidden.length === 0 && out.missing.length === 0;
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
function indexClaims(claims, toValue) {
|
|
530
|
+
const byPath = new Map;
|
|
531
|
+
for (const [key, value] of claims) {
|
|
532
|
+
const pathHex = hex(marfPath(key));
|
|
533
|
+
if (byPath.has(pathHex))
|
|
534
|
+
throw new Error(`duplicate claimed key ${key}`);
|
|
535
|
+
byPath.set(pathHex, { key, value: toValue(value) });
|
|
536
|
+
}
|
|
537
|
+
return byPath;
|
|
538
|
+
}
|
|
539
|
+
// src/signers.ts
|
|
540
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
541
|
+
import { deserializeCV } from "@secondlayer/stacks/clarity";
|
|
542
|
+
import { c32address, hash160 } from "@secondlayer/stacks/utils";
|
|
543
|
+
var MAINNET_P2PKH = 22;
|
|
544
|
+
var SIGNERS_CONTRACT = "SP000000000000000000002Q6VF78.signers";
|
|
545
|
+
function decodeSignerSet(value) {
|
|
546
|
+
const cv = deserializeCV(typeof value === "string" ? value : hex(value));
|
|
547
|
+
if (cv.type !== "some" || cv.value.type !== "list")
|
|
548
|
+
throw new Error("signer set must be (some (list ...))");
|
|
549
|
+
const set = new Map;
|
|
550
|
+
for (const entry of cv.value.value) {
|
|
551
|
+
if (entry.type !== "tuple")
|
|
552
|
+
throw new Error("signer entry must be a tuple");
|
|
553
|
+
const { signer, weight } = entry.value;
|
|
554
|
+
if (signer?.type !== "address" || weight?.type !== "uint")
|
|
555
|
+
throw new Error("signer entry needs (signer principal) (weight uint)");
|
|
556
|
+
if (set.has(signer.value))
|
|
557
|
+
throw new Error(`duplicate signer ${signer.value}`);
|
|
558
|
+
set.set(signer.value, BigInt(weight.value));
|
|
559
|
+
}
|
|
560
|
+
return set;
|
|
561
|
+
}
|
|
562
|
+
function verifySignerSignatures(header, set) {
|
|
563
|
+
const msg = signerSignatureHash(header);
|
|
564
|
+
let totalWeight = 0n;
|
|
565
|
+
for (const w of set.values())
|
|
566
|
+
totalWeight += w;
|
|
567
|
+
const threshold = (totalWeight * 7n + 9n) / 10n;
|
|
568
|
+
const out = {
|
|
569
|
+
valid: false,
|
|
570
|
+
signedWeight: 0n,
|
|
571
|
+
totalWeight,
|
|
572
|
+
threshold,
|
|
573
|
+
signers: [],
|
|
574
|
+
unknown: [],
|
|
575
|
+
duplicates: [],
|
|
576
|
+
invalid: 0
|
|
577
|
+
};
|
|
578
|
+
const seen = new Set;
|
|
579
|
+
for (const sig of header.signerSignatures) {
|
|
580
|
+
const who = recoverSigner(msg, sig);
|
|
581
|
+
if (!who)
|
|
582
|
+
out.invalid++;
|
|
583
|
+
else if (seen.has(who))
|
|
584
|
+
out.duplicates.push(who);
|
|
585
|
+
else {
|
|
586
|
+
seen.add(who);
|
|
587
|
+
const weight = set.get(who);
|
|
588
|
+
if (weight === undefined)
|
|
589
|
+
out.unknown.push(who);
|
|
590
|
+
else {
|
|
591
|
+
out.signers.push(who);
|
|
592
|
+
out.signedWeight += weight;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
out.valid = totalWeight > 0n && out.signedWeight >= threshold && out.unknown.length === 0 && out.duplicates.length === 0 && out.invalid === 0;
|
|
597
|
+
return out;
|
|
598
|
+
}
|
|
599
|
+
function recoverSigner(msg, sig) {
|
|
600
|
+
try {
|
|
601
|
+
const s = secp256k1.Signature.fromBytes(sig, "recovered");
|
|
602
|
+
if (s.hasHighS())
|
|
603
|
+
return null;
|
|
604
|
+
const pubkey = s.recoverPublicKey(msg).toBytes(true);
|
|
605
|
+
return c32address(MAINNET_P2PKH, hex(hash160(pubkey)));
|
|
606
|
+
} catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// src/burn.ts
|
|
611
|
+
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
612
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
613
|
+
var PREIMAGE_PREFIX = Uint8Array.of(23, 0, 0, 0);
|
|
614
|
+
var BHH_START = 4;
|
|
615
|
+
var BHH_END = 36;
|
|
616
|
+
function verifyConsensusPreimage(consensusHash, preimage) {
|
|
617
|
+
if (consensusHash.length !== 20 || preimage.length < BHH_END)
|
|
618
|
+
return null;
|
|
619
|
+
if (!bytesEqual(preimage.subarray(0, BHH_START), PREIMAGE_PREFIX))
|
|
620
|
+
return null;
|
|
621
|
+
if (!bytesEqual(ripemd160(sha256(preimage)), consensusHash))
|
|
622
|
+
return null;
|
|
623
|
+
return hex(preimage.subarray(BHH_START, BHH_END));
|
|
624
|
+
}
|
|
625
|
+
var MAINNET_FIRST_BURN_HEIGHT = 666050;
|
|
626
|
+
var MAINNET_REWARD_CYCLE_LENGTH = 2100;
|
|
627
|
+
var MAINNET_PREPARE_LENGTH = 100;
|
|
628
|
+
var cycleStart = (cycle, firstBurnHeight = MAINNET_FIRST_BURN_HEIGHT, cycleLength = MAINNET_REWARD_CYCLE_LENGTH) => firstBurnHeight + cycle * cycleLength;
|
|
629
|
+
function rewardCycle(burnHeight, firstBurnHeight = MAINNET_FIRST_BURN_HEIGHT, cycleLength = MAINNET_REWARD_CYCLE_LENGTH) {
|
|
630
|
+
if (burnHeight < firstBurnHeight)
|
|
631
|
+
throw new Error(`burn height ${burnHeight} precedes the first burn block`);
|
|
632
|
+
return Math.floor((burnHeight - firstBurnHeight) / cycleLength);
|
|
633
|
+
}
|
|
634
|
+
// src/keys.ts
|
|
635
|
+
import { serializeCV } from "@secondlayer/stacks/clarity";
|
|
636
|
+
import { c32addressDecode } from "@secondlayer/stacks/utils";
|
|
637
|
+
var StoreType = { DataMap: 0, Variable: 1, FungibleToken: 2 };
|
|
638
|
+
function mapEntryKey(contractId, mapName, key) {
|
|
639
|
+
return `vm::${contractId}::${StoreType.DataMap}::${mapName}::${serializeCV(key)}`;
|
|
640
|
+
}
|
|
641
|
+
function dataVarKey(contractId, varName) {
|
|
642
|
+
return `vm::${contractId}::${StoreType.Variable}::${varName}`;
|
|
643
|
+
}
|
|
644
|
+
function ftBalanceKey(contractId, tokenName, holder) {
|
|
645
|
+
return `vm::${contractId}::${StoreType.FungibleToken}::${tokenName}::${principalJson(holder)}`;
|
|
646
|
+
}
|
|
647
|
+
function principalJson(principal) {
|
|
648
|
+
const dot = principal.indexOf(".");
|
|
649
|
+
const address = dot === -1 ? principal : principal.slice(0, dot);
|
|
650
|
+
const [version, hash160] = c32addressDecode(address);
|
|
651
|
+
const issuer = [version, Array.from(unhex(hash160))];
|
|
652
|
+
if (dot === -1)
|
|
653
|
+
return JSON.stringify({ Standard: issuer });
|
|
654
|
+
return JSON.stringify({
|
|
655
|
+
Contract: { issuer, name: principal.slice(dot + 1) }
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
// src/bitcoin/chain.ts
|
|
659
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3 } from "@noble/hashes/utils.js";
|
|
660
|
+
|
|
661
|
+
// src/bitcoin/header.ts
|
|
662
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
663
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/hashes/utils.js";
|
|
664
|
+
var HEADER_SIZE = 80;
|
|
665
|
+
function toBytes(raw) {
|
|
666
|
+
return typeof raw === "string" ? hexToBytes2(raw) : raw;
|
|
667
|
+
}
|
|
668
|
+
function reverseBytes(bytes) {
|
|
669
|
+
return Uint8Array.from(bytes).reverse();
|
|
670
|
+
}
|
|
671
|
+
function parseHeader(raw) {
|
|
672
|
+
const bytes = toBytes(raw);
|
|
673
|
+
if (bytes.length !== HEADER_SIZE) {
|
|
674
|
+
throw new RangeError(`block header must be ${HEADER_SIZE} bytes, got ${bytes.length}`);
|
|
675
|
+
}
|
|
676
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, HEADER_SIZE);
|
|
677
|
+
return {
|
|
678
|
+
version: view.getInt32(0, true),
|
|
679
|
+
prevHash: bytesToHex2(reverseBytes(bytes.subarray(4, 36))),
|
|
680
|
+
merkleRoot: bytesToHex2(reverseBytes(bytes.subarray(36, 68))),
|
|
681
|
+
time: view.getUint32(68, true),
|
|
682
|
+
bits: view.getUint32(72, true),
|
|
683
|
+
nonce: view.getUint32(76, true)
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function headerHash(raw) {
|
|
687
|
+
const bytes = toBytes(raw);
|
|
688
|
+
if (bytes.length !== HEADER_SIZE) {
|
|
689
|
+
throw new RangeError(`block header must be ${HEADER_SIZE} bytes, got ${bytes.length}`);
|
|
690
|
+
}
|
|
691
|
+
return sha2562(sha2562(bytes));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/bitcoin/pow.ts
|
|
695
|
+
var POW_LIMIT = 0xffffn << 208n;
|
|
696
|
+
var RETARGET_INTERVAL = 2016;
|
|
697
|
+
var TARGET_TIMESPAN = 14 * 24 * 60 * 60;
|
|
698
|
+
var TWO_256 = 1n << 256n;
|
|
699
|
+
function decodeCompact(bits) {
|
|
700
|
+
const size = bits >>> 24;
|
|
701
|
+
let word = bits & 8388607;
|
|
702
|
+
let target;
|
|
703
|
+
if (size <= 3) {
|
|
704
|
+
word >>>= 8 * (3 - size);
|
|
705
|
+
target = BigInt(word);
|
|
706
|
+
} else {
|
|
707
|
+
target = BigInt(word) << BigInt(8 * (size - 3));
|
|
708
|
+
}
|
|
709
|
+
const negative = word !== 0 && (bits & 8388608) !== 0;
|
|
710
|
+
const overflow = word !== 0 && (size > 34 || word > 255 && size > 33 || word > 65535 && size > 32);
|
|
711
|
+
return { target: target & TWO_256 - 1n, negative, overflow };
|
|
712
|
+
}
|
|
713
|
+
function bitsToTarget(bits) {
|
|
714
|
+
const { target, negative, overflow } = decodeCompact(bits);
|
|
715
|
+
if (negative || overflow || target === 0n) {
|
|
716
|
+
throw new RangeError(`bits 0x${bits.toString(16).padStart(8, "0")} do not encode a valid target`);
|
|
717
|
+
}
|
|
718
|
+
return target;
|
|
719
|
+
}
|
|
720
|
+
function targetToBits(target) {
|
|
721
|
+
if (target < 0n)
|
|
722
|
+
throw new RangeError("target must be non-negative");
|
|
723
|
+
let size = target === 0n ? 0 : Math.ceil(target.toString(2).length / 8);
|
|
724
|
+
let compact = size <= 3 ? Number(target << BigInt(8 * (3 - size))) : Number(target >> BigInt(8 * (size - 3)));
|
|
725
|
+
if (compact & 8388608) {
|
|
726
|
+
compact >>>= 8;
|
|
727
|
+
size += 1;
|
|
728
|
+
}
|
|
729
|
+
return (size << 24 | compact) >>> 0;
|
|
730
|
+
}
|
|
731
|
+
function hashToBigInt(hash) {
|
|
732
|
+
let n = 0n;
|
|
733
|
+
for (let i = hash.length - 1;i >= 0; i--) {
|
|
734
|
+
n = n << 8n | BigInt(hash[i]);
|
|
735
|
+
}
|
|
736
|
+
return n;
|
|
737
|
+
}
|
|
738
|
+
function checkProofOfWork(hash, bits) {
|
|
739
|
+
const { target, negative, overflow } = decodeCompact(bits);
|
|
740
|
+
if (negative || overflow || target === 0n || target > POW_LIMIT) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
return hashToBigInt(hash) <= target;
|
|
744
|
+
}
|
|
745
|
+
function headerWork(bits) {
|
|
746
|
+
const target = bitsToTarget(bits);
|
|
747
|
+
return (TWO_256 - 1n - target) / (target + 1n) + 1n;
|
|
748
|
+
}
|
|
749
|
+
function nextRetargetBits(lastBits, firstTime, lastTime) {
|
|
750
|
+
let actual = lastTime - firstTime;
|
|
751
|
+
if (actual < TARGET_TIMESPAN / 4)
|
|
752
|
+
actual = TARGET_TIMESPAN / 4;
|
|
753
|
+
if (actual > TARGET_TIMESPAN * 4)
|
|
754
|
+
actual = TARGET_TIMESPAN * 4;
|
|
755
|
+
let target = bitsToTarget(lastBits) * BigInt(actual) / BigInt(TARGET_TIMESPAN);
|
|
756
|
+
if (target > POW_LIMIT)
|
|
757
|
+
target = POW_LIMIT;
|
|
758
|
+
return targetToBits(target);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/bitcoin/chain.ts
|
|
762
|
+
var MEDIAN_TIME_SPAN = 11;
|
|
763
|
+
var BIP65_HEIGHT = 388381;
|
|
764
|
+
|
|
765
|
+
class HeaderValidationError extends Error {
|
|
766
|
+
height;
|
|
767
|
+
rule;
|
|
768
|
+
name = "HeaderValidationError";
|
|
769
|
+
constructor(height, rule, detail) {
|
|
770
|
+
super(`header at height ${height} failed ${rule}: ${detail}`);
|
|
771
|
+
this.height = height;
|
|
772
|
+
this.rule = rule;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
var hex8 = (n) => `0x${n.toString(16).padStart(8, "0")}`;
|
|
776
|
+
|
|
777
|
+
class HeaderChain {
|
|
778
|
+
base;
|
|
779
|
+
periodStartTime;
|
|
780
|
+
entries = [];
|
|
781
|
+
heights = new Map;
|
|
782
|
+
work = 0n;
|
|
783
|
+
constructor(base, periodStartTime) {
|
|
784
|
+
this.base = base;
|
|
785
|
+
this.periodStartTime = periodStartTime;
|
|
786
|
+
}
|
|
787
|
+
static fromCheckpoint(checkpoint) {
|
|
788
|
+
const { height } = checkpoint;
|
|
789
|
+
if (!Number.isSafeInteger(height) || height < 0) {
|
|
790
|
+
throw new HeaderValidationError(height, "checkpoint", "height must be a non-negative integer");
|
|
791
|
+
}
|
|
792
|
+
const raw = toRaw(checkpoint.header, height);
|
|
793
|
+
const header = parseHeader(raw);
|
|
794
|
+
try {
|
|
795
|
+
bitsToTarget(header.bits);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
throw new HeaderValidationError(height, "checkpoint", err.message);
|
|
798
|
+
}
|
|
799
|
+
const atPeriodStart = height % RETARGET_INTERVAL === 0;
|
|
800
|
+
let periodStartTime = checkpoint.periodStartTime;
|
|
801
|
+
if (atPeriodStart) {
|
|
802
|
+
if (periodStartTime !== undefined && periodStartTime !== header.time) {
|
|
803
|
+
throw new HeaderValidationError(height, "checkpoint", `periodStartTime ${periodStartTime} disagrees with the period-start checkpoint's own time ${header.time}`);
|
|
804
|
+
}
|
|
805
|
+
periodStartTime = header.time;
|
|
806
|
+
} else if (periodStartTime === undefined) {
|
|
807
|
+
throw new HeaderValidationError(height, "checkpoint", `periodStartTime is required for a checkpoint inside a difficulty period (period starts at ${height - height % RETARGET_INTERVAL})`);
|
|
808
|
+
}
|
|
809
|
+
const chain = new HeaderChain(height, periodStartTime);
|
|
810
|
+
chain.push(raw, header);
|
|
811
|
+
return chain;
|
|
812
|
+
}
|
|
813
|
+
get tip() {
|
|
814
|
+
return this.entries[this.entries.length - 1];
|
|
815
|
+
}
|
|
816
|
+
get totalWork() {
|
|
817
|
+
return this.work;
|
|
818
|
+
}
|
|
819
|
+
get checkpointHeight() {
|
|
820
|
+
return this.base;
|
|
821
|
+
}
|
|
822
|
+
heightOf(hashHex) {
|
|
823
|
+
return this.heights.get(hashHex.toLowerCase());
|
|
824
|
+
}
|
|
825
|
+
headerAt(height) {
|
|
826
|
+
return this.entries[height - this.base];
|
|
827
|
+
}
|
|
828
|
+
append(headers) {
|
|
829
|
+
const before = this.entries.length;
|
|
830
|
+
const workBefore = this.work;
|
|
831
|
+
try {
|
|
832
|
+
for (const item of headers) {
|
|
833
|
+
const height = this.tip.height + 1;
|
|
834
|
+
const raw = toRaw(item, height);
|
|
835
|
+
this.validate(height, raw, parseHeader(raw));
|
|
836
|
+
}
|
|
837
|
+
} catch (err) {
|
|
838
|
+
for (const removed of this.entries.splice(before)) {
|
|
839
|
+
this.heights.delete(removed.hash);
|
|
840
|
+
}
|
|
841
|
+
this.work = workBefore;
|
|
842
|
+
throw err;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
validate(height, raw, header) {
|
|
846
|
+
const prev = this.tip;
|
|
847
|
+
if (header.prevHash !== prev.hash) {
|
|
848
|
+
const known = this.heights.get(header.prevHash);
|
|
849
|
+
throw new HeaderValidationError(height, "prev-link", known === undefined ? `prevHash ${header.prevHash} does not match tip ${prev.hash} at ${prev.height}` : `prevHash points at height ${known}, not the tip at ${prev.height} (forks/reorgs are not supported)`);
|
|
850
|
+
}
|
|
851
|
+
const expectedBits = this.expectedBits(height, prev);
|
|
852
|
+
if (header.bits !== expectedBits) {
|
|
853
|
+
const atRetarget = height % RETARGET_INTERVAL === 0;
|
|
854
|
+
throw new HeaderValidationError(height, atRetarget ? "retarget" : "bits", atRetarget ? `bits ${hex8(header.bits)} != retargeted ${hex8(expectedBits)}` : `bits ${hex8(header.bits)} != previous ${hex8(expectedBits)} (only height % ${RETARGET_INTERVAL} == 0 may change difficulty)`);
|
|
855
|
+
}
|
|
856
|
+
const mtp = this.medianTimePast();
|
|
857
|
+
if (header.time <= mtp) {
|
|
858
|
+
throw new HeaderValidationError(height, "median-time-past", `time ${header.time} <= median of previous ${Math.min(MEDIAN_TIME_SPAN, this.entries.length)} blocks (${mtp})`);
|
|
859
|
+
}
|
|
860
|
+
if (height >= BIP65_HEIGHT && header.version < 4) {
|
|
861
|
+
throw new HeaderValidationError(height, "version", `version ${header.version} < 4 (required from height ${BIP65_HEIGHT})`);
|
|
862
|
+
}
|
|
863
|
+
const hash = headerHash(raw);
|
|
864
|
+
if (!checkProofOfWork(hash, header.bits)) {
|
|
865
|
+
throw new HeaderValidationError(height, "proof-of-work", `hash ${bytesToHex3(reverseBytes(hash))} exceeds target for bits ${hex8(header.bits)}`);
|
|
866
|
+
}
|
|
867
|
+
this.push(raw, header, hash);
|
|
868
|
+
}
|
|
869
|
+
expectedBits(height, prev) {
|
|
870
|
+
if (height % RETARGET_INTERVAL !== 0)
|
|
871
|
+
return prev.header.bits;
|
|
872
|
+
const firstHeight = height - RETARGET_INTERVAL;
|
|
873
|
+
const firstTime = firstHeight < this.base ? this.periodStartTime : this.headerAt(firstHeight).header.time;
|
|
874
|
+
return nextRetargetBits(prev.header.bits, firstTime, prev.header.time);
|
|
875
|
+
}
|
|
876
|
+
medianTimePast() {
|
|
877
|
+
const times = this.entries.slice(-MEDIAN_TIME_SPAN).map((e) => e.header.time).sort((a, b) => a - b);
|
|
878
|
+
return times[Math.floor(times.length / 2)];
|
|
879
|
+
}
|
|
880
|
+
push(raw, header, hash = headerHash(raw)) {
|
|
881
|
+
const height = this.base + this.entries.length;
|
|
882
|
+
const hashHex = bytesToHex3(reverseBytes(hash));
|
|
883
|
+
this.entries.push({ height, hash: hashHex, header, raw });
|
|
884
|
+
this.heights.set(hashHex, height);
|
|
885
|
+
this.work += headerWork(header.bits);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function toRaw(item, height) {
|
|
889
|
+
let raw;
|
|
890
|
+
try {
|
|
891
|
+
raw = typeof item === "string" ? hexToBytes3(item) : Uint8Array.from(item);
|
|
892
|
+
} catch {
|
|
893
|
+
throw new HeaderValidationError(height, "format", "header is not valid hex");
|
|
894
|
+
}
|
|
895
|
+
if (raw.length !== HEADER_SIZE) {
|
|
896
|
+
throw new HeaderValidationError(height, "format", `header must be ${HEADER_SIZE} bytes, got ${raw.length}`);
|
|
897
|
+
}
|
|
898
|
+
return raw;
|
|
899
|
+
}
|
|
900
|
+
// src/checkpoint.ts
|
|
901
|
+
var MAINNET_CHECKPOINT = {
|
|
902
|
+
stacks: {
|
|
903
|
+
header: [
|
|
904
|
+
"01000000000088a9900000006629951a50e3616a1c1e0b8c05ba66615c7065053804c56e2a68dc605cf905df5a754c57",
|
|
905
|
+
"144e9ddde1191353b05f1a5e6d3c99244aeb006ba4f4db761cb1efecfb92693a8c08fefc766a4cd5ccfcb8f13789a809",
|
|
906
|
+
"e7ee0c2e6b7dfb64868cf1e4e6acc50513202acfab84553454b433248c0ef06cebda3c734c000000006aa28bfd013be3",
|
|
907
|
+
"826a60039f518d7021a4799f2e083dafaaaac7a2d56fda601d674d596d3046b9bbcd52e79fba491e54d92fdab0ca3ab9",
|
|
908
|
+
"9ccf346c6e6726fbd4c8579a02b80000000d0004f2d135dd684fcdf24973ad0e9d4f11ef10890a8159614b1df97d8f31",
|
|
909
|
+
"76532d1ce4ebdcbbf45afcd01d5bb298ef9556b232f877f3586d65303642dabc210059012d5f341c979fd10a1ce9b48f",
|
|
910
|
+
"4a38f68da5ccbb8749c02e164a99668328890ed706accfbec155202f63ff487a26aba793169341c2ff91a43ffed38f0e",
|
|
911
|
+
"941c3dc1012a12bb2148d6cf23dd05e228d29286e3bb87ff717712ecc2c9f0c4297c241ac41f879baeee87a570243a31",
|
|
912
|
+
"cf43e775862e71a7f51cb150bbc31f3a2f62a1772d00f8bdd615ab5b58841c7918d69cf98a030f52ca410c9b91a188ba",
|
|
913
|
+
"038138563ea35f8453075907fc3cdac7e3eff8fa9f61fcb53a1e14a102309fa297e420826bfe015628863a16f1bdc077",
|
|
914
|
+
"90c6eead0cfd7a0aa7222df6d8d41415427b0b4057eb972dc31bd413f1e425ef1bc738cc5c3b8507890bad0940bafdf2",
|
|
915
|
+
"220a6ecadb33ed002f9a476eb20c5e532bafa7b559b285932e1dab1159c93038d8cb0da8d55d25294cc8e6b0972efc08",
|
|
916
|
+
"fdf825407e3215d8d01531f5cddfe993db10be07b38c18fa006999eb6418eac4696bb42ca149f79832442754ea08b0e1",
|
|
917
|
+
"c6237feb522df034a3388c0de90b1a4652218b319f839ff16a132baa90dbbca2876f52b1a5750dc07001cffca3ed6e92",
|
|
918
|
+
"2483e6567f183a78a1c09d16846743f08c5cce93e7e06294c4a722283d31195722b6ce97087c6dd00eedecd1db77be10",
|
|
919
|
+
"9c298c322c53e83ea43201dd0684e8171ad29f6895ed9892d3932daeb0986f32992d6758db194ae9ba8eb256fdff3b61",
|
|
920
|
+
"af0cd448aafe5e4e62e7e75409dfcd3cef3913c792e412849a02550109083fa52598f5fa2907e840050c41db32163838",
|
|
921
|
+
"32aa7cb5e23de14ced47961e356b23e442f2a657ea89fed2845a6b380624dc12347e4d591c80e15016ae35fc005ae002",
|
|
922
|
+
"f1fc5837c21185b88b06cc8611cfb482c021df5329d878e2b4f221341a47e6fe02182c9250d25fe162f42a0bbe9b8ce2",
|
|
923
|
+
"f9a70aa2dcca9e955a61a0fe24016398a0fdf5e2541d3d0838d9f7e24b4093d27660a48cd71bf0728fe5d02e6d374a3e",
|
|
924
|
+
"aa67c91aee87635d7458bb1fe89755021fbe8d34e271fba1d765b065e266008a862c839aeec0d6a9940cfe44e0bc1586",
|
|
925
|
+
"2bd2ac23b3ab6fc59734a8171b44401b395984f0f906b2d15b4a3a24d2136a8ca925485f197197b3aa1bb2c46d02e000",
|
|
926
|
+
"01000000010100000000"
|
|
927
|
+
].join(""),
|
|
928
|
+
cycle: 143,
|
|
929
|
+
signerSet: [
|
|
930
|
+
"0a0b0000001d0c00000002067369676e65720516f5061b1c62f399ecc0db8cbb21f8d481af079a380677656967687401",
|
|
931
|
+
"000000000000000000000000000002860c00000002067369676e65720516a3bbf2a4133fbd5bd1880982957b7a760347",
|
|
932
|
+
"b2dc0677656967687401000000000000000000000000000000090c00000002067369676e657205164f72f6b5a9f5d863",
|
|
933
|
+
"34b2f0fe701db51a7a246f3d0677656967687401000000000000000000000000000000990c00000002067369676e6572",
|
|
934
|
+
"051653ec8b4f51c8286dad6f4ff81ca2a3409d2f382d0677656967687401000000000000000000000000000000050c00",
|
|
935
|
+
"000002067369676e65720516dba631825ff0f15c7c5946b560070f8f5f5747cb06776569676874010000000000000000",
|
|
936
|
+
"00000000000000240c00000002067369676e6572051600d058b39f4495b412ff7eada763eaa6386eb90a067765696768",
|
|
937
|
+
"7401000000000000000000000000000002860c00000002067369676e65720516321ac33715d684d5e8d0cf427cd98557",
|
|
938
|
+
"6f64c6df06776569676874010000000000000000000000000000000b0c00000002067369676e6572051677a52718ba90",
|
|
939
|
+
"fca67984166b2a084edb9b3bd8f10677656967687401000000000000000000000000000000030c00000002067369676e",
|
|
940
|
+
"65720516a54a20479b3b65a461a427b9161b78e64db58ba70677656967687401000000000000000000000000000001a9",
|
|
941
|
+
"0c00000002067369676e65720516bc1a58c2a058a9e4150db25562e8331ef8a4267c0677656967687401000000000000",
|
|
942
|
+
"000000000000000000060c00000002067369676e657205161091d2da4c9ee17a340171c0d23c4304b54ef43f06776569",
|
|
943
|
+
"67687401000000000000000000000000000000680c00000002067369676e6572051654d01a0973ed139736a1a43032a6",
|
|
944
|
+
"a18d9295ec7206776569676874010000000000000000000000000000008d0c00000002067369676e657205160c49f5b6",
|
|
945
|
+
"c62f1163ddbb21f92aba86f17f7be4880677656967687401000000000000000000000000000000680c00000002067369",
|
|
946
|
+
"676e65720516ad16c79ac56b58f0797f78c5343853674606268006776569676874010000000000000000000000000000",
|
|
947
|
+
"00680c00000002067369676e657205169689e56df00385b6bd81fb6b7769d5b04088acb8067765696768740100000000",
|
|
948
|
+
"0000000000000000000000ad0c00000002067369676e65720516a4f7f81412f4413771a48825cfbe2062a774d4e90677",
|
|
949
|
+
"656967687401000000000000000000000000000003090c00000002067369676e65720516ed73a7398f1c3eb566f3bd7f",
|
|
950
|
+
"4bd43f52adf33d8f0677656967687401000000000000000000000000000000680c00000002067369676e65720516937b",
|
|
951
|
+
"3b43e0d94c9ef8f171892bce949709e1d41f0677656967687401000000000000000000000000000000250c0000000206",
|
|
952
|
+
"7369676e6572051631542aacfd6e04858181c3067b5eb07399ba89df0677656967687401000000000000000000000000",
|
|
953
|
+
"0000005b0c00000002067369676e6572051649f034cd1662a222dbc38a6acd64008ba7e5576606776569676874010000",
|
|
954
|
+
"00000000000000000000000000060c00000002067369676e65720516a20f660dea498c169897a39cb89ce0d98856e572",
|
|
955
|
+
"0677656967687401000000000000000000000000000000090c00000002067369676e6572051652d58897b3678f3a758f",
|
|
956
|
+
"a4b9270744fec0605b210677656967687401000000000000000000000000000000680c00000002067369676e65720516",
|
|
957
|
+
"31733264626dc5fef36eb683a5069d89f63b64cc0677656967687401000000000000000000000000000000090c000000",
|
|
958
|
+
"02067369676e65720516d80db618c2def577604cd8e40a3940b5b639b1ac067765696768740100000000000000000000",
|
|
959
|
+
"00000000000a0c00000002067369676e657205162042a380d946a82866465af8521a273efe6d2a600677656967687401",
|
|
960
|
+
"000000000000000000000000000000490c00000002067369676e6572051635e1b36efc22bc607db92f9100cb08e3473d",
|
|
961
|
+
"602506776569676874010000000000000000000000000000006b0c00000002067369676e657205164843b97054e43dd1",
|
|
962
|
+
"a8571fc1e72949c5bedfb7e906776569676874010000000000000000000000000000004d0c00000002067369676e6572",
|
|
963
|
+
"05164883355670025c19ecae644ffa20e75ce1d730ad0677656967687401000000000000000000000000000000090c00",
|
|
964
|
+
"000002067369676e65720516089ffd0e0ecd61d9e3c8ee42ef94148acf88aa8806776569676874010000000000000000",
|
|
965
|
+
"0000000000000015"
|
|
966
|
+
].join("")
|
|
967
|
+
},
|
|
968
|
+
bitcoin: {
|
|
969
|
+
height: 967680,
|
|
970
|
+
header: "00000030410ccb1cba623e26f2e02f3fcb0e7f06f66998b68cb5010000000000000000003d16ae2558e8ec7b8dd369ee29bb2733fbd39bcc1058c681970a36a7f1a2bdea1135ae6ac51e0217480c3581"
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
// src/source.ts
|
|
974
|
+
class SourceError extends Error {
|
|
975
|
+
url;
|
|
976
|
+
status;
|
|
977
|
+
name = "SourceError";
|
|
978
|
+
constructor(url, status, detail) {
|
|
979
|
+
super(`${url}: ${detail}`);
|
|
980
|
+
this.url = url;
|
|
981
|
+
this.status = status;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
var MAX_ATTEMPTS = 3;
|
|
985
|
+
var MAX_RETRY_SECONDS = 10;
|
|
986
|
+
async function get(url, opts, attempt = 1) {
|
|
987
|
+
const doFetch = opts.fetch ?? fetch;
|
|
988
|
+
let res;
|
|
989
|
+
try {
|
|
990
|
+
res = await doFetch(url, { headers: opts.headers });
|
|
991
|
+
} catch (err) {
|
|
992
|
+
throw new SourceError(url, null, err.message);
|
|
993
|
+
}
|
|
994
|
+
if (res.status === 404)
|
|
995
|
+
return null;
|
|
996
|
+
if (res.status === 503 && attempt < MAX_ATTEMPTS) {
|
|
997
|
+
const header = res.headers.get("retry-after");
|
|
998
|
+
const seconds = header === null ? 1 : Number(header);
|
|
999
|
+
const wait = Number.isFinite(seconds) ? Math.min(Math.max(seconds, 0), MAX_RETRY_SECONDS) : 1;
|
|
1000
|
+
await new Promise((r) => setTimeout(r, wait * 1000));
|
|
1001
|
+
return get(url, opts, attempt + 1);
|
|
1002
|
+
}
|
|
1003
|
+
if (!res.ok)
|
|
1004
|
+
throw new SourceError(url, res.status, `HTTP ${res.status}`);
|
|
1005
|
+
return res;
|
|
1006
|
+
}
|
|
1007
|
+
async function getJson(url, opts) {
|
|
1008
|
+
const res = await get(url, opts);
|
|
1009
|
+
return res ? await res.json() : null;
|
|
1010
|
+
}
|
|
1011
|
+
async function getBytes(url, opts) {
|
|
1012
|
+
const res = await get(url, opts);
|
|
1013
|
+
return res ? new Uint8Array(await res.arrayBuffer()) : null;
|
|
1014
|
+
}
|
|
1015
|
+
var required = (url, v) => {
|
|
1016
|
+
if (v === null)
|
|
1017
|
+
throw new SourceError(url, 404, "not found");
|
|
1018
|
+
return v;
|
|
1019
|
+
};
|
|
1020
|
+
var trimSlash = (s) => s.replace(/\/+$/, "");
|
|
1021
|
+
var toEpoch2 = (url, j) => {
|
|
1022
|
+
const tip = required(url, j)[0];
|
|
1023
|
+
if (typeof tip?.header !== "string" || typeof tip.consensus_hash !== "string")
|
|
1024
|
+
throw new SourceError(url, 200, "answer needs header and consensus_hash");
|
|
1025
|
+
return {
|
|
1026
|
+
header: unhex(tip.header),
|
|
1027
|
+
consensusHash: unhex(tip.consensus_hash)
|
|
1028
|
+
};
|
|
1029
|
+
};
|
|
1030
|
+
var toMarf = (url, j) => {
|
|
1031
|
+
if (j === null)
|
|
1032
|
+
return null;
|
|
1033
|
+
const proof = j.proof ?? j.marf_proof;
|
|
1034
|
+
if (typeof proof !== "string" || typeof j.data !== "string")
|
|
1035
|
+
throw new SourceError(url, 200, "MARF answer needs data and proof");
|
|
1036
|
+
return { data: j.data, proof: unhex(proof) };
|
|
1037
|
+
};
|
|
1038
|
+
var VM_WRITE_TYPES = [
|
|
1039
|
+
"var_set",
|
|
1040
|
+
"map_set",
|
|
1041
|
+
"map_insert",
|
|
1042
|
+
"map_delete"
|
|
1043
|
+
];
|
|
1044
|
+
var PAGE = 1000;
|
|
1045
|
+
async function pages(field, url, http) {
|
|
1046
|
+
const out = [];
|
|
1047
|
+
let cursor = null;
|
|
1048
|
+
for (;; ) {
|
|
1049
|
+
const page = cursor ? `${url}&cursor=${encodeURIComponent(cursor)}` : url;
|
|
1050
|
+
const j = await getJson(page, http);
|
|
1051
|
+
if (j === null)
|
|
1052
|
+
return cursor === null ? null : out;
|
|
1053
|
+
const rows = j[field];
|
|
1054
|
+
if (!Array.isArray(rows))
|
|
1055
|
+
throw new SourceError(page, 200, `answer has no ${field} array`);
|
|
1056
|
+
out.push(...rows);
|
|
1057
|
+
const next = j.next_cursor;
|
|
1058
|
+
if (rows.length < PAGE || typeof next !== "string" || next === cursor)
|
|
1059
|
+
return out;
|
|
1060
|
+
cursor = next;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
class SecondlayerProofSource {
|
|
1065
|
+
getBlock;
|
|
1066
|
+
getEpoch2Header;
|
|
1067
|
+
getMarfProof;
|
|
1068
|
+
getWitness;
|
|
1069
|
+
getBurnPreimage;
|
|
1070
|
+
getBitcoinHeaders;
|
|
1071
|
+
getStateWrites;
|
|
1072
|
+
getVmEvents;
|
|
1073
|
+
constructor(opts) {
|
|
1074
|
+
const base = trimSlash(opts.baseUrl);
|
|
1075
|
+
const http = {
|
|
1076
|
+
headers: { authorization: `Bearer ${opts.apiKey}` },
|
|
1077
|
+
fetch: opts.fetch
|
|
1078
|
+
};
|
|
1079
|
+
this.getBlock = async (ref) => {
|
|
1080
|
+
const url = typeof ref === "number" ? `${base}/v1/proofs/block/height/${ref}` : `${base}/v1/proofs/block/${ref}`;
|
|
1081
|
+
return required(url, await getBytes(url, http));
|
|
1082
|
+
};
|
|
1083
|
+
this.getEpoch2Header = async (blockId) => {
|
|
1084
|
+
const url = `${base}/v1/proofs/epoch2-header/${blockId}`;
|
|
1085
|
+
return toEpoch2(url, await getJson(url, http));
|
|
1086
|
+
};
|
|
1087
|
+
this.getMarfProof = async (pathHex, tipId) => {
|
|
1088
|
+
const url = `${base}/v1/proofs/marf/${pathHex}?tip=${tipId}`;
|
|
1089
|
+
return toMarf(url, await getJson(url, http));
|
|
1090
|
+
};
|
|
1091
|
+
this.getWitness = async (blockId) => {
|
|
1092
|
+
const url = `${base}/v1/proofs/witness/${blockId}`;
|
|
1093
|
+
return required(url, await getBytes(url, http));
|
|
1094
|
+
};
|
|
1095
|
+
this.getBurnPreimage = async (consensusHash) => {
|
|
1096
|
+
const url = `${base}/v1/proofs/burn/${consensusHash}`;
|
|
1097
|
+
const j = required(url, await getJson(url, http));
|
|
1098
|
+
return { preimage: unhex(j.preimage), burnHeight: j.burn_height };
|
|
1099
|
+
};
|
|
1100
|
+
this.getBitcoinHeaders = async (from, count) => {
|
|
1101
|
+
const url = `${base}/v1/proofs/bitcoin-headers?from=${from}&count=${count}`;
|
|
1102
|
+
const j = required(url, await getJson(url, http));
|
|
1103
|
+
if (j.from !== from)
|
|
1104
|
+
throw new SourceError(url, 200, `headers start at ${j.from}`);
|
|
1105
|
+
return j.headers;
|
|
1106
|
+
};
|
|
1107
|
+
this.getStateWrites = (height) => pages("state_writes", `${base}/v1/index/state-writes?block_height=${height}&limit=${PAGE}`, http);
|
|
1108
|
+
this.getVmEvents = async (height) => {
|
|
1109
|
+
const out = [];
|
|
1110
|
+
for (const type of VM_WRITE_TYPES) {
|
|
1111
|
+
const url = `${base}/v1/index/events?event_type=${type}&from_height=${height}&to_height=${height}&limit=${PAGE}`;
|
|
1112
|
+
out.push(...await pages("events", url, http) ?? []);
|
|
1113
|
+
}
|
|
1114
|
+
return out.sort((a, b) => a.event_index - b.event_index);
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
class NodeRpcProofSource {
|
|
1120
|
+
getBlock;
|
|
1121
|
+
getEpoch2Header;
|
|
1122
|
+
getMarfProof;
|
|
1123
|
+
constructor(opts) {
|
|
1124
|
+
const base = trimSlash(opts.nodeUrl);
|
|
1125
|
+
const http = { fetch: opts.fetch };
|
|
1126
|
+
this.getBlock = async (ref) => {
|
|
1127
|
+
const url = typeof ref === "number" ? `${base}/v3/blocks/height/${ref}` : `${base}/v3/blocks/${ref}`;
|
|
1128
|
+
return required(url, await getBytes(url, http));
|
|
1129
|
+
};
|
|
1130
|
+
this.getEpoch2Header = async (blockId) => {
|
|
1131
|
+
const url = `${base}/v2/headers/1?tip=${blockId}`;
|
|
1132
|
+
return toEpoch2(url, await getJson(url, http));
|
|
1133
|
+
};
|
|
1134
|
+
this.getMarfProof = async (pathHex, tipId) => {
|
|
1135
|
+
const url = `${base}/v2/clarity/marf/${pathHex}?tip=${tipId}&proof=1`;
|
|
1136
|
+
return toMarf(url, await getJson(url, http));
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// src/state.ts
|
|
1141
|
+
var INTERNAL = {
|
|
1142
|
+
self: "__MARF_BLOCK_HEIGHT_SELF",
|
|
1143
|
+
hashToHeight: "__MARF_BLOCK_HASH_TO_HEIGHT"
|
|
1144
|
+
};
|
|
1145
|
+
var heightValue = (h) => {
|
|
1146
|
+
const v = new Uint8Array(MARF_VALUE_SIZE);
|
|
1147
|
+
new DataView(v.buffer).setUint32(0, h, true);
|
|
1148
|
+
return hex(v);
|
|
1149
|
+
};
|
|
1150
|
+
var padded = (b32) => `${hex(b32)}${"00".repeat(8)}`;
|
|
1151
|
+
function internalEntries(height, parentBlockId, leaves) {
|
|
1152
|
+
const out = [[INTERNAL.self, heightValue(height)]];
|
|
1153
|
+
const ownKey = heightToHashKey(height);
|
|
1154
|
+
const ownPath = marfPath(ownKey);
|
|
1155
|
+
const own = leaves.find((l) => bytesEqual(l.path, ownPath));
|
|
1156
|
+
if (own) {
|
|
1157
|
+
out.push([ownKey, padded(own.valueHash)]);
|
|
1158
|
+
out.push([
|
|
1159
|
+
`${INTERNAL.hashToHeight}::${hex(own.valueHash)}`,
|
|
1160
|
+
heightValue(height)
|
|
1161
|
+
]);
|
|
1162
|
+
}
|
|
1163
|
+
if (height > 0) {
|
|
1164
|
+
out.push([heightToHashKey(height - 1), padded(parentBlockId)]);
|
|
1165
|
+
out.push([
|
|
1166
|
+
`${INTERNAL.hashToHeight}::${hex(parentBlockId)}`,
|
|
1167
|
+
heightValue(height - 1)
|
|
1168
|
+
]);
|
|
1169
|
+
}
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1172
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
1173
|
+
var strip0x = (s) => s.startsWith("0x") ? s.slice(2) : s;
|
|
1174
|
+
var leafHex = (l) => ({
|
|
1175
|
+
path: hex(l.path),
|
|
1176
|
+
valueHash: hex(l.valueHash)
|
|
1177
|
+
});
|
|
1178
|
+
function lastWrites(writes, failures) {
|
|
1179
|
+
const out = new Map;
|
|
1180
|
+
for (const w of [...writes].sort((a, b) => a.ordinal - b.ordinal)) {
|
|
1181
|
+
try {
|
|
1182
|
+
out.set(w.key, {
|
|
1183
|
+
value: utf8.decode(unhex(strip0x(w.value_hex))),
|
|
1184
|
+
txIndex: w.tx_index
|
|
1185
|
+
});
|
|
1186
|
+
} catch {
|
|
1187
|
+
failures.push({
|
|
1188
|
+
step: "names",
|
|
1189
|
+
code: "bad-write",
|
|
1190
|
+
message: `state write ${w.ordinal} value is not UTF-8 hex`,
|
|
1191
|
+
key: w.key,
|
|
1192
|
+
ordinal: w.ordinal
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return out;
|
|
1197
|
+
}
|
|
1198
|
+
function rowWrite(row) {
|
|
1199
|
+
const need = (v, name) => {
|
|
1200
|
+
if (!v)
|
|
1201
|
+
throw new Error(`${row.event_type} row has no ${name}`);
|
|
1202
|
+
return v;
|
|
1203
|
+
};
|
|
1204
|
+
if (row.event_type === "var_set")
|
|
1205
|
+
return [
|
|
1206
|
+
dataVarKey(row.contract_id, need(row.var_name, "var_name")),
|
|
1207
|
+
strip0x(need(row.raw_value, "raw_value")).toLowerCase()
|
|
1208
|
+
];
|
|
1209
|
+
const key = `vm::${row.contract_id}::0::${need(row.map, "map")}::${strip0x(need(row.raw_key, "raw_key")).toLowerCase()}`;
|
|
1210
|
+
if (row.event_type === "map_delete")
|
|
1211
|
+
return [key, "09"];
|
|
1212
|
+
return [key, `0a${strip0x(need(row.raw_value, "raw_value")).toLowerCase()}`];
|
|
1213
|
+
}
|
|
1214
|
+
function checkRows(rows, leaves, failures) {
|
|
1215
|
+
const byPath = new Map(leaves.map((l) => [hex(l.path), l]));
|
|
1216
|
+
const named = [];
|
|
1217
|
+
for (const row of [...rows].sort((a, b) => a.event_index - b.event_index)) {
|
|
1218
|
+
try {
|
|
1219
|
+
const [key, value] = rowWrite(row);
|
|
1220
|
+
named.push({ row, key, path: hex(marfPath(key)), value });
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
failures.push({
|
|
1223
|
+
step: "rows",
|
|
1224
|
+
code: "bad-row",
|
|
1225
|
+
message: err.message,
|
|
1226
|
+
ordinal: row.event_index
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
const last = new Map(named.map((n, i) => [n.path, i]));
|
|
1231
|
+
named.forEach((n, i) => {
|
|
1232
|
+
const leaf = byPath.get(n.path);
|
|
1233
|
+
const at = { key: n.key, path: n.path, ordinal: n.row.event_index };
|
|
1234
|
+
if (!leaf)
|
|
1235
|
+
failures.push({
|
|
1236
|
+
step: "rows",
|
|
1237
|
+
code: "key-not-written",
|
|
1238
|
+
message: `${n.row.event_type} row ${n.row.event_index} names a key the block did not write`,
|
|
1239
|
+
...at
|
|
1240
|
+
});
|
|
1241
|
+
else if (last.get(n.path) === i && padded(leaf.valueHash) !== hex(marfValue(n.value)))
|
|
1242
|
+
failures.push({
|
|
1243
|
+
step: "rows",
|
|
1244
|
+
code: "value-mismatch",
|
|
1245
|
+
message: `${n.row.event_type} row ${n.row.event_index} value differs from the block's leaf`,
|
|
1246
|
+
...at
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
return named.length;
|
|
1250
|
+
}
|
|
1251
|
+
async function verifyBlockState(input) {
|
|
1252
|
+
const out = { rowsChecked: 0, notes: [], failures: [] };
|
|
1253
|
+
let witness;
|
|
1254
|
+
try {
|
|
1255
|
+
witness = parseWitness(input.witness);
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
out.failures.push({
|
|
1258
|
+
step: "witness",
|
|
1259
|
+
code: "malformed",
|
|
1260
|
+
message: err.message
|
|
1261
|
+
});
|
|
1262
|
+
return out;
|
|
1263
|
+
}
|
|
1264
|
+
if (!bytesEqual(witness.root, input.stateRoot)) {
|
|
1265
|
+
out.failures.push({
|
|
1266
|
+
step: "witness",
|
|
1267
|
+
code: "root-mismatch",
|
|
1268
|
+
message: `witness root ${hex(witness.root)} != header state_index_root ${hex(input.stateRoot)}`
|
|
1269
|
+
});
|
|
1270
|
+
return out;
|
|
1271
|
+
}
|
|
1272
|
+
const { leaves } = witness;
|
|
1273
|
+
const internal = internalEntries(input.height, input.parentBlockId, leaves);
|
|
1274
|
+
const named = input.writes !== null;
|
|
1275
|
+
const last = lastWrites(input.writes ?? [], out.failures);
|
|
1276
|
+
const writes = [...last].map(([k, w]) => [k, w.value]);
|
|
1277
|
+
const first = classifyBlockDiff({
|
|
1278
|
+
leaves,
|
|
1279
|
+
writes,
|
|
1280
|
+
internal,
|
|
1281
|
+
parentValue: () => {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
const inParent = new Map((input.parentLeaves ?? []).map((l) => [hex(l.path), hex(l.valueHash)]));
|
|
1286
|
+
const carried = new Map;
|
|
1287
|
+
for (const leaf of first.hidden) {
|
|
1288
|
+
const pathHex = hex(leaf.path);
|
|
1289
|
+
if (inParent.get(pathHex) === hex(leaf.valueHash) || named && await input.parentHolds?.(leaf))
|
|
1290
|
+
carried.set(pathHex, padded(leaf.valueHash));
|
|
1291
|
+
}
|
|
1292
|
+
const d = classifyBlockDiff({
|
|
1293
|
+
leaves,
|
|
1294
|
+
writes,
|
|
1295
|
+
internal,
|
|
1296
|
+
parentValue: (p) => carried.get(p)
|
|
1297
|
+
});
|
|
1298
|
+
out.diff = {
|
|
1299
|
+
named,
|
|
1300
|
+
writes: named ? d.writes.map((l) => ({
|
|
1301
|
+
...leafHex(l),
|
|
1302
|
+
key: l.key,
|
|
1303
|
+
txIndex: last.get(l.key)?.txIndex ?? null,
|
|
1304
|
+
named: true
|
|
1305
|
+
})) : d.hidden.map((l) => ({ ...leafHex(l), named: false })),
|
|
1306
|
+
carried: d.carried.map(leafHex),
|
|
1307
|
+
internal: d.internal.map((l) => ({ ...leafHex(l), key: l.key }))
|
|
1308
|
+
};
|
|
1309
|
+
if (!named)
|
|
1310
|
+
out.notes.push(`source has no state_writes for block ${input.height}: ${d.hidden.length} written leaves are proven in the block but unnamed`);
|
|
1311
|
+
else {
|
|
1312
|
+
for (const l of d.hidden)
|
|
1313
|
+
out.failures.push({
|
|
1314
|
+
step: "names",
|
|
1315
|
+
code: "hidden-write",
|
|
1316
|
+
message: `leaf ${hex(l.path)} is not a named write, a carried parent value or MARF bookkeeping`,
|
|
1317
|
+
path: hex(l.path)
|
|
1318
|
+
});
|
|
1319
|
+
for (const key of d.missing)
|
|
1320
|
+
out.failures.push({
|
|
1321
|
+
step: "names",
|
|
1322
|
+
code: "missing-write",
|
|
1323
|
+
message: `state write ${key} has no leaf holding its value`,
|
|
1324
|
+
key,
|
|
1325
|
+
path: hex(marfPath(key))
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
if (input.rows)
|
|
1329
|
+
out.rowsChecked = checkRows(input.rows, leaves, out.failures);
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
// src/verify-block.ts
|
|
1333
|
+
import { uintCV } from "@secondlayer/stacks/clarity";
|
|
1334
|
+
class Broken extends Error {
|
|
1335
|
+
failure;
|
|
1336
|
+
constructor(failure) {
|
|
1337
|
+
super(failure.message);
|
|
1338
|
+
this.failure = failure;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
var fail = (f) => {
|
|
1342
|
+
throw new Broken(f);
|
|
1343
|
+
};
|
|
1344
|
+
async function fetchFor(step, what, call, extra = {}) {
|
|
1345
|
+
try {
|
|
1346
|
+
return await call();
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
if (err instanceof Broken)
|
|
1349
|
+
throw err;
|
|
1350
|
+
return fail({
|
|
1351
|
+
step,
|
|
1352
|
+
code: "unavailable",
|
|
1353
|
+
message: `${what}: ${err.message}`,
|
|
1354
|
+
...extra
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
var BITCOIN_BATCH = 2016;
|
|
1359
|
+
var MAX_WALK = 16;
|
|
1360
|
+
async function findAnchor(low, high, window, probe, maxProbes = 32) {
|
|
1361
|
+
let [lo, hi] = [low, high];
|
|
1362
|
+
const target = (window[0] + window[1]) / 2;
|
|
1363
|
+
let bisect = false;
|
|
1364
|
+
for (let i = 0;i < maxProbes; i++) {
|
|
1365
|
+
const span = hi.height - lo.height;
|
|
1366
|
+
if (span <= 1)
|
|
1367
|
+
return null;
|
|
1368
|
+
let h = Math.floor((lo.height + hi.height) / 2);
|
|
1369
|
+
if (!bisect && lo.burn !== undefined && hi.burn !== undefined && hi.burn > lo.burn)
|
|
1370
|
+
h = lo.height + Math.round((target - lo.burn) * span / (hi.burn - lo.burn));
|
|
1371
|
+
h = Math.min(Math.max(h, lo.height + 1), hi.height - 1);
|
|
1372
|
+
const p = await probe(h);
|
|
1373
|
+
if (p.height <= lo.height || p.height >= hi.height)
|
|
1374
|
+
return null;
|
|
1375
|
+
if (p.burn >= window[0] && p.burn < window[1])
|
|
1376
|
+
return p;
|
|
1377
|
+
if (p.burn < window[0])
|
|
1378
|
+
lo = p;
|
|
1379
|
+
else
|
|
1380
|
+
hi = p;
|
|
1381
|
+
bisect = hi.height - lo.height > span / 2;
|
|
1382
|
+
}
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
class BlockVerifier {
|
|
1387
|
+
source;
|
|
1388
|
+
checkpoint;
|
|
1389
|
+
rows;
|
|
1390
|
+
chain;
|
|
1391
|
+
sets = new Map;
|
|
1392
|
+
anchors = new Map;
|
|
1393
|
+
headers = new Map;
|
|
1394
|
+
below = new Map;
|
|
1395
|
+
burns = new Map;
|
|
1396
|
+
preimages = new Map;
|
|
1397
|
+
start;
|
|
1398
|
+
constructor(opts) {
|
|
1399
|
+
this.source = opts.source;
|
|
1400
|
+
this.checkpoint = opts.checkpoint ?? MAINNET_CHECKPOINT;
|
|
1401
|
+
this.rows = opts.rows ?? false;
|
|
1402
|
+
const { stacks } = this.checkpoint;
|
|
1403
|
+
const header = parseNakamotoHeader(unhex(stacks.header));
|
|
1404
|
+
this.start = { header, id: hex(blockId(header)) };
|
|
1405
|
+
this.headers.set(this.start.id, header);
|
|
1406
|
+
this.sets.set(stacks.cycle, decodeSignerSet(stacks.signerSet));
|
|
1407
|
+
this.chain = HeaderChain.fromCheckpoint(this.checkpoint.bitcoin);
|
|
1408
|
+
}
|
|
1409
|
+
async verify(ref) {
|
|
1410
|
+
const out = { ok: false, notes: [], failures: [] };
|
|
1411
|
+
try {
|
|
1412
|
+
await this.run(ref, out);
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
if (!(err instanceof Broken))
|
|
1415
|
+
throw err;
|
|
1416
|
+
out.failures.unshift(err.failure);
|
|
1417
|
+
}
|
|
1418
|
+
out.ok = out.failures.length === 0;
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
async run(ref, out) {
|
|
1422
|
+
const start = Number(this.start.header.chainLength);
|
|
1423
|
+
if (typeof ref === "number" && ref < start)
|
|
1424
|
+
return this.runBelow(ref, undefined, out);
|
|
1425
|
+
let header;
|
|
1426
|
+
if (typeof ref === "string") {
|
|
1427
|
+
const id = ref.replace(/^0x/, "").toLowerCase();
|
|
1428
|
+
header = await this.header(id, "block");
|
|
1429
|
+
const height = Number(header.chainLength);
|
|
1430
|
+
if (height < start)
|
|
1431
|
+
return this.runBelow(height, id, out);
|
|
1432
|
+
if (isEpoch2Header(header))
|
|
1433
|
+
fail({
|
|
1434
|
+
step: "block",
|
|
1435
|
+
code: "invalid-header",
|
|
1436
|
+
message: `epoch 2.x block ${id} claims height ${height}, at or above the checkpoint`
|
|
1437
|
+
});
|
|
1438
|
+
} else {
|
|
1439
|
+
const raw = await fetchFor("block", `block ${ref}`, () => this.source.getBlock(ref));
|
|
1440
|
+
header = parseHeader2(raw, "block");
|
|
1441
|
+
}
|
|
1442
|
+
const id = hex(blockId(header));
|
|
1443
|
+
const height = Number(header.chainLength);
|
|
1444
|
+
if (typeof ref === "number" && height !== ref)
|
|
1445
|
+
fail({
|
|
1446
|
+
step: "block",
|
|
1447
|
+
code: "height-mismatch",
|
|
1448
|
+
message: `source returned height ${height} for ${ref}`
|
|
1449
|
+
});
|
|
1450
|
+
await this.runForward(header, id, out);
|
|
1451
|
+
}
|
|
1452
|
+
async runForward(header, id, out) {
|
|
1453
|
+
const height = Number(header.chainLength);
|
|
1454
|
+
out.height = height;
|
|
1455
|
+
out.blockId = id;
|
|
1456
|
+
out.stateRoot = hex(header.stateIndexRoot);
|
|
1457
|
+
const burn = await this.bindBurn(header, "burn");
|
|
1458
|
+
const cycle = rewardCycle(burn.burnHeight);
|
|
1459
|
+
out.burnHeight = burn.burnHeight;
|
|
1460
|
+
out.bitcoinBlockHash = burn.hash;
|
|
1461
|
+
out.cycle = cycle;
|
|
1462
|
+
const set = await this.signerSet(cycle, {
|
|
1463
|
+
height,
|
|
1464
|
+
burn: burn.burnHeight
|
|
1465
|
+
});
|
|
1466
|
+
const sig = verifySignerSignatures(header, set);
|
|
1467
|
+
out.signerWeight = sig.signedWeight;
|
|
1468
|
+
out.totalWeight = sig.totalWeight;
|
|
1469
|
+
out.threshold = sig.threshold;
|
|
1470
|
+
if (!sig.valid)
|
|
1471
|
+
fail(signatureFailure(sig, "signatures", cycle));
|
|
1472
|
+
await this.verifyState(header, id, out);
|
|
1473
|
+
}
|
|
1474
|
+
async runBelow(height, claimed, out) {
|
|
1475
|
+
out.height = height;
|
|
1476
|
+
const { id, ancestry } = await this.ancestor(height);
|
|
1477
|
+
out.ancestry = ancestry;
|
|
1478
|
+
if (claimed !== undefined && claimed !== id)
|
|
1479
|
+
fail({
|
|
1480
|
+
step: "ancestry",
|
|
1481
|
+
code: "not-ancestor",
|
|
1482
|
+
message: `block ${claimed} is not on the checkpoint's chain: its block at height ${height} is ${id}`,
|
|
1483
|
+
blockId: claimed
|
|
1484
|
+
});
|
|
1485
|
+
const header = await this.header(id, "block");
|
|
1486
|
+
if (Number(header.chainLength) !== height)
|
|
1487
|
+
fail({
|
|
1488
|
+
step: "block",
|
|
1489
|
+
code: "height-mismatch",
|
|
1490
|
+
message: `block ${id} claims height ${header.chainLength}, proven at ${height}`
|
|
1491
|
+
});
|
|
1492
|
+
out.blockId = id;
|
|
1493
|
+
out.stateRoot = hex(header.stateIndexRoot);
|
|
1494
|
+
await this.bindBurnBelow(header, out);
|
|
1495
|
+
await this.verifyState(header, id, out);
|
|
1496
|
+
}
|
|
1497
|
+
async ancestor(height) {
|
|
1498
|
+
const known = this.below.get(height);
|
|
1499
|
+
if (known)
|
|
1500
|
+
return known;
|
|
1501
|
+
const from = this.nearestAbove(height);
|
|
1502
|
+
if (from.height - height <= MAX_WALK) {
|
|
1503
|
+
const walked = await this.walk(from, height);
|
|
1504
|
+
if (walked)
|
|
1505
|
+
return walked;
|
|
1506
|
+
}
|
|
1507
|
+
return this.jump(this.nearestAbove(height), height);
|
|
1508
|
+
}
|
|
1509
|
+
nearestAbove(height) {
|
|
1510
|
+
let best = {
|
|
1511
|
+
height: Number(this.start.header.chainLength),
|
|
1512
|
+
id: this.start.id
|
|
1513
|
+
};
|
|
1514
|
+
for (const [h, { id }] of this.below)
|
|
1515
|
+
if (h > height && h < best.height)
|
|
1516
|
+
best = { height: h, id };
|
|
1517
|
+
return best;
|
|
1518
|
+
}
|
|
1519
|
+
async walk(from, height) {
|
|
1520
|
+
const ancestry = {
|
|
1521
|
+
fromHeight: from.height,
|
|
1522
|
+
fromBlockId: from.id,
|
|
1523
|
+
via: "parents"
|
|
1524
|
+
};
|
|
1525
|
+
let child = await this.header(from.id, "ancestry");
|
|
1526
|
+
for (let h = from.height - 1;h >= height; h--) {
|
|
1527
|
+
if (isEpoch2Header(child))
|
|
1528
|
+
return null;
|
|
1529
|
+
const id = hex(child.parentBlockId);
|
|
1530
|
+
const parent = await this.header(id, "ancestry", { blockId: id });
|
|
1531
|
+
if (Number(parent.chainLength) !== h)
|
|
1532
|
+
fail({
|
|
1533
|
+
step: "ancestry",
|
|
1534
|
+
code: "height-mismatch",
|
|
1535
|
+
message: `parent ${id} claims height ${parent.chainLength}, expected ${h}`,
|
|
1536
|
+
blockId: id
|
|
1537
|
+
});
|
|
1538
|
+
this.below.set(h, { id, ancestry });
|
|
1539
|
+
child = parent;
|
|
1540
|
+
}
|
|
1541
|
+
return this.below.get(height) ?? null;
|
|
1542
|
+
}
|
|
1543
|
+
async jump(from, height) {
|
|
1544
|
+
const at = { blockId: from.id };
|
|
1545
|
+
const tip = await this.header(from.id, "ancestry", at);
|
|
1546
|
+
const key = heightToHashKey(height);
|
|
1547
|
+
const path = marfPath(key);
|
|
1548
|
+
const answer = await fetchFor("ancestry", `${key} at ${from.id}`, () => this.source.getMarfProof(hex(path), from.id), at);
|
|
1549
|
+
if (!answer)
|
|
1550
|
+
return fail({
|
|
1551
|
+
step: "ancestry",
|
|
1552
|
+
code: "unavailable",
|
|
1553
|
+
message: `source has no MARF proof of ${key} at block ${from.height}, ${from.height - height} blocks up; parent links reach ${MAX_WALK}`,
|
|
1554
|
+
...at
|
|
1555
|
+
});
|
|
1556
|
+
const value = marfProofValue(answer.proof) ?? new Uint8Array;
|
|
1557
|
+
const proven = value.length === MARF_VALUE_SIZE && value.subarray(32).every((b) => b === 0) && await this.proveAt(tip, path, value, answer.proof, "ancestry", at);
|
|
1558
|
+
if (!proven)
|
|
1559
|
+
return fail({
|
|
1560
|
+
step: "ancestry",
|
|
1561
|
+
code: "ancestry-proof-invalid",
|
|
1562
|
+
message: `${key} is not proven in block ${from.height}'s state`,
|
|
1563
|
+
...at
|
|
1564
|
+
});
|
|
1565
|
+
const ancestry = {
|
|
1566
|
+
fromHeight: from.height,
|
|
1567
|
+
fromBlockId: from.id,
|
|
1568
|
+
via: "marf"
|
|
1569
|
+
};
|
|
1570
|
+
const found = { id: hex(value.subarray(0, 32)), ancestry };
|
|
1571
|
+
this.below.set(height, found);
|
|
1572
|
+
return found;
|
|
1573
|
+
}
|
|
1574
|
+
async bindBurnBelow(header, out) {
|
|
1575
|
+
let hint;
|
|
1576
|
+
try {
|
|
1577
|
+
hint = await this.burnHint(header);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
out.notes.push(`burn block not checked: ${err.message}`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (hint < this.chain.checkpointHeight) {
|
|
1583
|
+
out.notes.push(`burn block not checked: its height ${hint} is below the Bitcoin checkpoint ${this.chain.checkpointHeight}`);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const burn = await this.bindBurn(header, "burn");
|
|
1587
|
+
out.burnHeight = burn.burnHeight;
|
|
1588
|
+
out.bitcoinBlockHash = burn.hash;
|
|
1589
|
+
}
|
|
1590
|
+
async verifyState(header, id, out) {
|
|
1591
|
+
const src = this.source;
|
|
1592
|
+
const height = Number(header.chainLength);
|
|
1593
|
+
const witness = await fetchFor("witness", `witness ${id}`, () => src.getWitness(id));
|
|
1594
|
+
const { getStateWrites, getVmEvents } = this.rows ? src : {};
|
|
1595
|
+
const writes = getStateWrites ? await fetchFor("names", `state_writes ${height}`, () => getStateWrites.call(src, height)) : null;
|
|
1596
|
+
const rows = getVmEvents ? await fetchFor("rows", `vm_events ${height}`, () => getVmEvents.call(src, height)) : undefined;
|
|
1597
|
+
const parent = isEpoch2Header(header) ? epoch2ParentId(witness, header) : header.parentBlockId;
|
|
1598
|
+
const parentId = parent && hex(parent);
|
|
1599
|
+
const state = await verifyBlockState({
|
|
1600
|
+
height,
|
|
1601
|
+
parentBlockId: parent ?? new Uint8Array(32),
|
|
1602
|
+
stateRoot: header.stateIndexRoot,
|
|
1603
|
+
witness,
|
|
1604
|
+
writes,
|
|
1605
|
+
rows,
|
|
1606
|
+
parentLeaves: parentId ? await this.parentLeaves(parentId, out.notes) : undefined,
|
|
1607
|
+
parentHolds: async (leaf) => parentId ? this.parentHolds(parentId, leaf) : false
|
|
1608
|
+
});
|
|
1609
|
+
out.diff = state.diff;
|
|
1610
|
+
if (rows)
|
|
1611
|
+
out.rowsChecked = state.rowsChecked;
|
|
1612
|
+
out.notes.push(...state.notes);
|
|
1613
|
+
out.failures.push(...state.failures);
|
|
1614
|
+
}
|
|
1615
|
+
async header(id, step, extra = {}) {
|
|
1616
|
+
const known = this.headers.get(id);
|
|
1617
|
+
if (known)
|
|
1618
|
+
return known;
|
|
1619
|
+
const header = await this.fetchHeader(id, step, extra);
|
|
1620
|
+
if (hex(blockId(header)) !== id)
|
|
1621
|
+
fail({
|
|
1622
|
+
step,
|
|
1623
|
+
code: "id-mismatch",
|
|
1624
|
+
message: `source returned another block for ${id}`,
|
|
1625
|
+
blockId: id,
|
|
1626
|
+
...extra
|
|
1627
|
+
});
|
|
1628
|
+
this.headers.set(id, header);
|
|
1629
|
+
return header;
|
|
1630
|
+
}
|
|
1631
|
+
async fetchHeader(id, step, extra) {
|
|
1632
|
+
const src = this.source;
|
|
1633
|
+
let raw;
|
|
1634
|
+
try {
|
|
1635
|
+
raw = await src.getBlock(id);
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
const reason = `block ${id}: ${err.message}`;
|
|
1638
|
+
const { getEpoch2Header } = src;
|
|
1639
|
+
if (!getEpoch2Header)
|
|
1640
|
+
return fail({ step, code: "unavailable", message: reason, ...extra });
|
|
1641
|
+
const e2 = await fetchFor(step, `${reason}; epoch 2.x header`, () => getEpoch2Header.call(src, id), extra);
|
|
1642
|
+
try {
|
|
1643
|
+
return parseEpoch2Header(e2.header, e2.consensusHash);
|
|
1644
|
+
} catch (parseErr) {
|
|
1645
|
+
return fail({
|
|
1646
|
+
step,
|
|
1647
|
+
code: "invalid-header",
|
|
1648
|
+
message: `epoch 2.x header ${id} does not parse: ${parseErr.message}`,
|
|
1649
|
+
...extra
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return parseHeader2(raw, step);
|
|
1654
|
+
}
|
|
1655
|
+
async proveAt(tip, path, value, proof, step, extra = {}) {
|
|
1656
|
+
const headers = [tip];
|
|
1657
|
+
for (const id of marfProofAncestors(proof))
|
|
1658
|
+
headers.push(await this.header(id, step, extra));
|
|
1659
|
+
return verifyMarfProof({
|
|
1660
|
+
proof,
|
|
1661
|
+
path,
|
|
1662
|
+
value,
|
|
1663
|
+
root: tip.stateIndexRoot,
|
|
1664
|
+
headers
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
async parentLeaves(parentId, notes) {
|
|
1668
|
+
try {
|
|
1669
|
+
const parent = await this.header(parentId, "names");
|
|
1670
|
+
const w = parseWitness(await this.source.getWitness(parentId));
|
|
1671
|
+
if (bytesEqual(w.root, parent.stateIndexRoot))
|
|
1672
|
+
return w.leaves;
|
|
1673
|
+
notes.push(`parent witness ${parentId} root does not match its header`);
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
notes.push(`parent witness ${parentId} unavailable: ${err.message}`);
|
|
1676
|
+
}
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
async parentHolds(parentId, leaf) {
|
|
1680
|
+
const parent = await this.header(parentId, "names");
|
|
1681
|
+
const pathHex = hex(leaf.path);
|
|
1682
|
+
const answer = await fetchFor("names", `MARF ${pathHex} at parent`, () => this.source.getMarfProof(pathHex, parentId));
|
|
1683
|
+
if (!answer)
|
|
1684
|
+
return false;
|
|
1685
|
+
const value = new Uint8Array(MARF_VALUE_SIZE);
|
|
1686
|
+
value.set(leaf.valueHash);
|
|
1687
|
+
return this.proveAt(parent, leaf.path, value, answer.proof, "names");
|
|
1688
|
+
}
|
|
1689
|
+
async bindBurn(header, step, extra = {}) {
|
|
1690
|
+
const ch = hex(header.consensusHash);
|
|
1691
|
+
const known = this.burns.get(ch);
|
|
1692
|
+
if (known)
|
|
1693
|
+
return known;
|
|
1694
|
+
const bp = await fetchFor(step, `burn preimage ${ch}`, () => this.preimage(ch), extra);
|
|
1695
|
+
const hash = verifyConsensusPreimage(header.consensusHash, bp.preimage);
|
|
1696
|
+
if (!hash)
|
|
1697
|
+
return fail({
|
|
1698
|
+
step,
|
|
1699
|
+
code: "preimage-mismatch",
|
|
1700
|
+
message: `preimage does not hash to consensus hash ${ch}`,
|
|
1701
|
+
...extra
|
|
1702
|
+
});
|
|
1703
|
+
if (bp.burnHeight < this.chain.checkpointHeight)
|
|
1704
|
+
fail({
|
|
1705
|
+
step,
|
|
1706
|
+
code: "before-checkpoint",
|
|
1707
|
+
message: `burn height ${bp.burnHeight} is below the Bitcoin checkpoint ${this.chain.checkpointHeight}`,
|
|
1708
|
+
...extra
|
|
1709
|
+
});
|
|
1710
|
+
await this.syncBitcoin(bp.burnHeight);
|
|
1711
|
+
const burnHeight = this.chain.heightOf(hash);
|
|
1712
|
+
if (burnHeight === undefined)
|
|
1713
|
+
return fail({
|
|
1714
|
+
step,
|
|
1715
|
+
code: "burn-not-in-chain",
|
|
1716
|
+
message: `burn block ${hash} is not in the verified Bitcoin chain (synced ${this.chain.checkpointHeight}..${this.chain.tip.height})`,
|
|
1717
|
+
...extra
|
|
1718
|
+
});
|
|
1719
|
+
const bound = { burnHeight, hash };
|
|
1720
|
+
this.burns.set(ch, bound);
|
|
1721
|
+
return bound;
|
|
1722
|
+
}
|
|
1723
|
+
async syncBitcoin(height) {
|
|
1724
|
+
while (this.chain.tip.height < height) {
|
|
1725
|
+
const from = this.chain.tip.height + 1;
|
|
1726
|
+
const count = Math.min(BITCOIN_BATCH, height - from + 1);
|
|
1727
|
+
const headers = await fetchFor("bitcoin", `bitcoin headers ${from}`, () => this.source.getBitcoinHeaders(from, count));
|
|
1728
|
+
if (headers.length === 0)
|
|
1729
|
+
fail({
|
|
1730
|
+
step: "bitcoin",
|
|
1731
|
+
code: "unavailable",
|
|
1732
|
+
message: `source has no Bitcoin headers from ${from}`
|
|
1733
|
+
});
|
|
1734
|
+
try {
|
|
1735
|
+
this.chain.append(headers);
|
|
1736
|
+
} catch (err) {
|
|
1737
|
+
if (!(err instanceof HeaderValidationError))
|
|
1738
|
+
throw err;
|
|
1739
|
+
fail({ step: "bitcoin", code: "invalid-header", message: err.message });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async signerSet(cycle, target) {
|
|
1744
|
+
const known = this.sets.get(cycle);
|
|
1745
|
+
if (known)
|
|
1746
|
+
return known;
|
|
1747
|
+
const first = this.checkpoint.stacks.cycle;
|
|
1748
|
+
if (cycle < first)
|
|
1749
|
+
fail({
|
|
1750
|
+
step: "signer-set",
|
|
1751
|
+
code: "before-checkpoint",
|
|
1752
|
+
message: `cycle ${cycle} precedes the checkpoint's cycle ${first}`,
|
|
1753
|
+
cycle
|
|
1754
|
+
});
|
|
1755
|
+
let lo = await this.startPoint();
|
|
1756
|
+
for (let c = first;c < cycle; c++) {
|
|
1757
|
+
const next = this.sets.get(c + 1);
|
|
1758
|
+
if (next) {
|
|
1759
|
+
lo = this.anchors.get(c + 1) ?? lo;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const anchor = await this.locateAnchor(c + 1, lo, target);
|
|
1763
|
+
this.sets.set(c + 1, await this.proveNextSet(c + 1, anchor));
|
|
1764
|
+
this.anchors.set(c + 1, anchor);
|
|
1765
|
+
lo = anchor;
|
|
1766
|
+
}
|
|
1767
|
+
return this.sets.get(cycle);
|
|
1768
|
+
}
|
|
1769
|
+
async startPoint() {
|
|
1770
|
+
const { header } = this.start;
|
|
1771
|
+
const height = Number(header.chainLength);
|
|
1772
|
+
try {
|
|
1773
|
+
return { height, burn: await this.burnHint(header) };
|
|
1774
|
+
} catch {
|
|
1775
|
+
return { height };
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async burnHint(header) {
|
|
1779
|
+
const ch = hex(header.consensusHash);
|
|
1780
|
+
return this.burns.get(ch)?.burnHeight ?? (await this.preimage(ch)).burnHeight;
|
|
1781
|
+
}
|
|
1782
|
+
async preimage(ch) {
|
|
1783
|
+
let bp = this.preimages.get(ch);
|
|
1784
|
+
if (!bp) {
|
|
1785
|
+
bp = await this.source.getBurnPreimage(ch);
|
|
1786
|
+
this.preimages.set(ch, bp);
|
|
1787
|
+
}
|
|
1788
|
+
return bp;
|
|
1789
|
+
}
|
|
1790
|
+
async locateAnchor(cycle, lo, hi) {
|
|
1791
|
+
const start = cycleStart(cycle);
|
|
1792
|
+
const window = [start - MAINNET_PREPARE_LENGTH, start];
|
|
1793
|
+
const probe = async (h) => {
|
|
1794
|
+
const raw = await fetchFor("signer-set", `block at height ${h}`, () => this.source.getBlock(h), { cycle });
|
|
1795
|
+
const header = parseHeader2(raw, "signer-set");
|
|
1796
|
+
const id = hex(blockId(header));
|
|
1797
|
+
this.headers.set(id, header);
|
|
1798
|
+
const burn = await fetchFor("signer-set", `burn hint for block ${id}`, () => this.burnHint(header), { cycle, blockId: id });
|
|
1799
|
+
return { height: Number(header.chainLength), burn, header, id };
|
|
1800
|
+
};
|
|
1801
|
+
const anchor = await findAnchor(lo, hi, window, probe);
|
|
1802
|
+
if (!anchor)
|
|
1803
|
+
return fail({
|
|
1804
|
+
step: "signer-set",
|
|
1805
|
+
code: "anchor-not-found",
|
|
1806
|
+
message: `no block with a burn height in ${window[0]}..${window[1] - 1} (cycle ${cycle} prepare phase) between heights ${lo.height} and ${hi.height}`,
|
|
1807
|
+
cycle
|
|
1808
|
+
});
|
|
1809
|
+
return anchor;
|
|
1810
|
+
}
|
|
1811
|
+
async proveNextSet(cycle, anchor) {
|
|
1812
|
+
const at = { cycle, blockId: anchor.id };
|
|
1813
|
+
const { burnHeight } = await this.bindBurn(anchor.header, "signer-set", at);
|
|
1814
|
+
const start = cycleStart(cycle);
|
|
1815
|
+
if (burnHeight < start - MAINNET_PREPARE_LENGTH || burnHeight >= start)
|
|
1816
|
+
fail({
|
|
1817
|
+
step: "signer-set",
|
|
1818
|
+
code: "anchor-outside-window",
|
|
1819
|
+
message: `anchor burn height ${burnHeight} is outside cycle ${cycle}'s prepare phase ${start - MAINNET_PREPARE_LENGTH}..${start - 1}`,
|
|
1820
|
+
...at
|
|
1821
|
+
});
|
|
1822
|
+
const signing = rewardCycle(burnHeight);
|
|
1823
|
+
const sig = verifySignerSignatures(anchor.header, this.sets.get(signing));
|
|
1824
|
+
if (!sig.valid)
|
|
1825
|
+
fail({
|
|
1826
|
+
...signatureFailure(sig, "signer-set", signing),
|
|
1827
|
+
...at,
|
|
1828
|
+
code: "anchor-unsigned"
|
|
1829
|
+
});
|
|
1830
|
+
const path = marfPath(mapEntryKey(SIGNERS_CONTRACT, "cycle-signer-set", uintCV(cycle)));
|
|
1831
|
+
const answer = await fetchFor("signer-set", `signer set ${cycle} at ${anchor.id}`, () => this.source.getMarfProof(hex(path), anchor.id), at);
|
|
1832
|
+
const value = answer?.data.replace(/^0x/, "") ?? "";
|
|
1833
|
+
const proven = answer !== null && await this.proveAt(anchor.header, path, marfValue(value), answer.proof, "signer-set", at);
|
|
1834
|
+
if (!proven)
|
|
1835
|
+
fail({
|
|
1836
|
+
step: "signer-set",
|
|
1837
|
+
code: "set-proof-invalid",
|
|
1838
|
+
message: `signer set ${cycle} is not proven in anchor ${anchor.id}'s state`,
|
|
1839
|
+
...at
|
|
1840
|
+
});
|
|
1841
|
+
let set;
|
|
1842
|
+
try {
|
|
1843
|
+
set = decodeSignerSet(value);
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
return fail({
|
|
1846
|
+
step: "signer-set",
|
|
1847
|
+
code: "set-proof-invalid",
|
|
1848
|
+
message: `signer set ${cycle}: ${err.message}`,
|
|
1849
|
+
...at
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
return set;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function parseHeader2(raw, step) {
|
|
1856
|
+
try {
|
|
1857
|
+
return parseNakamotoHeader(raw);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
return fail({
|
|
1860
|
+
step,
|
|
1861
|
+
code: "invalid-header",
|
|
1862
|
+
message: `block bytes do not parse: ${err.message}`
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
function epoch2ParentId(witness, header) {
|
|
1867
|
+
const height = Number(header.chainLength);
|
|
1868
|
+
if (height === 0)
|
|
1869
|
+
return null;
|
|
1870
|
+
try {
|
|
1871
|
+
const w = parseWitness(witness);
|
|
1872
|
+
if (!bytesEqual(w.root, header.stateIndexRoot))
|
|
1873
|
+
return null;
|
|
1874
|
+
const path = marfPath(heightToHashKey(height - 1));
|
|
1875
|
+
return w.leaves.find((l) => bytesEqual(l.path, path))?.valueHash ?? null;
|
|
1876
|
+
} catch {
|
|
1877
|
+
return null;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function signatureFailure(sig, step, cycle) {
|
|
1881
|
+
const weight = `${sig.signedWeight}/${sig.totalWeight} signed, threshold ${sig.threshold}`;
|
|
1882
|
+
if (sig.unknown.length)
|
|
1883
|
+
return {
|
|
1884
|
+
step,
|
|
1885
|
+
code: "unknown-signer",
|
|
1886
|
+
message: `${sig.unknown.length} signer(s) not in cycle ${cycle}'s set (${weight})`,
|
|
1887
|
+
cycle
|
|
1888
|
+
};
|
|
1889
|
+
if (sig.duplicates.length)
|
|
1890
|
+
return {
|
|
1891
|
+
step,
|
|
1892
|
+
code: "duplicate-signer",
|
|
1893
|
+
message: `${sig.duplicates.length} duplicate signature(s) (${weight})`,
|
|
1894
|
+
cycle
|
|
1895
|
+
};
|
|
1896
|
+
if (sig.invalid)
|
|
1897
|
+
return {
|
|
1898
|
+
step,
|
|
1899
|
+
code: "bad-signature",
|
|
1900
|
+
message: `${sig.invalid} signature(s) do not recover (${weight})`,
|
|
1901
|
+
cycle
|
|
1902
|
+
};
|
|
1903
|
+
return {
|
|
1904
|
+
step,
|
|
1905
|
+
code: "below-threshold",
|
|
1906
|
+
message: `cycle ${cycle} signers: ${weight}`,
|
|
1907
|
+
cycle
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
var verifyBlock = (ref, opts) => new BlockVerifier(opts).verify(ref);
|
|
1911
|
+
export {
|
|
1912
|
+
BlockVerifier,
|
|
1913
|
+
HeaderChain,
|
|
1914
|
+
HeaderValidationError,
|
|
1915
|
+
MAINNET_CHECKPOINT,
|
|
1916
|
+
MAINNET_FIRST_BURN_HEIGHT,
|
|
1917
|
+
MAINNET_PREPARE_LENGTH,
|
|
1918
|
+
MAINNET_REWARD_CYCLE_LENGTH,
|
|
1919
|
+
NodeRpcProofSource,
|
|
1920
|
+
SIGNERS_CONTRACT,
|
|
1921
|
+
SecondlayerProofSource,
|
|
1922
|
+
SourceError,
|
|
1923
|
+
blockId,
|
|
1924
|
+
classifyBlockDiff,
|
|
1925
|
+
cycleStart,
|
|
1926
|
+
dataVarKey,
|
|
1927
|
+
decodeSignerSet,
|
|
1928
|
+
ftBalanceKey,
|
|
1929
|
+
hex,
|
|
1930
|
+
isEpoch2Header,
|
|
1931
|
+
mapEntryKey,
|
|
1932
|
+
marfPath,
|
|
1933
|
+
marfProofAncestors,
|
|
1934
|
+
marfValue,
|
|
1935
|
+
parseEpoch2Header,
|
|
1936
|
+
parseNakamotoHeader,
|
|
1937
|
+
parseWitness,
|
|
1938
|
+
rewardCycle,
|
|
1939
|
+
signerSignatureHash,
|
|
1940
|
+
unhex,
|
|
1941
|
+
verifyBlock,
|
|
1942
|
+
verifyBlockState,
|
|
1943
|
+
verifyConsensusPreimage,
|
|
1944
|
+
verifyMarfProof,
|
|
1945
|
+
verifySignerSignatures
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
//# debugId=028F52F18A5FBEDA64756E2164756E21
|
|
1949
|
+
//# sourceMappingURL=index.js.map
|