@smartledger/bsv 7.5.0 → 7.5.2

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.
@@ -0,0 +1,88 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // The GDAF anchoring wrappers take (payload, privateKey, options) while the underlying
6
+ // SmartLedgerAnchor methods take (payload, metadata, utxos). Every wrapper forwarded
7
+ // `privateKey` into a slot that is not a key. For anchorCredential/anchorBatch it landed
8
+ // in `metadata`, which is JSON.stringify-ed straight into the OP_RETURN — so the secret
9
+ // scalar was broadcast, and the key was recoverable from chain data with an identical WIF.
10
+
11
+ require('chai').should()
12
+ var bsv = require('../..')
13
+
14
+ describe('GDAF anchoring never publishes key material', function () {
15
+ var key = bsv.PrivateKey.fromBuffer(Buffer.alloc(32, 7))
16
+ var utxos = [{
17
+ txId: '11'.repeat(32),
18
+ outputIndex: 0,
19
+ script: bsv.Script.buildPublicKeyHashOut(key.toAddress()).toHex(),
20
+ satoshis: 100000
21
+ }]
22
+ var HASH = 'ab'.repeat(32)
23
+
24
+ function gdaf () { return new bsv.GDAF() }
25
+
26
+ // Pull every OP_RETURN payload out of a built transaction, as UTF-8.
27
+ function payloadsOf (result) {
28
+ var tx = new bsv.Transaction((result.transaction || result.tx || result).toString())
29
+ return tx.outputs.map(function (o) {
30
+ var asm = o.script.toASM().split(' ')
31
+ return Buffer.from(asm[asm.length - 1], 'hex').toString('utf8')
32
+ })
33
+ }
34
+
35
+ function assertNoSecret (result) {
36
+ var scalarHex = key.bn.toString('hex')
37
+ var raw = (result.transaction || result.tx || result).toString()
38
+ raw.indexOf(Buffer.from(scalarHex, 'utf8').toString('hex')).should.equal(-1)
39
+ raw.indexOf(Buffer.from('"bn"', 'utf8').toString('hex')).should.equal(-1)
40
+ payloadsOf(result).forEach(function (p) {
41
+ p.indexOf(scalarHex).should.equal(-1)
42
+ p.indexOf('"bn"').should.equal(-1)
43
+ })
44
+ }
45
+
46
+ it('anchorCredential does not put the key in the OP_RETURN', function () {
47
+ return gdaf().anchorCredential(HASH, key, utxos).then(assertNoSecret)
48
+ })
49
+
50
+ it('anchorBatch does not put the key in the OP_RETURN', function () {
51
+ return gdaf().anchorBatch([HASH, 'cd'.repeat(32)], key, utxos).then(assertNoSecret)
52
+ })
53
+
54
+ it('accepts the documented { utxos, metadata } options shape', function () {
55
+ return gdaf().anchorCredential(HASH, key, { utxos: utxos, metadata: { issuer: 'did:web:example' } })
56
+ .then(function (r) {
57
+ assertNoSecret(r)
58
+ // The caller's own metadata still reaches the chain.
59
+ payloadsOf(r).join('').indexOf('did:web:example').should.not.equal(-1)
60
+ })
61
+ })
62
+
63
+ it('still accepts a bare UTXO array, the only shape that used to work', function () {
64
+ return gdaf().anchorCredential(HASH, key, utxos).then(function (r) {
65
+ r.should.be.an('object')
66
+ })
67
+ })
68
+
69
+ // Defence in depth: even reaching the payload builder with key material must fail
70
+ // loudly rather than broadcast it.
71
+ it('refuses to anchor metadata containing a PrivateKey instance', function () {
72
+ return gdaf().anchorCredential(HASH, key, { utxos: utxos, metadata: { signer: key } })
73
+ .then(function () { throw new Error('should not have anchored') },
74
+ function (err) { err.message.should.match(/Refusing to anchor/) })
75
+ })
76
+
77
+ it('refuses to anchor metadata carrying a key-shaped field', function () {
78
+ return gdaf().anchorCredential(HASH, key, { utxos: utxos, metadata: { wif: key.toWIF() } })
79
+ .then(function () { throw new Error('should not have anchored') },
80
+ function (err) { err.message.should.match(/looks like key material/) })
81
+ })
82
+
83
+ it('leaves ordinary metadata alone', function () {
84
+ return gdaf().anchorCredential(HASH, key, {
85
+ utxos: utxos, metadata: { id: 'urn:uuid:1', nested: { note: 'fine' } }
86
+ }).then(function (r) { assertNoSecret(r) })
87
+ })
88
+ })
@@ -205,7 +205,13 @@ describe('PrivateKey', function () {
205
205
  network: 'livenet'
206
206
  })
207
207
  var key = PrivateKey.fromObject(JSON.parse(json))
208
- JSON.stringify(key).should.equal(json)
208
+ // toObject() is the deliberate export and still round-trips exactly...
209
+ JSON.stringify(key.toObject()).should.equal(json)
210
+ // ...but JSON.stringify() must NOT emit the secret. This test used to assert that
211
+ // it did, which is how a private key reached an OP_RETURN via the GDAF anchor path:
212
+ // anything that stringified an object holding a key published the scalar.
213
+ JSON.parse(JSON.stringify(key)).bn.should.equal('[REDACTED]')
214
+ JSON.stringify(key).indexOf('96c13222').should.equal(-1)
209
215
  })
210
216
 
211
217
  it('should input/output json', function () {
@@ -215,7 +221,13 @@ describe('PrivateKey', function () {
215
221
  network: 'livenet'
216
222
  })
217
223
  var key = PrivateKey.fromJSON(JSON.parse(json))
218
- JSON.stringify(key).should.equal(json)
224
+ // toObject() is the deliberate export and still round-trips exactly...
225
+ JSON.stringify(key.toObject()).should.equal(json)
226
+ // ...but JSON.stringify() must NOT emit the secret. This test used to assert that
227
+ // it did, which is how a private key reached an OP_RETURN via the GDAF anchor path:
228
+ // anything that stringified an object holding a key published the scalar.
229
+ JSON.parse(JSON.stringify(key)).bn.should.equal('[REDACTED]')
230
+ JSON.stringify(key).indexOf('96c13222').should.equal(-1)
219
231
  })
220
232
 
221
233
  it('input json should correctly initialize network field', function () {
@@ -427,4 +439,69 @@ describe('PrivateKey', function () {
427
439
  var privkey = new PrivateKey('92VYMmwFLXRwXn5688edGxYYgMFsc3fUXYhGp17WocQhU6zG1kd')
428
440
  privkey.publicKey.toAddress().toString().should.equal('moiAvLUw16qgrwhFGo1eDnXHC2wPMYiv7Y')
429
441
  })
442
+ // Reported from the field against 7.4.0 and reproduced here. Each of these returned a
443
+ // DIFFERENT key or address than the caller asked for, silently.
444
+ describe('argument handling that used to produce the wrong key', function () {
445
+ var HEX = '0000000000000000000000000000000000000000000000000000000000000001'
446
+
447
+ it('honours the network passed to fromString instead of discarding it', function () {
448
+ // Returned a livenet key — and therefore a MAINNET address — so funds sent to it
449
+ // landed on the wrong network.
450
+ var k = PrivateKey.fromString(HEX, 'testnet')
451
+ k.network.name.should.equal('testnet')
452
+ k.toAddress().toString().should.equal(new PrivateKey(HEX, 'testnet').toAddress().toString())
453
+ })
454
+
455
+ it('agrees with the constructor for WIF input', function () {
456
+ var wif = new PrivateKey(HEX, 'testnet').toWIF()
457
+ PrivateKey.fromString(wif).toWIF().should.equal(wif)
458
+ PrivateKey.fromString(wif, 'testnet').toWIF().should.equal(wif)
459
+ })
460
+
461
+ it('rejects a network that contradicts the WIF rather than ignoring it', function () {
462
+ var wif = new PrivateKey(HEX, 'testnet').toWIF()
463
+ ;(function () { PrivateKey.fromString(wif, 'livenet') }).should.throw()
464
+ })
465
+
466
+ it('fromHex and the constructor return the SAME key', function () {
467
+ // These disagreed on the compression flag, so the same input produced two different
468
+ // addresses and two different WIFs. Restore by the wrong route, derive an address
469
+ // you never funded.
470
+ var a = new PrivateKey(HEX)
471
+ var b = PrivateKey.fromHex(HEX)
472
+ var c = PrivateKey.fromBuffer(Buffer.from(HEX, 'hex'))
473
+ b.compressed.should.equal(a.compressed)
474
+ b.toWIF().should.equal(a.toWIF())
475
+ b.toAddress().toString().should.equal(a.toAddress().toString())
476
+ c.toWIF().should.equal(a.toWIF())
477
+ })
478
+
479
+ it('still allows the legacy uncompressed form explicitly', function () {
480
+ PrivateKey.fromHex(HEX, null, false).compressed.should.equal(false)
481
+ PrivateKey.fromBuffer(Buffer.from(HEX, 'hex'), null, false).compressed.should.equal(false)
482
+ })
483
+ })
484
+
485
+ // JSON.stringify() on a key — or on any object holding one — used to emit the secret
486
+ // scalar. That is how a private key reached an OP_RETURN through the GDAF anchor path.
487
+ describe('does not leak the secret through JSON', function () {
488
+ it('redacts bn from JSON.stringify', function () {
489
+ var k = new PrivateKey()
490
+ var scalar = k.bn.toString('hex')
491
+ JSON.stringify(k).indexOf(scalar).should.equal(-1)
492
+ JSON.parse(JSON.stringify(k)).bn.should.equal('[REDACTED]')
493
+ })
494
+
495
+ it('redacts it when the key is nested inside another object', function () {
496
+ var k = new PrivateKey()
497
+ var payload = JSON.stringify({ context: { signer: k } })
498
+ payload.indexOf(k.bn.toString('hex')).should.equal(-1)
499
+ })
500
+
501
+ it('keeps toObject() exact, so deliberate export still round-trips', function () {
502
+ var k = new PrivateKey()
503
+ k.toObject().bn.should.equal(k.bn.toString('hex'))
504
+ PrivateKey.fromObject(k.toObject()).toWIF().should.equal(k.toWIF())
505
+ })
506
+ })
430
507
  })
@@ -45,7 +45,9 @@ function collectDeclaredPaths (source) {
45
45
  } else if (ts.isClassDeclaration(node) && node.name) {
46
46
  paths.push(prefix.concat(node.name.text))
47
47
  } else if (ts.isFunctionDeclaration(node) && node.name) {
48
- paths.push(prefix.concat(node.name.text))
48
+ var p = prefix.concat(node.name.text)
49
+ p.returnType = node.type ? node.type.getText() : null
50
+ paths.push(p)
49
51
  } else if (ts.isVariableStatement(node)) {
50
52
  node.declarationList.declarations.forEach(function (d) {
51
53
  if (ts.isIdentifier(d.name)) paths.push(prefix.concat(d.name.text))
@@ -89,6 +91,26 @@ describe('types: bsv.d.ts does not drift from runtime', function () {
89
91
  { label: 'SPV', obj: bsv.SPV, decl: 'SPV' }
90
92
  ]
91
93
 
94
+ // An `async` runtime function declared with a non-Promise return type is the most
95
+ // dangerous kind of drift, because the type checker actively hides the bug rather than
96
+ // merely failing to catch it: `StatusList.getCredentialStatusEntry` was declared
97
+ // `: CredentialStatus` while being async, so `if (fn(...) === 'revoked')` compiled clean
98
+ // and was ALWAYS false — every revoked credential passed as valid.
99
+ it('no async runtime function is declared with a synchronous return type', function () {
100
+ var lies = []
101
+ declaredPaths.forEach(function (segs) {
102
+ if (!segs.returnType) return
103
+ var fn = resolve(bsv, segs)
104
+ if (typeof fn !== 'function') return
105
+ var isAsync = fn.constructor && fn.constructor.name === 'AsyncFunction'
106
+ var declaresPromise = /\bPromise\s*</.test(segs.returnType)
107
+ if (isAsync && !declaresPromise) {
108
+ lies.push(segs.join('.') + ' is async but is declared `: ' + segs.returnType + '`')
109
+ }
110
+ })
111
+ lies.should.deep.equal([], 'these declarations hide a Promise:\n ' + lies.join('\n '))
112
+ })
113
+
92
114
  AUDITED.forEach(function (ns) {
93
115
  it('every runtime function on ' + ns.label + ' is declared in bsv.d.ts', function () {
94
116
  ns.obj.should.be.an('object')
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Runtime half of a field review that found declarations disagreeing with the code.
6
+ // The declarations themselves are gated by dts_drift.js and by the tsc check in CI; the
7
+ // cases below are the ones where the RUNTIME also had to change, because a declaration
8
+ // that merely matched the old behaviour would have been documenting a bug.
9
+
10
+ var should = require('chai').should()
11
+ var bsv = require('../..')
12
+ var SC = bsv.SmartContract
13
+
14
+ describe('public surface tells the truth about itself', function () {
15
+ describe('SmartContract.ownershipToken forwards its authorizer', function () {
16
+ it('is not a two-argument alias that drops the third', function () {
17
+ // The top-level alias took (fee, ownerPubKeyHash) and called through with only
18
+ // those two, so a co-signed token built via this path came out single-key — the
19
+ // authorizer was accepted and silently discarded.
20
+ var auth = SC.Authorizers.multisig(2, 3)
21
+ var withAuth = SC.ownershipToken(1, Buffer.alloc(20), auth).toHex()
22
+ var without = SC.ownershipToken(1, Buffer.alloc(20)).toHex()
23
+ withAuth.should.not.equal(without)
24
+ })
25
+
26
+ it('agrees with SmartContract.Token.ownershipToken', function () {
27
+ var auth = SC.Authorizers.multisig(2, 3)
28
+ SC.ownershipToken(1, Buffer.alloc(20), auth).toHex()
29
+ .should.equal(SC.Token.ownershipToken(1, Buffer.alloc(20), auth).toHex())
30
+ })
31
+ })
32
+
33
+ describe('Authorizers.multisig takes a key COUNT', function () {
34
+ it('rejects an array of keys instead of silently building a broken authorizer', function () {
35
+ // `m > nKeys` compared a number to an array, which coerces to NaN, so the guard
36
+ // passed and the array was interpolated into the authorizer's name.
37
+ var keys = [1, 2, 3].map(function () {
38
+ return bsv.PrivateKey.fromRandom().toPublicKey().toBuffer()
39
+ })
40
+ ;(function () { SC.Authorizers.multisig(2, keys) }).should.throw(/NUMBER of keys/)
41
+ // The message names the fix.
42
+ ;(function () { SC.Authorizers.multisig(2, keys) }).should.throw(/pass 3 instead/)
43
+ })
44
+
45
+ it('still accepts the documented count form', function () {
46
+ SC.Authorizers.multisig(2, 3).n.should.equal(3)
47
+ SC.Authorizers.multisig(2, 3).m.should.equal(2)
48
+ })
49
+
50
+ it('rejects non-integer m', function () {
51
+ (function () { SC.Authorizers.multisig('2', 3) }).should.throw(/m must be an integer/)
52
+ })
53
+ })
54
+
55
+ describe('StatusList refuses to record a suspension as a revocation', function () {
56
+ it('throws on status: suspended rather than setting the revocation bit', function () {
57
+ // Both statuses set the SAME bit and read back as 'revoked', so a suspension was
58
+ // silently recorded — and later reported — as a permanent revocation.
59
+ return bsv.StatusList.updateStatusList({
60
+ listVcJwt: 'x', index: 1, status: 'suspended', privateJwk: {}
61
+ }).then(
62
+ function () { throw new Error('should not have accepted suspended') },
63
+ function (err) { err.message.should.match(/'suspended' is not supported/) }
64
+ )
65
+ })
66
+ })
67
+
68
+ describe('securityFeatures describes what is actually shipped', function () {
69
+ it('no longer claims elliptic patches, since elliptic is not a dependency', function () {
70
+ var pkg = require('../../package.json')
71
+ var deps = Object.keys(pkg.dependencies || {})
72
+ deps.indexOf('elliptic').should.equal(-1)
73
+ bsv.securityFeatures.indexOf('elliptic-patches').should.equal(-1)
74
+ bsv.securityFeatures.should.be.an('array')
75
+ bsv.securityFeatures.length.should.be.above(0)
76
+ })
77
+ })
78
+
79
+ describe('documented sub-path entry points resolve', function () {
80
+ // Every one of these was MODULE_NOT_FOUND: the package `exports` map had no aliases
81
+ // for the *-entry.js files, so a documented deep import could not be loaded at all.
82
+ var SUBPATHS = ['didweb', 'vcjwt', 'gdaf', 'ltp', 'statuslist', 'shamir',
83
+ 'anchor', 'covenant', 'security', 'smartcontract', 'script-helper']
84
+
85
+ SUBPATHS.forEach(function (name) {
86
+ it('@smartledger/bsv/' + name, function () {
87
+ var mod = require('@smartledger/bsv/' + name)
88
+ // Some entry points export a class (a function), others a namespace object.
89
+ var t = typeof mod
90
+ t.should.be.oneOf(['object', 'function'])
91
+ should.exist(mod)
92
+ })
93
+ })
94
+
95
+ it('still resolves the package root and lib deep imports', function () {
96
+ require('@smartledger/bsv').should.be.an('object')
97
+ require('@smartledger/bsv/version').should.be.a('string')
98
+ // Directory deep-imports need the explicit file — a documented 7.0 exports break.
99
+ require('@smartledger/bsv/lib/ordinals/index.js').should.be.an('object')
100
+ })
101
+ })
102
+ })
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 = '7.5.0'
3
+ module.exports = '7.5.2'