@secondlayer/verify 0.1.0 → 0.3.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 +21 -6
- package/dist/index.d.ts +200 -22
- package/dist/index.js +375 -27
- package/dist/index.js.map +10 -9
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -100,7 +100,34 @@ function parseNakamotoHeader(raw) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
var signerSignatureHash = (h) => sha512_2562(h.signerSighashPreimage);
|
|
103
|
-
var
|
|
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
|
+
]);
|
|
104
131
|
// src/marf.ts
|
|
105
132
|
import { sha512_256 as sha512_2563 } from "@noble/hashes/sha2.js";
|
|
106
133
|
var MARF_VALUE_SIZE = 40;
|
|
@@ -120,6 +147,7 @@ var NODE_PTR_COUNT = {
|
|
|
120
147
|
};
|
|
121
148
|
var isBackptr = (id) => (id & 128) !== 0;
|
|
122
149
|
var marfPath = (key) => sha512_2563(new TextEncoder().encode(key));
|
|
150
|
+
var heightToHashKey = (height) => `__MARF_BLOCK_HEIGHT_TO_HASH::${height}`;
|
|
123
151
|
function marfValue(value) {
|
|
124
152
|
const v = new Uint8Array(MARF_VALUE_SIZE);
|
|
125
153
|
v.set(sha512_2563(new TextEncoder().encode(value)));
|
|
@@ -352,6 +380,14 @@ function marfProofAncestors(proof) {
|
|
|
352
380
|
}
|
|
353
381
|
return [...ids];
|
|
354
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
|
+
}
|
|
355
391
|
function verifyMarfProof(input) {
|
|
356
392
|
const rootToBlock = new Map;
|
|
357
393
|
for (const h of input.headers)
|
|
@@ -982,6 +1018,15 @@ var required = (url, v) => {
|
|
|
982
1018
|
return v;
|
|
983
1019
|
};
|
|
984
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
|
+
};
|
|
985
1030
|
var toMarf = (url, j) => {
|
|
986
1031
|
if (j === null)
|
|
987
1032
|
return null;
|
|
@@ -1018,6 +1063,7 @@ async function pages(field, url, http) {
|
|
|
1018
1063
|
|
|
1019
1064
|
class SecondlayerProofSource {
|
|
1020
1065
|
getBlock;
|
|
1066
|
+
getEpoch2Header;
|
|
1021
1067
|
getMarfProof;
|
|
1022
1068
|
getWitness;
|
|
1023
1069
|
getBurnPreimage;
|
|
@@ -1034,6 +1080,10 @@ class SecondlayerProofSource {
|
|
|
1034
1080
|
const url = typeof ref === "number" ? `${base}/v1/proofs/block/height/${ref}` : `${base}/v1/proofs/block/${ref}`;
|
|
1035
1081
|
return required(url, await getBytes(url, http));
|
|
1036
1082
|
};
|
|
1083
|
+
this.getEpoch2Header = async (blockId) => {
|
|
1084
|
+
const url = `${base}/v1/proofs/epoch2-header/${blockId}`;
|
|
1085
|
+
return toEpoch2(url, await getJson(url, http));
|
|
1086
|
+
};
|
|
1037
1087
|
this.getMarfProof = async (pathHex, tipId) => {
|
|
1038
1088
|
const url = `${base}/v1/proofs/marf/${pathHex}?tip=${tipId}`;
|
|
1039
1089
|
return toMarf(url, await getJson(url, http));
|
|
@@ -1054,7 +1104,15 @@ class SecondlayerProofSource {
|
|
|
1054
1104
|
throw new SourceError(url, 200, `headers start at ${j.from}`);
|
|
1055
1105
|
return j.headers;
|
|
1056
1106
|
};
|
|
1057
|
-
this.getStateWrites = (height) =>
|
|
1107
|
+
this.getStateWrites = async (height) => {
|
|
1108
|
+
const url = `${base}/v1/proofs/writes/${height}`;
|
|
1109
|
+
const j = await getJson(url, http);
|
|
1110
|
+
if (j === null)
|
|
1111
|
+
return null;
|
|
1112
|
+
if (!Array.isArray(j.state_writes))
|
|
1113
|
+
throw new SourceError(url, 200, "answer has no state_writes array");
|
|
1114
|
+
return j.state_writes;
|
|
1115
|
+
};
|
|
1058
1116
|
this.getVmEvents = async (height) => {
|
|
1059
1117
|
const out = [];
|
|
1060
1118
|
for (const type of VM_WRITE_TYPES) {
|
|
@@ -1068,6 +1126,7 @@ class SecondlayerProofSource {
|
|
|
1068
1126
|
|
|
1069
1127
|
class NodeRpcProofSource {
|
|
1070
1128
|
getBlock;
|
|
1129
|
+
getEpoch2Header;
|
|
1071
1130
|
getMarfProof;
|
|
1072
1131
|
constructor(opts) {
|
|
1073
1132
|
const base = trimSlash(opts.nodeUrl);
|
|
@@ -1076,6 +1135,10 @@ class NodeRpcProofSource {
|
|
|
1076
1135
|
const url = typeof ref === "number" ? `${base}/v3/blocks/height/${ref}` : `${base}/v3/blocks/${ref}`;
|
|
1077
1136
|
return required(url, await getBytes(url, http));
|
|
1078
1137
|
};
|
|
1138
|
+
this.getEpoch2Header = async (blockId) => {
|
|
1139
|
+
const url = `${base}/v2/headers/1?tip=${blockId}`;
|
|
1140
|
+
return toEpoch2(url, await getJson(url, http));
|
|
1141
|
+
};
|
|
1079
1142
|
this.getMarfProof = async (pathHex, tipId) => {
|
|
1080
1143
|
const url = `${base}/v2/clarity/marf/${pathHex}?tip=${tipId}&proof=1`;
|
|
1081
1144
|
return toMarf(url, await getJson(url, http));
|
|
@@ -1085,7 +1148,6 @@ class NodeRpcProofSource {
|
|
|
1085
1148
|
// src/state.ts
|
|
1086
1149
|
var INTERNAL = {
|
|
1087
1150
|
self: "__MARF_BLOCK_HEIGHT_SELF",
|
|
1088
|
-
heightToHash: "__MARF_BLOCK_HEIGHT_TO_HASH",
|
|
1089
1151
|
hashToHeight: "__MARF_BLOCK_HASH_TO_HEIGHT"
|
|
1090
1152
|
};
|
|
1091
1153
|
var heightValue = (h) => {
|
|
@@ -1096,7 +1158,7 @@ var heightValue = (h) => {
|
|
|
1096
1158
|
var padded = (b32) => `${hex(b32)}${"00".repeat(8)}`;
|
|
1097
1159
|
function internalEntries(height, parentBlockId, leaves) {
|
|
1098
1160
|
const out = [[INTERNAL.self, heightValue(height)]];
|
|
1099
|
-
const ownKey =
|
|
1161
|
+
const ownKey = heightToHashKey(height);
|
|
1100
1162
|
const ownPath = marfPath(ownKey);
|
|
1101
1163
|
const own = leaves.find((l) => bytesEqual(l.path, ownPath));
|
|
1102
1164
|
if (own) {
|
|
@@ -1107,10 +1169,7 @@ function internalEntries(height, parentBlockId, leaves) {
|
|
|
1107
1169
|
]);
|
|
1108
1170
|
}
|
|
1109
1171
|
if (height > 0) {
|
|
1110
|
-
out.push([
|
|
1111
|
-
`${INTERNAL.heightToHash}::${height - 1}`,
|
|
1112
|
-
padded(parentBlockId)
|
|
1113
|
-
]);
|
|
1172
|
+
out.push([heightToHashKey(height - 1), padded(parentBlockId)]);
|
|
1114
1173
|
out.push([
|
|
1115
1174
|
`${INTERNAL.hashToHeight}::${hex(parentBlockId)}`,
|
|
1116
1175
|
heightValue(height - 1)
|
|
@@ -1250,6 +1309,7 @@ async function verifyBlockState(input) {
|
|
|
1250
1309
|
...leafHex(l),
|
|
1251
1310
|
key: l.key,
|
|
1252
1311
|
txIndex: last.get(l.key)?.txIndex ?? null,
|
|
1312
|
+
value: last.get(l.key)?.value,
|
|
1253
1313
|
named: true
|
|
1254
1314
|
})) : d.hidden.map((l) => ({ ...leafHex(l), named: false })),
|
|
1255
1315
|
carried: d.carried.map(leafHex),
|
|
@@ -1278,6 +1338,58 @@ async function verifyBlockState(input) {
|
|
|
1278
1338
|
out.rowsChecked = checkRows(input.rows, leaves, out.failures);
|
|
1279
1339
|
return out;
|
|
1280
1340
|
}
|
|
1341
|
+
// src/txs.ts
|
|
1342
|
+
import { sha512_256 as sha512_2564 } from "@noble/hashes/sha2.js";
|
|
1343
|
+
import { splitTransactions } from "@secondlayer/stacks/transactions";
|
|
1344
|
+
var LEAF_TAG = new Uint8Array([0]);
|
|
1345
|
+
var NODE_TAG = new Uint8Array([1]);
|
|
1346
|
+
function txMerkleRoot(txids) {
|
|
1347
|
+
if (txids.length === 0)
|
|
1348
|
+
throw new Error("no transactions");
|
|
1349
|
+
let level = txids.map((t) => sha512_2564(concat([LEAF_TAG, t])));
|
|
1350
|
+
do {
|
|
1351
|
+
if (level.length % 2 === 1)
|
|
1352
|
+
level.push(level[level.length - 1]);
|
|
1353
|
+
const next = [];
|
|
1354
|
+
for (let i = 0;i < level.length; i += 2) {
|
|
1355
|
+
next.push(sha512_2564(concat([NODE_TAG, level[i], level[i + 1]])));
|
|
1356
|
+
}
|
|
1357
|
+
level = next;
|
|
1358
|
+
} while (level.length > 1);
|
|
1359
|
+
return level[0];
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
class TransactionsError extends Error {
|
|
1363
|
+
code;
|
|
1364
|
+
constructor(code, message) {
|
|
1365
|
+
super(message);
|
|
1366
|
+
this.code = code;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function blockTransactions(raw, header) {
|
|
1370
|
+
const body = raw.subarray(header.byteLength);
|
|
1371
|
+
if (body.length === 0)
|
|
1372
|
+
return null;
|
|
1373
|
+
if (body.length < 4)
|
|
1374
|
+
throw new TransactionsError("malformed", "block body has no tx count");
|
|
1375
|
+
const count = new DataView(body.buffer, body.byteOffset, body.byteLength).getUint32(0);
|
|
1376
|
+
if (count === 0)
|
|
1377
|
+
throw new TransactionsError("malformed", "block has no transactions");
|
|
1378
|
+
let txs;
|
|
1379
|
+
try {
|
|
1380
|
+
txs = splitTransactions(body.subarray(4), count);
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
throw new TransactionsError("malformed", `transactions do not parse: ${err.message}`);
|
|
1383
|
+
}
|
|
1384
|
+
const used = txs.reduce((n, t) => n + t.length, 4);
|
|
1385
|
+
if (used !== body.length)
|
|
1386
|
+
throw new TransactionsError("malformed", `${body.length - used} bytes after the block's ${count} transactions`);
|
|
1387
|
+
const ids = txs.map((t) => sha512_2564(t));
|
|
1388
|
+
const root = txMerkleRoot(ids);
|
|
1389
|
+
if (!bytesEqual(root, header.txMerkleRoot))
|
|
1390
|
+
throw new TransactionsError("tx-root-mismatch", `transactions hash to ${hex(root)}, header tx_merkle_root is ${hex(header.txMerkleRoot)}`);
|
|
1391
|
+
return txs.map((t, i) => ({ txid: hex(ids[i]), raw: t }));
|
|
1392
|
+
}
|
|
1281
1393
|
// src/verify-block.ts
|
|
1282
1394
|
import { uintCV } from "@secondlayer/stacks/clarity";
|
|
1283
1395
|
class Broken extends Error {
|
|
@@ -1305,6 +1417,7 @@ async function fetchFor(step, what, call, extra = {}) {
|
|
|
1305
1417
|
}
|
|
1306
1418
|
}
|
|
1307
1419
|
var BITCOIN_BATCH = 2016;
|
|
1420
|
+
var MAX_WALK = 16;
|
|
1308
1421
|
async function findAnchor(low, high, window, probe, maxProbes = 32) {
|
|
1309
1422
|
let [lo, hi] = [low, high];
|
|
1310
1423
|
const target = (window[0] + window[1]) / 2;
|
|
@@ -1339,8 +1452,10 @@ class BlockVerifier {
|
|
|
1339
1452
|
sets = new Map;
|
|
1340
1453
|
anchors = new Map;
|
|
1341
1454
|
headers = new Map;
|
|
1455
|
+
below = new Map;
|
|
1342
1456
|
burns = new Map;
|
|
1343
1457
|
preimages = new Map;
|
|
1458
|
+
raws = new Map;
|
|
1344
1459
|
start;
|
|
1345
1460
|
constructor(opts) {
|
|
1346
1461
|
this.source = opts.source;
|
|
@@ -1366,26 +1481,42 @@ class BlockVerifier {
|
|
|
1366
1481
|
return out;
|
|
1367
1482
|
}
|
|
1368
1483
|
async run(ref, out) {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1484
|
+
const start = Number(this.start.header.chainLength);
|
|
1485
|
+
if (typeof ref === "number" && ref < start)
|
|
1486
|
+
return this.runBelow(ref, undefined, out);
|
|
1487
|
+
let header;
|
|
1488
|
+
if (typeof ref === "string") {
|
|
1489
|
+
const id = ref.replace(/^0x/, "").toLowerCase();
|
|
1490
|
+
header = await this.header(id, "block");
|
|
1491
|
+
const height = Number(header.chainLength);
|
|
1492
|
+
if (height < start)
|
|
1493
|
+
return this.runBelow(height, id, out);
|
|
1494
|
+
if (isEpoch2Header(header))
|
|
1495
|
+
fail({
|
|
1496
|
+
step: "block",
|
|
1497
|
+
code: "invalid-header",
|
|
1498
|
+
message: `epoch 2.x block ${id} claims height ${height}, at or above the checkpoint`
|
|
1499
|
+
});
|
|
1500
|
+
} else {
|
|
1501
|
+
const raw = await fetchFor("block", `block ${ref}`, () => this.source.getBlock(ref));
|
|
1502
|
+
header = parseHeader2(raw, "block");
|
|
1503
|
+
this.rememberRaw(hex(blockId(header)), raw);
|
|
1504
|
+
}
|
|
1372
1505
|
const id = hex(blockId(header));
|
|
1373
1506
|
const height = Number(header.chainLength);
|
|
1374
|
-
if (typeof ref === "string" && id !== ref.replace(/^0x/, "").toLowerCase())
|
|
1375
|
-
fail({
|
|
1376
|
-
step: "block",
|
|
1377
|
-
code: "id-mismatch",
|
|
1378
|
-
message: `source returned block ${id} for ${ref}`
|
|
1379
|
-
});
|
|
1380
1507
|
if (typeof ref === "number" && height !== ref)
|
|
1381
1508
|
fail({
|
|
1382
1509
|
step: "block",
|
|
1383
1510
|
code: "height-mismatch",
|
|
1384
1511
|
message: `source returned height ${height} for ${ref}`
|
|
1385
1512
|
});
|
|
1513
|
+
await this.runForward(header, id, out);
|
|
1514
|
+
}
|
|
1515
|
+
async runForward(header, id, out) {
|
|
1516
|
+
const height = Number(header.chainLength);
|
|
1386
1517
|
out.height = height;
|
|
1387
1518
|
out.blockId = id;
|
|
1388
|
-
|
|
1519
|
+
describeHeader(header, out);
|
|
1389
1520
|
const burn = await this.bindBurn(header, "burn");
|
|
1390
1521
|
const cycle = rewardCycle(burn.burnHeight);
|
|
1391
1522
|
out.burnHeight = burn.burnHeight;
|
|
@@ -1401,20 +1532,174 @@ class BlockVerifier {
|
|
|
1401
1532
|
out.threshold = sig.threshold;
|
|
1402
1533
|
if (!sig.valid)
|
|
1403
1534
|
fail(signatureFailure(sig, "signatures", cycle));
|
|
1535
|
+
await this.checkTransactions(header, id, out);
|
|
1536
|
+
await this.verifyState(header, id, out);
|
|
1537
|
+
}
|
|
1538
|
+
async runBelow(height, claimed, out) {
|
|
1539
|
+
out.height = height;
|
|
1540
|
+
const { id, ancestry } = await this.ancestor(height);
|
|
1541
|
+
out.ancestry = ancestry;
|
|
1542
|
+
if (claimed !== undefined && claimed !== id)
|
|
1543
|
+
fail({
|
|
1544
|
+
step: "ancestry",
|
|
1545
|
+
code: "not-ancestor",
|
|
1546
|
+
message: `block ${claimed} is not on the checkpoint's chain: its block at height ${height} is ${id}`,
|
|
1547
|
+
blockId: claimed
|
|
1548
|
+
});
|
|
1549
|
+
const header = await this.header(id, "block");
|
|
1550
|
+
if (Number(header.chainLength) !== height)
|
|
1551
|
+
fail({
|
|
1552
|
+
step: "block",
|
|
1553
|
+
code: "height-mismatch",
|
|
1554
|
+
message: `block ${id} claims height ${header.chainLength}, proven at ${height}`
|
|
1555
|
+
});
|
|
1556
|
+
out.blockId = id;
|
|
1557
|
+
describeHeader(header, out);
|
|
1558
|
+
await this.bindBurnBelow(header, out);
|
|
1559
|
+
await this.checkTransactions(header, id, out);
|
|
1560
|
+
await this.verifyState(header, id, out);
|
|
1561
|
+
}
|
|
1562
|
+
rememberRaw(id, raw) {
|
|
1563
|
+
this.raws.delete(id);
|
|
1564
|
+
this.raws.set(id, raw);
|
|
1565
|
+
while (this.raws.size > 4) {
|
|
1566
|
+
const oldest = this.raws.keys().next().value;
|
|
1567
|
+
this.raws.delete(oldest);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async checkTransactions(header, id, out) {
|
|
1571
|
+
if (isEpoch2Header(header)) {
|
|
1572
|
+
out.notes.push("epoch 2.x: transactions not checked");
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const raw = this.raws.get(id) ?? await fetchFor("txs", `block ${id}`, () => this.source.getBlock(id));
|
|
1576
|
+
let txs;
|
|
1577
|
+
try {
|
|
1578
|
+
txs = blockTransactions(raw, header);
|
|
1579
|
+
} catch (err) {
|
|
1580
|
+
if (!(err instanceof TransactionsError))
|
|
1581
|
+
throw err;
|
|
1582
|
+
return fail({ step: "txs", code: err.code, message: err.message });
|
|
1583
|
+
}
|
|
1584
|
+
if (txs === null) {
|
|
1585
|
+
out.notes.push(`transactions not checked: the source served block ${id}'s header only`);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
out.transactions = txs;
|
|
1589
|
+
}
|
|
1590
|
+
async ancestor(height) {
|
|
1591
|
+
const known = this.below.get(height);
|
|
1592
|
+
if (known)
|
|
1593
|
+
return known;
|
|
1594
|
+
const from = this.nearestAbove(height);
|
|
1595
|
+
if (from.height - height <= MAX_WALK) {
|
|
1596
|
+
const walked = await this.walk(from, height);
|
|
1597
|
+
if (walked)
|
|
1598
|
+
return walked;
|
|
1599
|
+
}
|
|
1600
|
+
return this.jump(this.nearestAbove(height), height);
|
|
1601
|
+
}
|
|
1602
|
+
nearestAbove(height) {
|
|
1603
|
+
let best = {
|
|
1604
|
+
height: Number(this.start.header.chainLength),
|
|
1605
|
+
id: this.start.id
|
|
1606
|
+
};
|
|
1607
|
+
for (const [h, { id }] of this.below)
|
|
1608
|
+
if (h > height && h < best.height)
|
|
1609
|
+
best = { height: h, id };
|
|
1610
|
+
return best;
|
|
1611
|
+
}
|
|
1612
|
+
async walk(from, height) {
|
|
1613
|
+
const ancestry = {
|
|
1614
|
+
fromHeight: from.height,
|
|
1615
|
+
fromBlockId: from.id,
|
|
1616
|
+
via: "parents"
|
|
1617
|
+
};
|
|
1618
|
+
let child = await this.header(from.id, "ancestry");
|
|
1619
|
+
for (let h = from.height - 1;h >= height; h--) {
|
|
1620
|
+
if (isEpoch2Header(child))
|
|
1621
|
+
return null;
|
|
1622
|
+
const id = hex(child.parentBlockId);
|
|
1623
|
+
const parent = await this.header(id, "ancestry", { blockId: id });
|
|
1624
|
+
if (Number(parent.chainLength) !== h)
|
|
1625
|
+
fail({
|
|
1626
|
+
step: "ancestry",
|
|
1627
|
+
code: "height-mismatch",
|
|
1628
|
+
message: `parent ${id} claims height ${parent.chainLength}, expected ${h}`,
|
|
1629
|
+
blockId: id
|
|
1630
|
+
});
|
|
1631
|
+
this.below.set(h, { id, ancestry });
|
|
1632
|
+
child = parent;
|
|
1633
|
+
}
|
|
1634
|
+
return this.below.get(height) ?? null;
|
|
1635
|
+
}
|
|
1636
|
+
async jump(from, height) {
|
|
1637
|
+
const at = { blockId: from.id };
|
|
1638
|
+
const tip = await this.header(from.id, "ancestry", at);
|
|
1639
|
+
const key = heightToHashKey(height);
|
|
1640
|
+
const path = marfPath(key);
|
|
1641
|
+
const answer = await fetchFor("ancestry", `${key} at ${from.id}`, () => this.source.getMarfProof(hex(path), from.id), at);
|
|
1642
|
+
if (!answer)
|
|
1643
|
+
return fail({
|
|
1644
|
+
step: "ancestry",
|
|
1645
|
+
code: "unavailable",
|
|
1646
|
+
message: `source has no MARF proof of ${key} at block ${from.height}, ${from.height - height} blocks up; parent links reach ${MAX_WALK}`,
|
|
1647
|
+
...at
|
|
1648
|
+
});
|
|
1649
|
+
const value = marfProofValue(answer.proof) ?? new Uint8Array;
|
|
1650
|
+
const proven = value.length === MARF_VALUE_SIZE && value.subarray(32).every((b) => b === 0) && await this.proveAt(tip, path, value, answer.proof, "ancestry", at);
|
|
1651
|
+
if (!proven)
|
|
1652
|
+
return fail({
|
|
1653
|
+
step: "ancestry",
|
|
1654
|
+
code: "ancestry-proof-invalid",
|
|
1655
|
+
message: `${key} is not proven in block ${from.height}'s state`,
|
|
1656
|
+
...at
|
|
1657
|
+
});
|
|
1658
|
+
const ancestry = {
|
|
1659
|
+
fromHeight: from.height,
|
|
1660
|
+
fromBlockId: from.id,
|
|
1661
|
+
via: "marf"
|
|
1662
|
+
};
|
|
1663
|
+
const found = { id: hex(value.subarray(0, 32)), ancestry };
|
|
1664
|
+
this.below.set(height, found);
|
|
1665
|
+
return found;
|
|
1666
|
+
}
|
|
1667
|
+
async bindBurnBelow(header, out) {
|
|
1668
|
+
let hint;
|
|
1669
|
+
try {
|
|
1670
|
+
hint = await this.burnHint(header);
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
out.notes.push(`burn block not checked: ${err.message}`);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
if (hint < this.chain.checkpointHeight) {
|
|
1676
|
+
out.notes.push(`burn block not checked: its height ${hint} is below the Bitcoin checkpoint ${this.chain.checkpointHeight}`);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const burn = await this.bindBurn(header, "burn");
|
|
1680
|
+
out.burnHeight = burn.burnHeight;
|
|
1681
|
+
out.bitcoinBlockHash = burn.hash;
|
|
1682
|
+
}
|
|
1683
|
+
async verifyState(header, id, out) {
|
|
1684
|
+
const src = this.source;
|
|
1685
|
+
const height = Number(header.chainLength);
|
|
1404
1686
|
const witness = await fetchFor("witness", `witness ${id}`, () => src.getWitness(id));
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1687
|
+
const writes = await this.stateWrites(height, out.notes);
|
|
1688
|
+
if (writes)
|
|
1689
|
+
out.writes = writes;
|
|
1690
|
+
const { getVmEvents } = this.rows ? src : {};
|
|
1407
1691
|
const rows = getVmEvents ? await fetchFor("rows", `vm_events ${height}`, () => getVmEvents.call(src, height)) : undefined;
|
|
1408
|
-
const
|
|
1692
|
+
const parent = isEpoch2Header(header) ? epoch2ParentId(witness, header) : header.parentBlockId;
|
|
1693
|
+
const parentId = parent && hex(parent);
|
|
1409
1694
|
const state = await verifyBlockState({
|
|
1410
1695
|
height,
|
|
1411
|
-
parentBlockId:
|
|
1696
|
+
parentBlockId: parent ?? new Uint8Array(32),
|
|
1412
1697
|
stateRoot: header.stateIndexRoot,
|
|
1413
1698
|
witness,
|
|
1414
1699
|
writes,
|
|
1415
1700
|
rows,
|
|
1416
|
-
parentLeaves: await this.parentLeaves(parentId, out.notes),
|
|
1417
|
-
parentHolds: (leaf) => this.parentHolds(parentId, leaf)
|
|
1701
|
+
parentLeaves: parentId ? await this.parentLeaves(parentId, out.notes) : undefined,
|
|
1702
|
+
parentHolds: async (leaf) => parentId ? this.parentHolds(parentId, leaf) : false
|
|
1418
1703
|
});
|
|
1419
1704
|
out.diff = state.diff;
|
|
1420
1705
|
if (rows)
|
|
@@ -1422,12 +1707,23 @@ class BlockVerifier {
|
|
|
1422
1707
|
out.notes.push(...state.notes);
|
|
1423
1708
|
out.failures.push(...state.failures);
|
|
1424
1709
|
}
|
|
1710
|
+
async stateWrites(height, notes) {
|
|
1711
|
+
const { getStateWrites } = this.source;
|
|
1712
|
+
if (!getStateWrites)
|
|
1713
|
+
return null;
|
|
1714
|
+
try {
|
|
1715
|
+
const writes = await getStateWrites.call(this.source, height);
|
|
1716
|
+
return writes?.length ? writes : null;
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
notes.push(`state writes for block ${height} unavailable: ${err.message}`);
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1425
1722
|
async header(id, step, extra = {}) {
|
|
1426
1723
|
const known = this.headers.get(id);
|
|
1427
1724
|
if (known)
|
|
1428
1725
|
return known;
|
|
1429
|
-
const
|
|
1430
|
-
const header = parseHeader2(raw, step);
|
|
1726
|
+
const header = await this.fetchHeader(id, step, extra);
|
|
1431
1727
|
if (hex(blockId(header)) !== id)
|
|
1432
1728
|
fail({
|
|
1433
1729
|
step,
|
|
@@ -1439,6 +1735,31 @@ class BlockVerifier {
|
|
|
1439
1735
|
this.headers.set(id, header);
|
|
1440
1736
|
return header;
|
|
1441
1737
|
}
|
|
1738
|
+
async fetchHeader(id, step, extra) {
|
|
1739
|
+
const src = this.source;
|
|
1740
|
+
let raw;
|
|
1741
|
+
try {
|
|
1742
|
+
raw = await src.getBlock(id);
|
|
1743
|
+
this.rememberRaw(id, raw);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
const reason = `block ${id}: ${err.message}`;
|
|
1746
|
+
const { getEpoch2Header } = src;
|
|
1747
|
+
if (!getEpoch2Header)
|
|
1748
|
+
return fail({ step, code: "unavailable", message: reason, ...extra });
|
|
1749
|
+
const e2 = await fetchFor(step, `${reason}; epoch 2.x header`, () => getEpoch2Header.call(src, id), extra);
|
|
1750
|
+
try {
|
|
1751
|
+
return parseEpoch2Header(e2.header, e2.consensusHash);
|
|
1752
|
+
} catch (parseErr) {
|
|
1753
|
+
return fail({
|
|
1754
|
+
step,
|
|
1755
|
+
code: "invalid-header",
|
|
1756
|
+
message: `epoch 2.x header ${id} does not parse: ${parseErr.message}`,
|
|
1757
|
+
...extra
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return parseHeader2(raw, step);
|
|
1762
|
+
}
|
|
1442
1763
|
async proveAt(tip, path, value, proof, step, extra = {}) {
|
|
1443
1764
|
const headers = [tip];
|
|
1444
1765
|
for (const id of marfProofAncestors(proof))
|
|
@@ -1639,6 +1960,14 @@ class BlockVerifier {
|
|
|
1639
1960
|
return set;
|
|
1640
1961
|
}
|
|
1641
1962
|
}
|
|
1963
|
+
function describeHeader(header, out) {
|
|
1964
|
+
out.stateRoot = hex(header.stateIndexRoot);
|
|
1965
|
+
out.consensusHash = hex(header.consensusHash);
|
|
1966
|
+
if (!isEpoch2Header(header)) {
|
|
1967
|
+
out.blockHash = hex(signerSignatureHash(header));
|
|
1968
|
+
out.timestamp = Number(header.timestamp);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1642
1971
|
function parseHeader2(raw, step) {
|
|
1643
1972
|
try {
|
|
1644
1973
|
return parseNakamotoHeader(raw);
|
|
@@ -1650,6 +1979,20 @@ function parseHeader2(raw, step) {
|
|
|
1650
1979
|
});
|
|
1651
1980
|
}
|
|
1652
1981
|
}
|
|
1982
|
+
function epoch2ParentId(witness, header) {
|
|
1983
|
+
const height = Number(header.chainLength);
|
|
1984
|
+
if (height === 0)
|
|
1985
|
+
return null;
|
|
1986
|
+
try {
|
|
1987
|
+
const w = parseWitness(witness);
|
|
1988
|
+
if (!bytesEqual(w.root, header.stateIndexRoot))
|
|
1989
|
+
return null;
|
|
1990
|
+
const path = marfPath(heightToHashKey(height - 1));
|
|
1991
|
+
return w.leaves.find((l) => bytesEqual(l.path, path))?.valueHash ?? null;
|
|
1992
|
+
} catch {
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1653
1996
|
function signatureFailure(sig, step, cycle) {
|
|
1654
1997
|
const weight = `${sig.signedWeight}/${sig.totalWeight} signed, threshold ${sig.threshold}`;
|
|
1655
1998
|
if (sig.unknown.length)
|
|
@@ -1693,21 +2036,26 @@ export {
|
|
|
1693
2036
|
SIGNERS_CONTRACT,
|
|
1694
2037
|
SecondlayerProofSource,
|
|
1695
2038
|
SourceError,
|
|
2039
|
+
TransactionsError,
|
|
1696
2040
|
blockId,
|
|
2041
|
+
blockTransactions,
|
|
1697
2042
|
classifyBlockDiff,
|
|
1698
2043
|
cycleStart,
|
|
1699
2044
|
dataVarKey,
|
|
1700
2045
|
decodeSignerSet,
|
|
1701
2046
|
ftBalanceKey,
|
|
1702
2047
|
hex,
|
|
2048
|
+
isEpoch2Header,
|
|
1703
2049
|
mapEntryKey,
|
|
1704
2050
|
marfPath,
|
|
1705
2051
|
marfProofAncestors,
|
|
1706
2052
|
marfValue,
|
|
2053
|
+
parseEpoch2Header,
|
|
1707
2054
|
parseNakamotoHeader,
|
|
1708
2055
|
parseWitness,
|
|
1709
2056
|
rewardCycle,
|
|
1710
2057
|
signerSignatureHash,
|
|
2058
|
+
txMerkleRoot,
|
|
1711
2059
|
unhex,
|
|
1712
2060
|
verifyBlock,
|
|
1713
2061
|
verifyBlockState,
|
|
@@ -1716,5 +2064,5 @@ export {
|
|
|
1716
2064
|
verifySignerSignatures
|
|
1717
2065
|
};
|
|
1718
2066
|
|
|
1719
|
-
//# debugId=
|
|
2067
|
+
//# debugId=AFAA56E81913DBC664756E2164756E21
|
|
1720
2068
|
//# sourceMappingURL=index.js.map
|