@smartledger/bsv 9.19.0 → 9.21.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.
@@ -126,12 +126,40 @@ NotaryHash.txidFromRawTx = function (rawTx) {
126
126
  /**
127
127
  * Find and parse the NotaryHash record in a raw transaction.
128
128
  *
129
+ * **Pass `vout` whenever you are verifying a certificate.** Called with one argument this
130
+ * scans every output and returns the first record it finds, which is useful for inspecting
131
+ * a transaction you know nothing about — and wrong for verification, because it makes the
132
+ * certificate's `anchor.vout` decorative: the field can name a payment output, or one past
133
+ * the end, and a record is still found somewhere else in the transaction. `anchor.vout` is
134
+ * covered by no signature, so the scanning form hands an editable field the appearance of
135
+ * having been checked.
136
+ *
137
+ * `verifyAnchorSPV` passes `anchor.vout` for exactly this reason. The one-argument form is
138
+ * kept for inspection and for callers that predate the parameter.
139
+ *
129
140
  * @param {Buffer|String} rawTx
141
+ * @param {Number} [vout] read only this output; omit to scan (inspection only)
130
142
  * @returns {Object|null} the parsed record, or null if there is none
131
143
  */
132
- NotaryHash.recordFromRawTx = function (rawTx) {
144
+ NotaryHash.recordFromRawTx = function (rawTx, vout) {
133
145
  try {
134
146
  var tx = new Transaction(Buffer.isBuffer(rawTx) ? rawTx.toString('hex') : rawTx)
147
+
148
+ // With a vout, read THAT output and no other.
149
+ //
150
+ // Scanning every output means the certificate's `anchor.vout` is decorative: it can name
151
+ // any index, or one past the end, and the record is still found somewhere else in the
152
+ // transaction. `anchor.vout` is covered by no signature, so that made it an editable
153
+ // field that looked authoritative. A caller verifying an anchor must read the output the
154
+ // certificate points at.
155
+ if (vout !== undefined && vout !== null) {
156
+ if (!Number.isInteger(vout) || vout < 0 || vout >= tx.outputs.length) {
157
+ return null
158
+ }
159
+ var at = tx.outputs[vout].script
160
+ return NotaryScript.isNotaryHash(at) ? NotaryScript.parse(at) : null
161
+ }
162
+
135
163
  for (var i = 0; i < tx.outputs.length; i++) {
136
164
  var script = tx.outputs[i].script
137
165
  if (NotaryScript.isNotaryHash(script)) {
@@ -252,9 +280,9 @@ NotaryHash.verifyAnchorSPV = function (certificate, opts) {
252
280
  errors.push('rawTx does not hash to anchor.txid')
253
281
  }
254
282
 
255
- var record = NotaryHash.recordFromRawTx(spv.rawTx)
283
+ var record = NotaryHash.recordFromRawTx(spv.rawTx, c.anchor && c.anchor.vout)
256
284
  if (!record) {
257
- errors.push('no NotaryHash record found in rawTx')
285
+ errors.push('no NotaryHash record at anchor.vout in rawTx')
258
286
  } else if (!NotaryHash.recordMatchesCertificate(record, c)) {
259
287
  errors.push('on-chain record does not match the certificate')
260
288
  }
@@ -349,6 +377,68 @@ NotaryHash.verifyAnchorSPV = function (certificate, opts) {
349
377
  }
350
378
  }
351
379
 
380
+ // The anchor's own labels must agree with what was actually proved.
381
+ //
382
+ // `anchor.blockTime` and `anchor.blockHeight` are covered by no signature and were
383
+ // checked by nothing, so an edited certificate verified while reporting a different time
384
+ // or height than the header and proof establish. A reader shown "anchored at
385
+ // 2026-06-23T10:06:04Z in block 954784" has no reason to doubt either number.
386
+ //
387
+ // A null is not a claim: a certificate issued before confirmation legitimately carries
388
+ // nulls, and only a stated value is checked.
389
+ var headerBuf = Buffer.isBuffer(opts.header)
390
+ ? opts.header
391
+ : Buffer.from(String(opts.header).replace(/^0x/, ''), 'hex')
392
+ if (headerBuf.length === 80 && c.anchor) {
393
+ if (c.anchor.blockTime !== undefined && c.anchor.blockTime !== null) {
394
+ var headerTime = headerBuf.readUInt32LE(68)
395
+ if (Number(c.anchor.blockTime) !== headerTime) {
396
+ errors.push('anchor.blockTime ' + c.anchor.blockTime +
397
+ ' is not the time in the block header (' + headerTime + ')')
398
+ }
399
+ }
400
+ if (c.anchor.blockHeight !== undefined && c.anchor.blockHeight !== null &&
401
+ spv.blockHeight !== undefined && spv.blockHeight !== null &&
402
+ Number(c.anchor.blockHeight) !== Number(spv.blockHeight)) {
403
+ errors.push('anchor.blockHeight ' + c.anchor.blockHeight +
404
+ ' does not equal spv.blockHeight ' + spv.blockHeight)
405
+ }
406
+ }
407
+
408
+ // anchor.network, when the caller says which chain it expects.
409
+ //
410
+ // The last field in a certificate that no signature covers. BRC-220 calls it descriptive
411
+ // — the headers decide the chain, not the label — so accepting any value is conformant,
412
+ // and this stays OPT-IN for that reason: a testnet caller must not start failing because
413
+ // a default appeared. But a relabelled certificate still renders as a verified record on
414
+ // a page, and a mainnet certificate relabelled "bsv-testnet" verified here, as did one
415
+ // relabelled "not-a-chain".
416
+ //
417
+ // Be clear about what this is worth: it compares a label against the CALLER'S OWN
418
+ // setting. It does not prove which chain the header source serves. A caller who needs
419
+ // that must get its headers from a source it trusts for the chain it means — which is
420
+ // the same reason this library never fetches a header.
421
+ if (opts.network !== undefined && opts.network !== null &&
422
+ c.anchor && c.anchor.network !== undefined && c.anchor.network !== null &&
423
+ String(c.anchor.network) !== String(opts.network)) {
424
+ errors.push('anchor.network is "' + c.anchor.network + '" but the verifier expects "' +
425
+ opts.network + '"')
426
+ }
427
+
428
+ // A TSC index must be representable by the path it comes with: a path of n nodes
429
+ // addresses at most 2^n leaves, so a larger index describes a tree the path cannot
430
+ // belong to. Unchecked, the index was another unsigned field that verified while naming
431
+ // an impossible position.
432
+ var mp = spv.merkleProof
433
+ if (mp && Array.isArray(mp.nodes) && mp.index !== undefined && mp.index !== null) {
434
+ var capacity = Math.pow(2, mp.nodes.length)
435
+ if (!Number.isInteger(Number(mp.index)) || Number(mp.index) < 0 ||
436
+ Number(mp.index) >= capacity) {
437
+ errors.push('spv.merkleProof.index ' + mp.index + ' does not fit a path of ' +
438
+ mp.nodes.length + ' nodes, which addresses at most ' + capacity + ' leaves')
439
+ }
440
+ }
441
+
352
442
  return { valid: errors.length === 0, errors: errors }
353
443
  } catch (e) {
354
444
  return { valid: false, errors: ['anchor verification error: ' + e.message] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "9.19.0",
3
+ "version": "9.21.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.19.0'
3
+ module.exports = '9.21.0'