@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.
- package/CHANGELOG.md +148 -0
- package/README.md +38 -38
- package/bsv-gdaf.min.js +33 -33
- package/bsv-ltp.min.js +33 -33
- package/bsv-smartcontract.min.js +2 -2
- package/bsv-statuslist.min.js +1 -1
- package/bsv.bundle.js +33 -33
- package/bsv.d.ts +101 -22
- package/bsv.min.js +33 -33
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- 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/index.js +4 -1
- package/lib/gdaf/index.js +29 -6
- package/lib/gdaf/smartledger-anchor.js +31 -1
- package/lib/privatekey.js +59 -10
- package/lib/smart_contract/authorizers.js +8 -0
- package/lib/smart_contract/index.js +4 -2
- package/lib/statuslist/index.js +11 -3
- package/package.json +12 -1
- package/test/gdaf/anchor_no_key_leak.js +88 -0
- package/test/privatekey.js +79 -2
- package/test/types/dts_drift.js +23 -1
- package/test/types/surface_honesty.js +102 -0
- package/version.js +1 -1
|
@@ -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
|
+
})
|
package/test/privatekey.js
CHANGED
|
@@ -205,7 +205,13 @@ describe('PrivateKey', function () {
|
|
|
205
205
|
network: 'livenet'
|
|
206
206
|
})
|
|
207
207
|
var key = PrivateKey.fromObject(JSON.parse(json))
|
|
208
|
-
|
|
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
|
-
|
|
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
|
})
|
package/test/types/dts_drift.js
CHANGED
|
@@ -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
|
-
|
|
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