@secondlayer/verify 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 blockId = (h) => hashAll([signerSignatureHash(h), h.consensusHash]);
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));
@@ -1068,6 +1118,7 @@ class SecondlayerProofSource {
1068
1118
 
1069
1119
  class NodeRpcProofSource {
1070
1120
  getBlock;
1121
+ getEpoch2Header;
1071
1122
  getMarfProof;
1072
1123
  constructor(opts) {
1073
1124
  const base = trimSlash(opts.nodeUrl);
@@ -1076,6 +1127,10 @@ class NodeRpcProofSource {
1076
1127
  const url = typeof ref === "number" ? `${base}/v3/blocks/height/${ref}` : `${base}/v3/blocks/${ref}`;
1077
1128
  return required(url, await getBytes(url, http));
1078
1129
  };
1130
+ this.getEpoch2Header = async (blockId) => {
1131
+ const url = `${base}/v2/headers/1?tip=${blockId}`;
1132
+ return toEpoch2(url, await getJson(url, http));
1133
+ };
1079
1134
  this.getMarfProof = async (pathHex, tipId) => {
1080
1135
  const url = `${base}/v2/clarity/marf/${pathHex}?tip=${tipId}&proof=1`;
1081
1136
  return toMarf(url, await getJson(url, http));
@@ -1085,7 +1140,6 @@ class NodeRpcProofSource {
1085
1140
  // src/state.ts
1086
1141
  var INTERNAL = {
1087
1142
  self: "__MARF_BLOCK_HEIGHT_SELF",
1088
- heightToHash: "__MARF_BLOCK_HEIGHT_TO_HASH",
1089
1143
  hashToHeight: "__MARF_BLOCK_HASH_TO_HEIGHT"
1090
1144
  };
1091
1145
  var heightValue = (h) => {
@@ -1096,7 +1150,7 @@ var heightValue = (h) => {
1096
1150
  var padded = (b32) => `${hex(b32)}${"00".repeat(8)}`;
1097
1151
  function internalEntries(height, parentBlockId, leaves) {
1098
1152
  const out = [[INTERNAL.self, heightValue(height)]];
1099
- const ownKey = `${INTERNAL.heightToHash}::${height}`;
1153
+ const ownKey = heightToHashKey(height);
1100
1154
  const ownPath = marfPath(ownKey);
1101
1155
  const own = leaves.find((l) => bytesEqual(l.path, ownPath));
1102
1156
  if (own) {
@@ -1107,10 +1161,7 @@ function internalEntries(height, parentBlockId, leaves) {
1107
1161
  ]);
1108
1162
  }
1109
1163
  if (height > 0) {
1110
- out.push([
1111
- `${INTERNAL.heightToHash}::${height - 1}`,
1112
- padded(parentBlockId)
1113
- ]);
1164
+ out.push([heightToHashKey(height - 1), padded(parentBlockId)]);
1114
1165
  out.push([
1115
1166
  `${INTERNAL.hashToHeight}::${hex(parentBlockId)}`,
1116
1167
  heightValue(height - 1)
@@ -1305,6 +1356,7 @@ async function fetchFor(step, what, call, extra = {}) {
1305
1356
  }
1306
1357
  }
1307
1358
  var BITCOIN_BATCH = 2016;
1359
+ var MAX_WALK = 16;
1308
1360
  async function findAnchor(low, high, window, probe, maxProbes = 32) {
1309
1361
  let [lo, hi] = [low, high];
1310
1362
  const target = (window[0] + window[1]) / 2;
@@ -1339,6 +1391,7 @@ class BlockVerifier {
1339
1391
  sets = new Map;
1340
1392
  anchors = new Map;
1341
1393
  headers = new Map;
1394
+ below = new Map;
1342
1395
  burns = new Map;
1343
1396
  preimages = new Map;
1344
1397
  start;
@@ -1366,23 +1419,38 @@ class BlockVerifier {
1366
1419
  return out;
1367
1420
  }
1368
1421
  async run(ref, out) {
1369
- const src = this.source;
1370
- const raw = await fetchFor("block", `block ${ref}`, () => src.getBlock(ref));
1371
- const header = parseHeader2(raw, "block");
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
+ }
1372
1442
  const id = hex(blockId(header));
1373
1443
  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
1444
  if (typeof ref === "number" && height !== ref)
1381
1445
  fail({
1382
1446
  step: "block",
1383
1447
  code: "height-mismatch",
1384
1448
  message: `source returned height ${height} for ${ref}`
1385
1449
  });
1450
+ await this.runForward(header, id, out);
1451
+ }
1452
+ async runForward(header, id, out) {
1453
+ const height = Number(header.chainLength);
1386
1454
  out.height = height;
1387
1455
  out.blockId = id;
1388
1456
  out.stateRoot = hex(header.stateIndexRoot);
@@ -1401,20 +1469,142 @@ class BlockVerifier {
1401
1469
  out.threshold = sig.threshold;
1402
1470
  if (!sig.valid)
1403
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);
1404
1593
  const witness = await fetchFor("witness", `witness ${id}`, () => src.getWitness(id));
1405
1594
  const { getStateWrites, getVmEvents } = this.rows ? src : {};
1406
1595
  const writes = getStateWrites ? await fetchFor("names", `state_writes ${height}`, () => getStateWrites.call(src, height)) : null;
1407
1596
  const rows = getVmEvents ? await fetchFor("rows", `vm_events ${height}`, () => getVmEvents.call(src, height)) : undefined;
1408
- const parentId = hex(header.parentBlockId);
1597
+ const parent = isEpoch2Header(header) ? epoch2ParentId(witness, header) : header.parentBlockId;
1598
+ const parentId = parent && hex(parent);
1409
1599
  const state = await verifyBlockState({
1410
1600
  height,
1411
- parentBlockId: header.parentBlockId,
1601
+ parentBlockId: parent ?? new Uint8Array(32),
1412
1602
  stateRoot: header.stateIndexRoot,
1413
1603
  witness,
1414
1604
  writes,
1415
1605
  rows,
1416
- parentLeaves: await this.parentLeaves(parentId, out.notes),
1417
- parentHolds: (leaf) => this.parentHolds(parentId, leaf)
1606
+ parentLeaves: parentId ? await this.parentLeaves(parentId, out.notes) : undefined,
1607
+ parentHolds: async (leaf) => parentId ? this.parentHolds(parentId, leaf) : false
1418
1608
  });
1419
1609
  out.diff = state.diff;
1420
1610
  if (rows)
@@ -1426,8 +1616,7 @@ class BlockVerifier {
1426
1616
  const known = this.headers.get(id);
1427
1617
  if (known)
1428
1618
  return known;
1429
- const raw = await fetchFor(step, `block ${id}`, () => this.source.getBlock(id), extra);
1430
- const header = parseHeader2(raw, step);
1619
+ const header = await this.fetchHeader(id, step, extra);
1431
1620
  if (hex(blockId(header)) !== id)
1432
1621
  fail({
1433
1622
  step,
@@ -1439,6 +1628,30 @@ class BlockVerifier {
1439
1628
  this.headers.set(id, header);
1440
1629
  return header;
1441
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
+ }
1442
1655
  async proveAt(tip, path, value, proof, step, extra = {}) {
1443
1656
  const headers = [tip];
1444
1657
  for (const id of marfProofAncestors(proof))
@@ -1650,6 +1863,20 @@ function parseHeader2(raw, step) {
1650
1863
  });
1651
1864
  }
1652
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
+ }
1653
1880
  function signatureFailure(sig, step, cycle) {
1654
1881
  const weight = `${sig.signedWeight}/${sig.totalWeight} signed, threshold ${sig.threshold}`;
1655
1882
  if (sig.unknown.length)
@@ -1700,10 +1927,12 @@ export {
1700
1927
  decodeSignerSet,
1701
1928
  ftBalanceKey,
1702
1929
  hex,
1930
+ isEpoch2Header,
1703
1931
  mapEntryKey,
1704
1932
  marfPath,
1705
1933
  marfProofAncestors,
1706
1934
  marfValue,
1935
+ parseEpoch2Header,
1707
1936
  parseNakamotoHeader,
1708
1937
  parseWitness,
1709
1938
  rewardCycle,
@@ -1716,5 +1945,5 @@ export {
1716
1945
  verifySignerSignatures
1717
1946
  };
1718
1947
 
1719
- //# debugId=67FB81E2AAB4645564756E2164756E21
1948
+ //# debugId=028F52F18A5FBEDA64756E2164756E21
1720
1949
  //# sourceMappingURL=index.js.map