@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.
- package/CHANGELOG.md +123 -0
- package/README.md +19 -19
- package/bsv-gdaf.min.js +32 -32
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +32 -32
- package/bsv.min.js +32 -32
- package/docs/AUDIT_SCOPE.md +7 -7
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/audit-rfq/cure53.txt +2 -2
- package/docs/audit-rfq/ncc-group.txt +2 -2
- package/docs/audit-rfq/trail-of-bits.txt +2 -2
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/lib/gdaf/zk-prover.js +80 -10
- package/lib/notaryhash/certificate.js +64 -1
- package/lib/notaryhash/index.js +108 -3
- package/package.json +1 -1
- package/version.js +1 -1
package/lib/notaryhash/index.js
CHANGED
|
@@ -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
|
|
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.
|
|
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