@smartledger/bsv 9.0.0 → 9.1.1
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 +164 -0
- package/README.md +152 -1041
- package/STABILITY.md +134 -0
- package/bsv-gdaf.min.js +53 -48
- package/bsv-ltp.min.js +23 -18
- package/bsv-smartcontract.min.js +18 -18
- package/bsv.bundle.js +53 -48
- package/bsv.min.js +53 -48
- 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/docs/proposals/10.0.0-package-split.md +131 -0
- package/index.js +5 -0
- package/lib/block/merkleblock.js +53 -9
- package/lib/hdprivatekey.js +15 -0
- package/lib/script/interpreter.js +109 -1
- package/lib/smart_contract/builder.js +14 -0
- package/lib/util/deprecate.js +159 -0
- package/package.json +19 -82
- package/version.js +1 -1
- package/.mocharc.json +0 -5
- package/test/address.js +0 -629
- package/test/block/block.js +0 -239
- package/test/block/blockheader.js +0 -270
- package/test/block/merkleblock.js +0 -207
- package/test/build/bundle_crypto_shim.js +0 -104
- package/test/build/bundle_externals.js +0 -125
- package/test/build/bundle_smoke.js +0 -55
- package/test/build/esbuild_main.js +0 -65
- package/test/build/esm_wrapper.js +0 -97
- package/test/build/exports_resolution.js +0 -84
- package/test/build/version_sync.js +0 -14
- package/test/cli/smoke.js +0 -261
- package/test/consensus/base58-vectors.js +0 -120
- package/test/consensus/sv-script-vectors.js +0 -81
- package/test/consensus/sv-sighash-vectors.js +0 -51
- package/test/consensus/sv-tx-vectors.js +0 -48
- package/test/credentials-test.js +0 -332
- package/test/crypto/backend_selection.js +0 -126
- package/test/crypto/bn.js +0 -168
- package/test/crypto/ecdsa.js +0 -426
- package/test/crypto/elliptic-fixed.js +0 -61
- package/test/crypto/hash.browser.js +0 -121
- package/test/crypto/hash.js +0 -122
- package/test/crypto/point.js +0 -193
- package/test/crypto/random.js +0 -105
- package/test/crypto/security.js +0 -176
- package/test/crypto/shamir.js +0 -177
- package/test/crypto/shamir_rngcap.js +0 -29
- package/test/crypto/signature.js +0 -399
- package/test/data/bip69.json +0 -215
- package/test/data/bitcoin-sv/README.md +0 -170
- package/test/data/bitcoin-sv/base58_encode_decode.json +0 -14
- package/test/data/bitcoin-sv/base58_keys_invalid.json +0 -152
- package/test/data/bitcoin-sv/base58_keys_valid.json +0 -452
- package/test/data/bitcoin-sv/script_tests.json +0 -2591
- package/test/data/bitcoin-sv/sighash.json +0 -1003
- package/test/data/bitcoin-sv/tx_invalid.json +0 -285
- package/test/data/bitcoin-sv/tx_valid.json +0 -367
- package/test/data/bitcoind/base58_keys_invalid.json +0 -152
- package/test/data/bitcoind/base58_keys_valid.json +0 -452
- package/test/data/bitcoind/blocks.json +0 -27
- package/test/data/bitcoind/script_tests.json +0 -2244
- package/test/data/bitcoind/sig_canonical.json +0 -7
- package/test/data/bitcoind/sig_noncanonical.json +0 -22
- package/test/data/bitcoind/tx_invalid.json +0 -177
- package/test/data/bitcoind/tx_valid.json +0 -224
- package/test/data/blk86756-testnet.dat +0 -0
- package/test/data/blk86756-testnet.js +0 -12
- package/test/data/blk86756-testnet.json +0 -684
- package/test/data/brc220-batch-vector.json +0 -129
- package/test/data/ecdsa.json +0 -230
- package/test/data/merkleblocks.js +0 -486
- package/test/data/messages.json +0 -22
- package/test/data/sighash.json +0 -1004
- package/test/data/tx_creation.json +0 -85
- package/test/didweb/relationships.js +0 -144
- package/test/ecies/bitcore-ecies.js +0 -178
- package/test/ecies/electrum-ecies.js +0 -206
- package/test/encoding/base58.js +0 -131
- package/test/encoding/base58check.js +0 -145
- package/test/encoding/bufferreader.js +0 -328
- package/test/encoding/bufferwriter.js +0 -160
- package/test/encoding/varint.js +0 -104
- package/test/gdaf/anchor_no_key_leak.js +0 -146
- package/test/gdaf/anchor_spv.js +0 -163
- package/test/gdaf/canonicalization.js +0 -106
- package/test/gdaf/canonicalize.js +0 -140
- package/test/gdaf/zk_prover.js +0 -204
- package/test/hdkeys.js +0 -365
- package/test/hdprivatekey.js +0 -339
- package/test/hdpublickey.js +0 -293
- package/test/index.js +0 -16
- package/test/ltp/ids.js +0 -74
- package/test/ltp/right.js +0 -198
- package/test/ltp/verify_failclosed.js +0 -134
- package/test/message/message.js +0 -190
- package/test/mnemonic/data/fixtures.json +0 -300
- package/test/mnemonic/mnemonic.js +0 -277
- package/test/mnemonic/mocha.opts +0 -1
- package/test/mnemonic/pbkdf2.test.js +0 -43
- package/test/networks.js +0 -207
- package/test/notaryhash/batch_leaf.js +0 -140
- package/test/notaryhash/batch_vector.js +0 -282
- package/test/notaryhash/certificate.js +0 -249
- package/test/notaryhash/encoding.js +0 -186
- package/test/notaryhash/interop.js +0 -112
- package/test/notaryhash/merkle.js +0 -181
- package/test/notaryhash/script.js +0 -270
- package/test/notaryhash/verify.js +0 -342
- package/test/opcode.js +0 -186
- package/test/ordinals/bsv20.js +0 -337
- package/test/ordinals/inscription.js +0 -329
- package/test/ordinals/ordlock.js +0 -567
- package/test/privatekey.js +0 -540
- package/test/publickey.js +0 -411
- package/test/regressions.js +0 -215
- package/test/script/chronicle.js +0 -543
- package/test/script/defaults.js +0 -160
- package/test/script/genesis_limits.js +0 -203
- package/test/script/interpreter.js +0 -776
- package/test/script/script.js +0 -1259
- package/test/script/string_ops.js +0 -88
- package/test/security/fail_closed_contracts.js +0 -104
- package/test/security/threat_model_coverage.js +0 -31
- package/test/smart_contract/covenants.js +0 -207
- package/test/smart_contract/dsl_debugger.js +0 -92
- package/test/smart_contract/extract_field.js +0 -61
- package/test/smart_contract/nonenforcing_guard.js +0 -44
- package/test/smart_contract/ordinal_transfer.js +0 -71
- package/test/smart_contract/preimage.js +0 -98
- package/test/smart_contract/sighash_marketplace.js +0 -98
- package/test/smart_contract/token_generalized.js +0 -175
- package/test/spv/headerchain.js +0 -86
- package/test/spv/merkleproof.js +0 -133
- package/test/statuslist/failclosed.js +0 -78
- package/test/transaction/deserialize.js +0 -33
- package/test/transaction/input/input.js +0 -92
- package/test/transaction/input/multisig.js +0 -174
- package/test/transaction/input/multisigscripthash.js +0 -111
- package/test/transaction/input/publickey.js +0 -68
- package/test/transaction/input/publickeyhash.js +0 -59
- package/test/transaction/output.js +0 -185
- package/test/transaction/sighash.js +0 -91
- package/test/transaction/signature.js +0 -127
- package/test/transaction/transaction.js +0 -1299
- package/test/transaction/unspentoutput.js +0 -97
- package/test/types/dts_drift.js +0 -124
- package/test/types/surface_honesty.js +0 -102
- package/test/util/id.js +0 -72
- package/test/util/js.js +0 -76
- package/test/util/preconditions.js +0 -79
- package/test/vcjwt/interop.js +0 -126
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
/* global describe, it */
|
|
4
|
-
|
|
5
|
-
// BRC-220 certificate. This is validity check 2 of the three the spec requires — proof
|
|
6
|
-
// integrity — plus the object a verifier is actually handed.
|
|
7
|
-
//
|
|
8
|
-
// The tests below concentrate on three things the spec makes explicit and that would fail
|
|
9
|
-
// silently if got wrong: the certificate carries FULL blobs even when the chain carries
|
|
10
|
-
// digests; the SPV envelope never disturbs proofHash; and proofHash is over the binary
|
|
11
|
-
// proof bytes, NOT over the certificate JSON.
|
|
12
|
-
|
|
13
|
-
require('chai').should()
|
|
14
|
-
var Certificate = require('../../lib/notaryhash/certificate')
|
|
15
|
-
var Encoding = require('../../lib/notaryhash/encoding')
|
|
16
|
-
var NS = require('../../lib/notaryhash/script')
|
|
17
|
-
var Hash = require('../../lib/crypto/hash')
|
|
18
|
-
var JCS = require('../../lib/util/jcs')
|
|
19
|
-
|
|
20
|
-
var PARAMS = {
|
|
21
|
-
mode: NS.MODE.FULL,
|
|
22
|
-
algorithm: 'ECDSA-secp256k1',
|
|
23
|
-
hashAlgorithm: 'SHA-256',
|
|
24
|
-
payloadHash: Buffer.alloc(32, 0x11),
|
|
25
|
-
publicKey: Buffer.alloc(33, 0x02),
|
|
26
|
-
signature: Buffer.alloc(64, 0x30),
|
|
27
|
-
createdAt: '2026-08-16T00:00:00.000Z',
|
|
28
|
-
anchor: { txid: 'ab'.repeat(32), blockHeight: 800000 }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
var SPV = {
|
|
32
|
-
rawTx: 'deadbeef',
|
|
33
|
-
blockHash: 'cc'.repeat(32),
|
|
34
|
-
blockHeight: 800000,
|
|
35
|
-
merkleProof: { index: 0, nodes: ['aa'.repeat(32), '*'] }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('BRC-220 certificate', function () {
|
|
39
|
-
describe('build', function () {
|
|
40
|
-
it('carries every field the spec requires', function () {
|
|
41
|
-
var cert = Certificate.build(PARAMS)
|
|
42
|
-
Certificate.REQUIRED_FIELDS.forEach(function (field) {
|
|
43
|
-
;(cert[field] === undefined).should.equal(false, 'missing ' + field)
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('defaults encoding to raw', function () {
|
|
48
|
-
Certificate.build(PARAMS).encoding.should.equal('raw')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('accepts der for Bitcoin-native signers', function () {
|
|
52
|
-
Certificate.build(Object.assign({}, PARAMS, { encoding: 'der' })).encoding.should.equal('der')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('rejects an unknown encoding rather than passing it through', function () {
|
|
56
|
-
;(function () {
|
|
57
|
-
Certificate.build(Object.assign({}, PARAMS, { encoding: 'base64' }))
|
|
58
|
-
}).should.throw(/encoding must be/)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('hex-encodes the byte fields', function () {
|
|
62
|
-
var cert = Certificate.build(PARAMS)
|
|
63
|
-
cert.payloadHash.should.equal(PARAMS.payloadHash.toString('hex'))
|
|
64
|
-
cert.publicKey.should.equal(PARAMS.publicKey.toString('hex'))
|
|
65
|
-
cert.signature.should.equal(PARAMS.signature.toString('hex'))
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// proofHash is computed, never accepted. A caller cannot hand in a value that
|
|
69
|
-
// disagrees with the fields beside it — which is precisely the artefact this protocol
|
|
70
|
-
// exists to make impossible.
|
|
71
|
-
it('computes proofHash rather than accepting one', function () {
|
|
72
|
-
var cert = Certificate.build(Object.assign({}, PARAMS, { proofHash: 'ff'.repeat(32) }))
|
|
73
|
-
cert.proofHash.should.not.equal('ff'.repeat(32))
|
|
74
|
-
Certificate.proofHashMatches(cert).should.equal(true)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('requires a merkle proof for batch mode', function () {
|
|
78
|
-
;(function () {
|
|
79
|
-
Certificate.build(Object.assign({}, PARAMS, { mode: NS.MODE.BATCH }))
|
|
80
|
-
}).should.throw(/merkle/)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('carries the merkle proof when batched', function () {
|
|
84
|
-
var cert = Certificate.build(Object.assign({}, PARAMS, {
|
|
85
|
-
mode: NS.MODE.BATCH,
|
|
86
|
-
merkle: { root: 'aa'.repeat(32), leafIndex: 2, leafCount: 8, path: [] }
|
|
87
|
-
}))
|
|
88
|
-
cert.merkle.leafIndex.should.equal(2)
|
|
89
|
-
cert.merkle.leafCount.should.equal(8)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
// Hybrid puts only digests on chain; the certificate keeps the originals. That
|
|
93
|
-
// asymmetry is the whole point of the mode.
|
|
94
|
-
it('keeps the FULL key and signature in hybrid mode', function () {
|
|
95
|
-
var cert = Certificate.build(Object.assign({}, PARAMS, { mode: NS.MODE.HYBRID }))
|
|
96
|
-
cert.publicKey.should.equal(PARAMS.publicKey.toString('hex'))
|
|
97
|
-
cert.signature.should.equal(PARAMS.signature.toString('hex'))
|
|
98
|
-
// What goes on chain is the digest of each — different values entirely.
|
|
99
|
-
var onChain = NS.parse(NS.build(Object.assign({}, PARAMS, { mode: NS.MODE.HYBRID, proofHash: Buffer.alloc(32) })))
|
|
100
|
-
onChain.publicKeyHash.toString('hex').should.equal(Hash.sha256(PARAMS.publicKey).toString('hex'))
|
|
101
|
-
onChain.publicKeyHash.toString('hex').should.not.equal(cert.publicKey)
|
|
102
|
-
})
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
describe('proof integrity (validity check 2)', function () {
|
|
106
|
-
it('matches for a well-formed certificate', function () {
|
|
107
|
-
Certificate.proofHashMatches(Certificate.build(PARAMS)).should.equal(true)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// Each field is inside the canonical proof bytes, so tampering with any of them must
|
|
111
|
-
// be detected. This is the certificate-level counterpart of the encoding module's
|
|
112
|
-
// coverage test.
|
|
113
|
-
it('detects tampering with any covered field', function () {
|
|
114
|
-
var cert = Certificate.build(PARAMS)
|
|
115
|
-
var tampers = {
|
|
116
|
-
algorithm: 'ML-DSA-65',
|
|
117
|
-
hashAlgorithm: 'SHA-512',
|
|
118
|
-
payloadHash: 'ff'.repeat(32),
|
|
119
|
-
publicKey: '03' + '11'.repeat(32),
|
|
120
|
-
signature: 'aa'.repeat(64),
|
|
121
|
-
createdAt: '2026-08-17T00:00:00.000Z'
|
|
122
|
-
}
|
|
123
|
-
Object.keys(tampers).forEach(function (field) {
|
|
124
|
-
var bad = Object.assign({}, cert)
|
|
125
|
-
bad[field] = tampers[field]
|
|
126
|
-
Certificate.proofHashMatches(bad)
|
|
127
|
-
.should.equal(false, 'tampering with ' + field + ' was not detected')
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Fields NOT in the canonical proof bytes must not affect it — otherwise the SPV
|
|
132
|
-
// envelope could not be additive.
|
|
133
|
-
it('is unaffected by fields outside the canonical bytes', function () {
|
|
134
|
-
var cert = Certificate.build(PARAMS)
|
|
135
|
-
var moved = Object.assign({}, cert, {
|
|
136
|
-
anchor: { txid: 'ff'.repeat(32), blockHeight: 999999 },
|
|
137
|
-
mode: NS.MODE.HYBRID
|
|
138
|
-
})
|
|
139
|
-
Certificate.proofHashMatches(moved).should.equal(true)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('returns a strict boolean, never a truthy object', function () {
|
|
143
|
-
Certificate.proofHashMatches(Certificate.build(PARAMS)).should.be.a('boolean')
|
|
144
|
-
Certificate.proofHashMatches(null).should.equal(false)
|
|
145
|
-
Certificate.proofHashMatches({}).should.equal(false)
|
|
146
|
-
Certificate.proofHashMatches({ proofHash: 'nonsense' }).should.equal(false)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
// The hex fields decode to bytes before hashing. Hashing the strings would be twice
|
|
150
|
-
// as many bytes and produce something that still looks like a hash.
|
|
151
|
-
it('hashes the decoded bytes, not the hex strings', function () {
|
|
152
|
-
var cert = Certificate.build(PARAMS)
|
|
153
|
-
var overHex = Hash.sha256(Encoding.canonicalBytes({
|
|
154
|
-
algorithm: cert.algorithm,
|
|
155
|
-
hashAlgorithm: cert.hashAlgorithm,
|
|
156
|
-
payloadHash: Buffer.from(cert.payloadHash, 'utf8'), // the STRING, wrongly
|
|
157
|
-
publicKey: Buffer.from(cert.publicKey, 'hex'),
|
|
158
|
-
signature: Buffer.from(cert.signature, 'hex'),
|
|
159
|
-
createdAtUnix: Encoding.toUnixSeconds(cert.createdAt)
|
|
160
|
-
})).toString('hex')
|
|
161
|
-
overHex.should.not.equal(cert.proofHash)
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
describe('SPV envelope', function () {
|
|
166
|
-
// The spec's explicit guarantee. If it ever broke, every previously issued
|
|
167
|
-
// certificate would become unverifiable.
|
|
168
|
-
it('never changes proofHash', function () {
|
|
169
|
-
var cert = Certificate.build(PARAMS)
|
|
170
|
-
var withSPV = Certificate.attachSPV(cert, SPV)
|
|
171
|
-
withSPV.proofHash.should.equal(cert.proofHash)
|
|
172
|
-
Certificate.proofHashMatches(withSPV).should.equal(true)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('does not mutate the certificate it was given', function () {
|
|
176
|
-
var cert = Certificate.build(PARAMS)
|
|
177
|
-
Certificate.attachSPV(cert, SPV)
|
|
178
|
-
;(cert.spv === undefined).should.equal(true)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('defaults format to TSC', function () {
|
|
182
|
-
Certificate.attachSPV(Certificate.build(PARAMS), SPV).spv.format.should.equal('TSC')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('keeps an explicit format, for BUMP or BEEF', function () {
|
|
186
|
-
var withSPV = Certificate.attachSPV(Certificate.build(PARAMS),
|
|
187
|
-
Object.assign({}, SPV, { format: 'BUMP' }))
|
|
188
|
-
withSPV.spv.format.should.equal('BUMP')
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
describe('validateShape', function () {
|
|
193
|
-
it('passes a well-formed certificate', function () {
|
|
194
|
-
Certificate.validateShape(Certificate.build(PARAMS)).should.deep.equal([])
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('names every missing required field', function () {
|
|
198
|
-
var problems = Certificate.validateShape({})
|
|
199
|
-
problems.length.should.equal(Certificate.REQUIRED_FIELDS.length)
|
|
200
|
-
problems[0].should.match(/missing required field/)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('rejects a foreign protocol or version', function () {
|
|
204
|
-
var cert = Certificate.build(PARAMS)
|
|
205
|
-
Certificate.validateShape(Object.assign({}, cert, { protocol: 'Other' }))
|
|
206
|
-
.should.include('protocol must be "NotaryHash"')
|
|
207
|
-
Certificate.validateShape(Object.assign({}, cert, { version: 2 }))
|
|
208
|
-
.should.include('unsupported version: 2')
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('rejects a hash field of the wrong length or that is not hex', function () {
|
|
212
|
-
var cert = Certificate.build(PARAMS)
|
|
213
|
-
Certificate.validateShape(Object.assign({}, cert, { payloadHash: 'ab' }))
|
|
214
|
-
.should.include('payloadHash must be 32 bytes (64 hex chars)')
|
|
215
|
-
Certificate.validateShape(Object.assign({}, cert, { proofHash: 'zz'.repeat(32) }))
|
|
216
|
-
.should.include('proofHash must be a hex string')
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Shape is not verification, and a test says so — otherwise it gets used as one.
|
|
220
|
-
it('says nothing about whether the proofHash is correct', function () {
|
|
221
|
-
var cert = Certificate.build(PARAMS)
|
|
222
|
-
var lying = Object.assign({}, cert, { proofHash: '00'.repeat(32) })
|
|
223
|
-
Certificate.validateShape(lying).should.deep.equal([])
|
|
224
|
-
Certificate.proofHashMatches(lying).should.equal(false)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
describe('canonicalize', function () {
|
|
229
|
-
it('is RFC 8785 over the certificate', function () {
|
|
230
|
-
var cert = Certificate.build(PARAMS)
|
|
231
|
-
Certificate.canonicalize(cert).should.equal(JCS.stringify(cert))
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('sorts keys, so transport order does not matter', function () {
|
|
235
|
-
var cert = Certificate.build(PARAMS)
|
|
236
|
-
var reordered = {}
|
|
237
|
-
Object.keys(cert).reverse().forEach(function (k) { reordered[k] = cert[k] })
|
|
238
|
-
Certificate.canonicalize(reordered).should.equal(Certificate.canonicalize(cert))
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// proofHash is over the length-prefixed BINARY proof bytes, not over this JSON.
|
|
242
|
-
// Confusing the two produces a value that looks like a proofHash and is not one.
|
|
243
|
-
it('is not what proofHash is computed over', function () {
|
|
244
|
-
var cert = Certificate.build(PARAMS)
|
|
245
|
-
Hash.sha256(Buffer.from(Certificate.canonicalize(cert), 'utf8')).toString('hex')
|
|
246
|
-
.should.not.equal(cert.proofHash)
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
})
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
/* global describe, it */
|
|
4
|
-
|
|
5
|
-
// BRC-220 canonical encoding. The property that matters is not that these bytes are
|
|
6
|
-
// stable for us — it is that another implementation reading the spec produces the same
|
|
7
|
-
// ones. So the tests assert the byte layout the spec dictates, field by field, rather
|
|
8
|
-
// than only pinning our own output.
|
|
9
|
-
//
|
|
10
|
-
// The failure this module is most exposed to is a proofHash that is self-consistent and
|
|
11
|
-
// wrong: it verifies against itself, every local test passes, and nobody finds out until
|
|
12
|
-
// a third party tries to verify a certificate. Several tests below exist only to make
|
|
13
|
-
// that shape impossible.
|
|
14
|
-
|
|
15
|
-
require('chai').should()
|
|
16
|
-
var Encoding = require('../../lib/notaryhash/encoding')
|
|
17
|
-
var Hash = require('../../lib/crypto/hash')
|
|
18
|
-
|
|
19
|
-
var FIELDS = {
|
|
20
|
-
algorithm: 'ECDSA-secp256k1',
|
|
21
|
-
hashAlgorithm: 'SHA-256',
|
|
22
|
-
payloadHash: Buffer.alloc(32, 0x11),
|
|
23
|
-
publicKey: Buffer.alloc(33, 0x02),
|
|
24
|
-
signature: Buffer.alloc(71, 0x30),
|
|
25
|
-
createdAtUnix: 1786838400
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('BRC-220 encoding', function () {
|
|
29
|
-
describe('lp()', function () {
|
|
30
|
-
it('is u32be(len) followed by the bytes', function () {
|
|
31
|
-
Encoding.lp(Buffer.from([0xaa, 0xbb])).toString('hex').should.equal('00000002aabb')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('encodes an empty value as a bare zero length', function () {
|
|
35
|
-
Encoding.lp(Buffer.alloc(0)).toString('hex').should.equal('00000000')
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('measures BYTES, not characters, for text', function () {
|
|
39
|
-
// 'é' is one character and two UTF-8 bytes. Using .length here would prefix 1,
|
|
40
|
-
// and every proof containing a non-ASCII algorithm name would be unverifiable.
|
|
41
|
-
Encoding.lp('é').toString('hex').should.equal('00000002c3a9')
|
|
42
|
-
Encoding.lp('abc').toString('hex').should.equal('00000003616263')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
// Length-prefixing exists to make field boundaries unambiguous. If it did not, these
|
|
46
|
-
// two field pairs would concatenate to identical bytes.
|
|
47
|
-
it('keeps ambiguous field splits distinct', function () {
|
|
48
|
-
var a = Buffer.concat([Encoding.lp('ab'), Encoding.lp('c')])
|
|
49
|
-
var b = Buffer.concat([Encoding.lp('a'), Encoding.lp('bc')])
|
|
50
|
-
a.toString('hex').should.not.equal(b.toString('hex'))
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
describe('u64be()', function () {
|
|
55
|
-
it('is 8 bytes, big-endian', function () {
|
|
56
|
-
Encoding.u64be(1).toString('hex').should.equal('0000000000000001')
|
|
57
|
-
Encoding.u64be(1786838400).toString('hex').should.equal('000000006a80fd80')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// Date.now() is milliseconds. A caller who forgets to divide gets a timestamp about
|
|
61
|
-
// a thousand years in the future that still encodes cleanly, so the guard is on the
|
|
62
|
-
// input rather than the output.
|
|
63
|
-
it('rejects a fractional value, which is what a millisecond mistake looks like', function () {
|
|
64
|
-
;(function () { Encoding.u64be(1786838400.5) }).should.throw(/whole seconds/)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('rejects non-finite and negative values', function () {
|
|
68
|
-
;(function () { Encoding.u64be(NaN) }).should.throw(/finite/)
|
|
69
|
-
;(function () { Encoding.u64be(-1) }).should.throw(/negative/)
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('toUnixSeconds()', function () {
|
|
74
|
-
it('converts an ISO 8601 string to whole seconds', function () {
|
|
75
|
-
Encoding.toUnixSeconds('2026-08-16T00:00:00.000Z').should.equal(1786838400)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// Certificates carry millisecond precision in createdAt; the canonical bytes do not.
|
|
79
|
-
// Two implementations must agree on the fractional part, and truncation is the only
|
|
80
|
-
// choice that never moves a timestamp forward.
|
|
81
|
-
it('truncates rather than rounds', function () {
|
|
82
|
-
Encoding.toUnixSeconds('2026-08-16T00:00:00.999Z').should.equal(1786838400)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('rejects an unparseable date', function () {
|
|
86
|
-
;(function () { Encoding.toUnixSeconds('not-a-date') }).should.throw(/not a valid date/)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
describe('canonicalBytes()', function () {
|
|
91
|
-
// The layout, asserted against a hand-built expectation rather than against the
|
|
92
|
-
// function's own output — otherwise this only proves the function is deterministic.
|
|
93
|
-
it('lays the fields out exactly as the spec specifies', function () {
|
|
94
|
-
var expected = Buffer.concat([
|
|
95
|
-
Encoding.lp('NotaryHash/1.0'),
|
|
96
|
-
Buffer.from([1]),
|
|
97
|
-
Encoding.lp('ECDSA-secp256k1'),
|
|
98
|
-
Encoding.lp('SHA-256'),
|
|
99
|
-
Encoding.lp(FIELDS.payloadHash),
|
|
100
|
-
Encoding.lp(FIELDS.publicKey),
|
|
101
|
-
Encoding.lp(FIELDS.signature),
|
|
102
|
-
Encoding.u64be(FIELDS.createdAtUnix)
|
|
103
|
-
])
|
|
104
|
-
Encoding.canonicalBytes(FIELDS).toString('hex').should.equal(expected.toString('hex'))
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('opens with the protocol prefix and the version byte', function () {
|
|
108
|
-
var bytes = Encoding.canonicalBytes(FIELDS)
|
|
109
|
-
bytes.slice(0, 4).readUInt32BE(0).should.equal(14) // len('NotaryHash/1.0')
|
|
110
|
-
bytes.slice(4, 18).toString('utf8').should.equal('NotaryHash/1.0')
|
|
111
|
-
bytes[18].should.equal(1) // u8(version)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('ends with the timestamp', function () {
|
|
115
|
-
var bytes = Encoding.canonicalBytes(FIELDS)
|
|
116
|
-
bytes.slice(-8).toString('hex').should.equal(Encoding.u64be(FIELDS.createdAtUnix).toString('hex'))
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
// THE test for this module. GDAF once signed a serialization that silently omitted
|
|
120
|
-
// credentialSubject, so a forger could rewrite the claims without breaking the proof.
|
|
121
|
-
// Every field must move the hash, or it is not covered by it.
|
|
122
|
-
it('covers every field — changing any one changes the hash', function () {
|
|
123
|
-
var base = Encoding.proofHash(FIELDS).toString('hex')
|
|
124
|
-
var mutations = {
|
|
125
|
-
algorithm: 'ML-DSA-65',
|
|
126
|
-
hashAlgorithm: 'SHA-512',
|
|
127
|
-
payloadHash: Buffer.alloc(32, 0x12),
|
|
128
|
-
publicKey: Buffer.alloc(33, 0x03),
|
|
129
|
-
signature: Buffer.alloc(71, 0x31),
|
|
130
|
-
createdAtUnix: FIELDS.createdAtUnix + 1
|
|
131
|
-
}
|
|
132
|
-
Object.keys(mutations).forEach(function (field) {
|
|
133
|
-
var altered = Object.assign({}, FIELDS)
|
|
134
|
-
altered[field] = mutations[field]
|
|
135
|
-
Encoding.proofHash(altered).toString('hex')
|
|
136
|
-
.should.not.equal(base, field + ' does not affect proofHash, so it is not covered by it')
|
|
137
|
-
})
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// Hex is what certificates carry, and hex is twice as long as the bytes it encodes.
|
|
141
|
-
// Accepting it here would produce a proof that verifies locally and nowhere else.
|
|
142
|
-
it('refuses a hex string where raw bytes are required', function () {
|
|
143
|
-
var withHex = Object.assign({}, FIELDS, { payloadHash: FIELDS.payloadHash.toString('hex') })
|
|
144
|
-
;(function () { Encoding.canonicalBytes(withHex) }).should.throw(/must be a Buffer/)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('names the offending field and points at the hex trap', function () {
|
|
148
|
-
var withHex = Object.assign({}, FIELDS, { signature: FIELDS.signature.toString('hex') })
|
|
149
|
-
;(function () { Encoding.canonicalBytes(withHex) }).should.throw(/signature.*hex/)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('requires the string fields to be present and non-empty', function () {
|
|
153
|
-
;(function () {
|
|
154
|
-
Encoding.canonicalBytes(Object.assign({}, FIELDS, { algorithm: '' }))
|
|
155
|
-
}).should.throw(/algorithm/)
|
|
156
|
-
;(function () {
|
|
157
|
-
Encoding.canonicalBytes(Object.assign({}, FIELDS, { hashAlgorithm: undefined }))
|
|
158
|
-
}).should.throw(/hashAlgorithm/)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('handles a post-quantum-sized signature', function () {
|
|
162
|
-
// SLH-DSA signatures run to tens of kilobytes; the u32be length prefix must carry
|
|
163
|
-
// them without truncation.
|
|
164
|
-
var big = Object.assign({}, FIELDS, { signature: Buffer.alloc(49856, 0x7f) })
|
|
165
|
-
var bytes = Encoding.canonicalBytes(big)
|
|
166
|
-
var sigLenOffset = bytes.length - 8 - 49856 - 4
|
|
167
|
-
bytes.readUInt32BE(sigLenOffset).should.equal(49856)
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('proofHash()', function () {
|
|
172
|
-
it('is SHA-256 of the canonical bytes', function () {
|
|
173
|
-
Encoding.proofHash(FIELDS).toString('hex')
|
|
174
|
-
.should.equal(Hash.sha256(Encoding.canonicalBytes(FIELDS)).toString('hex'))
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('is 32 bytes', function () {
|
|
178
|
-
Encoding.proofHash(FIELDS).length.should.equal(32)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('is deterministic', function () {
|
|
182
|
-
Encoding.proofHash(FIELDS).toString('hex')
|
|
183
|
-
.should.equal(Encoding.proofHash(Object.assign({}, FIELDS)).toString('hex'))
|
|
184
|
-
})
|
|
185
|
-
})
|
|
186
|
-
})
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
/* global describe, it */
|
|
4
|
-
|
|
5
|
-
// BRC-220 signature interoperability.
|
|
6
|
-
//
|
|
7
|
-
// Every other test in this directory signs with `lib/crypto/ecdsa.js` and verifies
|
|
8
|
-
// through `lib/notaryhash/suites.js`, which also uses `lib/crypto/ecdsa.js`. That
|
|
9
|
-
// proves the module agrees with itself. It cannot prove the module agrees with the
|
|
10
|
-
// spec, and in 8.3.0 it did not: the suite set `endian: 'little'` before verifying,
|
|
11
|
-
// so it rejected any signature produced the way BRC-220 describes and accepted only
|
|
12
|
-
// signatures made with bsv's own byte-reversed convention. Twenty-odd passing tests
|
|
13
|
-
// said nothing about it, because they all signed the same wrong way.
|
|
14
|
-
//
|
|
15
|
-
// So this file verifies against `@noble/curves` instead — a separate implementation
|
|
16
|
-
// that shares no verification code with ours. It is the only test here that can fail
|
|
17
|
-
// when our code and our tests are wrong together.
|
|
18
|
-
//
|
|
19
|
-
// One trap worth naming, because it cost real time when this was diagnosed: noble v2
|
|
20
|
-
// PREHASHES by default. `secp256k1.sign(digest, key)` signs `sha256(digest)`, and
|
|
21
|
-
// `secp256k1.verify` prehashes to match, so noble looks self-consistent while
|
|
22
|
-
// disagreeing with everyone. BRC-220 signs the payloadHash itself, so every call
|
|
23
|
-
// below passes `{ prehash: false }`. Omit it and these tests compare the wrong things
|
|
24
|
-
// and "pass" for the wrong reason.
|
|
25
|
-
|
|
26
|
-
require('chai').should()
|
|
27
|
-
var bsv = require('../..')
|
|
28
|
-
var Suites = require('../../lib/notaryhash/suites')
|
|
29
|
-
var Hash = require('../../lib/crypto/hash')
|
|
30
|
-
var { secp256k1 } = require('@noble/curves/secp256k1.js')
|
|
31
|
-
|
|
32
|
-
var PAYLOAD_HASH = Hash.sha256(Buffer.from('the document nobody sees'))
|
|
33
|
-
|
|
34
|
-
// A fixed key, so a failure is reproducible rather than one-in-N flaky.
|
|
35
|
-
var SECRET = Buffer.from('c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee01', 'hex')
|
|
36
|
-
var PUBLIC_KEY = Buffer.from(secp256k1.getPublicKey(SECRET, true))
|
|
37
|
-
|
|
38
|
-
function rawSig (sig) {
|
|
39
|
-
return Buffer.concat([
|
|
40
|
-
sig.r.toArrayLike(Buffer, 'be', 32),
|
|
41
|
-
sig.s.toArrayLike(Buffer, 'be', 32)
|
|
42
|
-
])
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe('BRC-220 signature interop', function () {
|
|
46
|
-
it('agrees with @noble/curves on the public key for a given secret', function () {
|
|
47
|
-
// If this fails nothing below means anything — the two libraries would be
|
|
48
|
-
// signing under different keys.
|
|
49
|
-
var ours = bsv.PrivateKey.fromBuffer(SECRET).toPublicKey().toBuffer()
|
|
50
|
-
ours.toString('hex').should.equal(PUBLIC_KEY.toString('hex'))
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('ACCEPTS a signature made the way the spec describes', function () {
|
|
54
|
-
// This is the regression. Under 8.3.0 this assertion fails.
|
|
55
|
-
var conformant = Buffer.from(secp256k1.sign(PAYLOAD_HASH, SECRET, { prehash: false }))
|
|
56
|
-
Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, conformant, PUBLIC_KEY)
|
|
57
|
-
.should.equal(true, 'a conformant BRC-220 signature must verify')
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('emits signatures an independent implementation accepts', function () {
|
|
61
|
-
var key = bsv.PrivateKey.fromBuffer(SECRET)
|
|
62
|
-
var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: PAYLOAD_HASH, privkey: key })
|
|
63
|
-
ecdsa.sign()
|
|
64
|
-
secp256k1.verify(rawSig(ecdsa.sig), PAYLOAD_HASH, PUBLIC_KEY, { prehash: false })
|
|
65
|
-
.should.equal(true, 'our signature must verify under @noble/curves')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('REJECTS a signature over the byte-reversed digest', function () {
|
|
69
|
-
// The 8.3.0 convention. It must now fail: accepting both would mean two valid
|
|
70
|
-
// signatures exist for one signing act, and both are inside proofHash.
|
|
71
|
-
var key = bsv.PrivateKey.fromBuffer(SECRET)
|
|
72
|
-
var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: PAYLOAD_HASH, endian: 'little', privkey: key })
|
|
73
|
-
ecdsa.sign()
|
|
74
|
-
Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, rawSig(ecdsa.sig), PUBLIC_KEY)
|
|
75
|
-
.should.equal(false, 'the pre-8.3.1 little-endian convention must not verify')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('round-trips both ways over many payloads, not just one', function () {
|
|
79
|
-
// A single vector can pass by coincidence — the reversal of a digest whose bytes
|
|
80
|
-
// happen to be near-palindromic still differs, but a lucky scalar could mask a
|
|
81
|
-
// subtler bug. Twenty payloads with distinct digests makes that implausible.
|
|
82
|
-
var key = bsv.PrivateKey.fromBuffer(SECRET)
|
|
83
|
-
for (var i = 0; i < 20; i++) {
|
|
84
|
-
var h = Hash.sha256(Buffer.from('payload ' + i))
|
|
85
|
-
|
|
86
|
-
var theirs = Buffer.from(secp256k1.sign(h, SECRET, { prehash: false }))
|
|
87
|
-
Suites.verify('ECDSA-secp256k1', h, theirs, PUBLIC_KEY)
|
|
88
|
-
.should.equal(true, 'noble sig ' + i + ' rejected by our suite')
|
|
89
|
-
|
|
90
|
-
var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: h, privkey: key })
|
|
91
|
-
ecdsa.sign()
|
|
92
|
-
secp256k1.verify(rawSig(ecdsa.sig), h, PUBLIC_KEY, { prehash: false })
|
|
93
|
-
.should.equal(true, 'our sig ' + i + ' rejected by noble')
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('still rejects a signature over a DIFFERENT payload', function () {
|
|
98
|
-
// Guards against the fix degenerating into "accept anything". A verifier that
|
|
99
|
-
// returns true unconditionally would pass every test above.
|
|
100
|
-
var other = Hash.sha256(Buffer.from('a different document'))
|
|
101
|
-
var sig = Buffer.from(secp256k1.sign(other, SECRET, { prehash: false }))
|
|
102
|
-
Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, sig, PUBLIC_KEY)
|
|
103
|
-
.should.equal(false)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('still rejects a signature from a DIFFERENT key', function () {
|
|
107
|
-
var otherSecret = Buffer.from('a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce001', 'hex')
|
|
108
|
-
var sig = Buffer.from(secp256k1.sign(PAYLOAD_HASH, otherSecret, { prehash: false }))
|
|
109
|
-
Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, sig, PUBLIC_KEY)
|
|
110
|
-
.should.equal(false)
|
|
111
|
-
})
|
|
112
|
-
})
|