@smartledger/bsv 9.18.0 → 9.20.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.
@@ -129,9 +129,25 @@ NotaryHash.txidFromRawTx = function (rawTx) {
129
129
  * @param {Buffer|String} rawTx
130
130
  * @returns {Object|null} the parsed record, or null if there is none
131
131
  */
132
- NotaryHash.recordFromRawTx = function (rawTx) {
132
+ NotaryHash.recordFromRawTx = function (rawTx, vout) {
133
133
  try {
134
134
  var tx = new Transaction(Buffer.isBuffer(rawTx) ? rawTx.toString('hex') : rawTx)
135
+
136
+ // With a vout, read THAT output and no other.
137
+ //
138
+ // Scanning every output means the certificate's `anchor.vout` is decorative: it can name
139
+ // any index, or one past the end, and the record is still found somewhere else in the
140
+ // transaction. `anchor.vout` is covered by no signature, so that made it an editable
141
+ // field that looked authoritative. A caller verifying an anchor must read the output the
142
+ // certificate points at.
143
+ if (vout !== undefined && vout !== null) {
144
+ if (!Number.isInteger(vout) || vout < 0 || vout >= tx.outputs.length) {
145
+ return null
146
+ }
147
+ var at = tx.outputs[vout].script
148
+ return NotaryScript.isNotaryHash(at) ? NotaryScript.parse(at) : null
149
+ }
150
+
135
151
  for (var i = 0; i < tx.outputs.length; i++) {
136
152
  var script = tx.outputs[i].script
137
153
  if (NotaryScript.isNotaryHash(script)) {
@@ -252,9 +268,9 @@ NotaryHash.verifyAnchorSPV = function (certificate, opts) {
252
268
  errors.push('rawTx does not hash to anchor.txid')
253
269
  }
254
270
 
255
- var record = NotaryHash.recordFromRawTx(spv.rawTx)
271
+ var record = NotaryHash.recordFromRawTx(spv.rawTx, c.anchor && c.anchor.vout)
256
272
  if (!record) {
257
- errors.push('no NotaryHash record found in rawTx')
273
+ errors.push('no NotaryHash record at anchor.vout in rawTx')
258
274
  } else if (!NotaryHash.recordMatchesCertificate(record, c)) {
259
275
  errors.push('on-chain record does not match the certificate')
260
276
  }
@@ -349,6 +365,48 @@ NotaryHash.verifyAnchorSPV = function (certificate, opts) {
349
365
  }
350
366
  }
351
367
 
368
+ // The anchor's own labels must agree with what was actually proved.
369
+ //
370
+ // `anchor.blockTime` and `anchor.blockHeight` are covered by no signature and were
371
+ // checked by nothing, so an edited certificate verified while reporting a different time
372
+ // or height than the header and proof establish. A reader shown "anchored at
373
+ // 2026-06-23T10:06:04Z in block 954784" has no reason to doubt either number.
374
+ //
375
+ // A null is not a claim: a certificate issued before confirmation legitimately carries
376
+ // nulls, and only a stated value is checked.
377
+ var headerBuf = Buffer.isBuffer(opts.header)
378
+ ? opts.header
379
+ : Buffer.from(String(opts.header).replace(/^0x/, ''), 'hex')
380
+ if (headerBuf.length === 80 && c.anchor) {
381
+ if (c.anchor.blockTime !== undefined && c.anchor.blockTime !== null) {
382
+ var headerTime = headerBuf.readUInt32LE(68)
383
+ if (Number(c.anchor.blockTime) !== headerTime) {
384
+ errors.push('anchor.blockTime ' + c.anchor.blockTime +
385
+ ' is not the time in the block header (' + headerTime + ')')
386
+ }
387
+ }
388
+ if (c.anchor.blockHeight !== undefined && c.anchor.blockHeight !== null &&
389
+ spv.blockHeight !== undefined && spv.blockHeight !== null &&
390
+ Number(c.anchor.blockHeight) !== Number(spv.blockHeight)) {
391
+ errors.push('anchor.blockHeight ' + c.anchor.blockHeight +
392
+ ' does not equal spv.blockHeight ' + spv.blockHeight)
393
+ }
394
+ }
395
+
396
+ // A TSC index must be representable by the path it comes with: a path of n nodes
397
+ // addresses at most 2^n leaves, so a larger index describes a tree the path cannot
398
+ // belong to. Unchecked, the index was another unsigned field that verified while naming
399
+ // an impossible position.
400
+ var mp = spv.merkleProof
401
+ if (mp && Array.isArray(mp.nodes) && mp.index !== undefined && mp.index !== null) {
402
+ var capacity = Math.pow(2, mp.nodes.length)
403
+ if (!Number.isInteger(Number(mp.index)) || Number(mp.index) < 0 ||
404
+ Number(mp.index) >= capacity) {
405
+ errors.push('spv.merkleProof.index ' + mp.index + ' does not fit a path of ' +
406
+ mp.nodes.length + ' nodes, which addresses at most ' + capacity + ' leaves')
407
+ }
408
+ }
409
+
352
410
  return { valid: errors.length === 0, errors: errors }
353
411
  } catch (e) {
354
412
  return { valid: false, errors: ['anchor verification error: ' + e.message] }
@@ -408,6 +466,53 @@ function merkleFolds (c, failure) {
408
466
  var leafData = Certificate.decodeBytes(c.proofHash, 'hex', 'proofHash')
409
467
  var root = Certificate.decodeBytes(m.root, 'hex', 'merkle.root')
410
468
 
469
+ // `leafIndex` must agree with the sides the path carries, and this is the only place it
470
+ // can be checked.
471
+ //
472
+ // A sided path folds by its OWN sides, and `Certificate.normalize` returns a
473
+ // non-legacy certificate untouched — so for a reference certificate `leafIndex` was read
474
+ // and never used. It could be changed to any other value in range and the proof still
475
+ // verified. A legacy certificate was fine, because normalising it DERIVES the sides from
476
+ // `leafIndex`, so a wrong index produced a wrong fold and failed.
477
+ //
478
+ // That left `leafIndex` an unvalidated assertion sitting beside validated ones, and a
479
+ // reader shown "leaf 3 of 25" cannot tell which of those fields is load-bearing.
480
+ // `leafCount` is pinned by the on-chain `u32be` that `recordMatchesCertificate` compares;
481
+ // `leafIndex` appears on chain nowhere.
482
+ //
483
+ // No hashing is needed: RFC 6962 determines the side sequence from the index and the tree
484
+ // size, so the position the path implies is recomputed and compared in one pass.
485
+ //
486
+ // Reported by the ordinals mint anchoring BRC-220 proofs, whose tamper test caught it on
487
+ // moving from the legacy format to the reference one.
488
+ if (Number.isInteger(m.leafIndex) && Number.isInteger(m.leafCount) &&
489
+ m.leafCount > 0 && m.leafIndex >= 0 && m.leafIndex < m.leafCount) {
490
+ var implied = Merkle.pathSides(m.leafIndex, m.leafCount)
491
+ var carried = (m.path || []).map(function (n) {
492
+ return n && typeof n === 'object' && !Buffer.isBuffer(n) ? n.side : undefined
493
+ })
494
+ if (implied.length !== carried.length) {
495
+ return {
496
+ valid: false,
497
+ errors: ['merkle.leafIndex ' + m.leafIndex + ' in a tree of ' + m.leafCount +
498
+ ' leaves needs ' + implied.length + ' path nodes; the path has ' + carried.length]
499
+ }
500
+ }
501
+ for (var si = 0; si < implied.length; si++) {
502
+ // A bare-hash path carries no sides; those are derived from leafIndex when the
503
+ // certificate is normalised, so they agree by construction and there is nothing to
504
+ // cross-check.
505
+ if (carried[si] !== undefined && carried[si] !== implied[si]) {
506
+ return {
507
+ valid: false,
508
+ errors: ['merkle.leafIndex ' + m.leafIndex + ' disagrees with the path: node ' +
509
+ si + ' is marked "' + carried[si] + '" but leaf ' + m.leafIndex + ' of ' +
510
+ m.leafCount + ' requires "' + implied[si] + '"']
511
+ }
512
+ }
513
+ }
514
+ }
515
+
411
516
  // Folded by side, as the reference folds it. The sides are what the certificate
412
517
  // carries; a legacy bare-hash path had them derived from leafIndex and leafCount when
413
518
  // it was normalised.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "9.18.0",
3
+ "version": "9.20.0",
4
4
  "description": "Bitcoin SV library with an interpreter-verified script engine: OP_PUSH_TX covenants, BIP-143 preimage tooling, and consensus flags that match what miners actually enforce. Also ships DID:web / W3C VC-JWT credentials and the Legal Token Protocol.",
5
5
  "author": "SmartLedger Technology <hello@smartledger.technology> (https://smartledger.technology)",
6
6
  "homepage": "https://github.com/codenlighten/smartledger-bsv#readme",
package/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  'use strict'
2
2
  // GENERATED by scripts/sync-version.js on `npm version` — do not edit.
3
- module.exports = '9.18.0'
3
+ module.exports = '9.20.0'