@smartledger/bsv 8.3.0 → 9.0.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +374 -222
  3. package/bsv-gdaf.min.js +49 -49
  4. package/bsv-script-helper.min.js +1 -1
  5. package/bsv-smartcontract.min.js +21 -21
  6. package/bsv.bundle.js +49 -49
  7. package/bsv.d.ts +0 -4
  8. package/bsv.min.js +49 -49
  9. package/docs/BRC220_BATCH_LEAF_AMENDMENT.md +194 -0
  10. package/docs/BRC220_PLAN.md +9 -0
  11. package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
  12. package/docs/README.md +2 -1
  13. package/docs/THREAT_MODEL.md +10 -2
  14. package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
  15. package/docs/getting-started/INSTALLATION.md +23 -23
  16. package/docs/getting-started/QUICK_START.md +7 -7
  17. package/docs/migration/FROM_BSV_1_5_6.md +5 -5
  18. package/index.js +10 -26
  19. package/lib/covenant/helpers.js +34 -35
  20. package/lib/covenant/pushtx.js +2 -2
  21. package/lib/custom-script-helper.js +0 -16
  22. package/lib/notaryhash/index.js +20 -0
  23. package/lib/notaryhash/merkle.js +8 -0
  24. package/lib/notaryhash/suites.js +17 -1
  25. package/lib/ordinals/ordlock.js +1 -1
  26. package/lib/smart_contract/debugger.js +2 -2
  27. package/lib/smart_contract/dsl.js +2 -2
  28. package/lib/smart_contract/index.js +0 -3
  29. package/lib/smart_contract/locks.js +1 -41
  30. package/lib/smart_contract/pels.js +1 -1
  31. package/lib/smart_contract/token.js +1 -1
  32. package/package.json +3 -2
  33. package/test/build/esm_wrapper.js +32 -8
  34. package/test/data/brc220-batch-vector.json +129 -0
  35. package/test/notaryhash/batch_leaf.js +140 -0
  36. package/test/notaryhash/batch_vector.js +282 -0
  37. package/test/notaryhash/interop.js +112 -0
  38. package/test/notaryhash/verify.js +5 -2
  39. package/test/ordinals/inscription.js +24 -14
  40. package/test/ordinals/ordlock.js +0 -46
  41. package/test/smart_contract/covenants.js +0 -38
  42. package/test/smart_contract/dsl_debugger.js +0 -9
  43. package/test/smart_contract/ordinal_transfer.js +0 -10
  44. package/test/smart_contract/sighash_marketplace.js +0 -10
  45. package/test/smart_contract/token_generalized.js +0 -10
  46. package/tools/gen-brc220-batch-vector.js +205 -0
  47. package/version.js +1 -1
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // BRC-220 batch mode: what is in a Merkle leaf.
6
+ //
7
+ // The spec writes `leaf = SHA256(0x00 ‖ d)` and never binds `d`. That is RFC 6962's own
8
+ // generic notation for the construction, quoted to identify WHICH tree — domain
9
+ // separation, the power-of-two split, no duplicated last leaf — not to define the leaf's
10
+ // contents. `canonicalBytes` appears once in the whole document, in the proofHash
11
+ // definition, and nowhere in the batch text.
12
+ //
13
+ // This library reads `d` as proofHash. The alternative reading, d = canonicalBytes, is
14
+ // equally sound cryptographically and produces a DIFFERENT root — so the two do not
15
+ // interoperate, and the failure is silent: the fold just does not reach the root.
16
+ //
17
+ // These tests pin the choice, and pin the divergence too. A comment saying "we use
18
+ // proofHash" would not fail if someone changed it; this does.
19
+ //
20
+ // Reasoning and proposed spec text: docs/BRC220_BATCH_LEAF_AMENDMENT.md
21
+
22
+ require('chai').should()
23
+ var NotaryHash = require('../../lib/notaryhash')
24
+ var Merkle = require('../../lib/notaryhash/merkle')
25
+ var Hash = require('../../lib/crypto/hash')
26
+ var NotaryScript = require('../../lib/notaryhash/script')
27
+
28
+ // Four fixed proofHash values, standing in for a four-proof batch.
29
+ var PROOF_HASHES = [
30
+ '00'.repeat(31) + '01',
31
+ '00'.repeat(31) + '02',
32
+ 'ab'.repeat(32),
33
+ 'ff'.repeat(32)
34
+ ].map(function (h) { return Buffer.from(h, 'hex') })
35
+
36
+ function batchCertificate (index, leaves) {
37
+ return {
38
+ mode: NotaryScript.MODE.BATCH,
39
+ proofHash: leaves[index].toString('hex'),
40
+ merkle: {
41
+ root: Merkle.root(leaves).toString('hex'),
42
+ leafIndex: index,
43
+ leafCount: leaves.length,
44
+ path: Merkle.path(leaves, index).map(function (n) { return n.toString('hex') })
45
+ }
46
+ }
47
+ }
48
+
49
+ describe('BRC-220 batch leaf', function () {
50
+ describe('the leaf datum is proofHash', function () {
51
+ it('a leaf is SHA-256(0x00 || proofHash)', function () {
52
+ var proofHash = PROOF_HASHES[0]
53
+ Merkle.hashLeaf(proofHash).toString('hex').should.equal(
54
+ Hash.sha256(Buffer.concat([Buffer.from([0x00]), proofHash])).toString('hex')
55
+ )
56
+ })
57
+
58
+ // Stated the long way round, because this is the identity the spec leaves open.
59
+ it('is therefore SHA-256(0x00 || SHA-256(canonicalBytes))', function () {
60
+ var canonicalBytes = Buffer.from('any canonical proof bytes at all')
61
+ var proofHash = Hash.sha256(canonicalBytes)
62
+ Merkle.hashLeaf(proofHash).toString('hex').should.equal(
63
+ Hash.sha256(Buffer.concat([
64
+ Buffer.from([0x00]), Hash.sha256(canonicalBytes)
65
+ ])).toString('hex')
66
+ )
67
+ })
68
+
69
+ it('verifies inclusion for every leaf of a four-proof batch', function () {
70
+ for (var i = 0; i < PROOF_HASHES.length; i++) {
71
+ var report = NotaryHash.verifyBatchInclusion(batchCertificate(i, PROOF_HASHES))
72
+ report.valid.should.equal(true, 'leaf ' + i + ' should verify')
73
+ report.errors.should.deep.equal([])
74
+ }
75
+ })
76
+ })
77
+
78
+ describe('the two readings of `d` are not interchangeable', function () {
79
+ var canonicalBytesSet = [
80
+ Buffer.from('proof one canonical bytes'),
81
+ Buffer.from('proof two canonical bytes'),
82
+ Buffer.from('proof three canonical bytes'),
83
+ Buffer.from('proof four canonical bytes')
84
+ ]
85
+ var proofHashes = canonicalBytesSet.map(function (b) { return Hash.sha256(b) })
86
+
87
+ it('produces a different root under each reading', function () {
88
+ Merkle.root(proofHashes).toString('hex')
89
+ .should.not.equal(Merkle.root(canonicalBytesSet).toString('hex'))
90
+ })
91
+
92
+ // The interoperability failure, made concrete. This is what a batch built by an
93
+ // implementation using the other reading would look like to this verifier.
94
+ it('REJECTS a certificate whose tree was built over canonicalBytes', function () {
95
+ var certificate = {
96
+ mode: NotaryScript.MODE.BATCH,
97
+ proofHash: proofHashes[1].toString('hex'),
98
+ merkle: {
99
+ // Root and path from the canonicalBytes tree — the other reading.
100
+ root: Merkle.root(canonicalBytesSet).toString('hex'),
101
+ leafIndex: 1,
102
+ leafCount: 4,
103
+ path: Merkle.path(canonicalBytesSet, 1).map(function (n) { return n.toString('hex') })
104
+ }
105
+ }
106
+ var report = NotaryHash.verifyBatchInclusion(certificate)
107
+ report.valid.should.equal(false)
108
+ report.errors.should.deep.equal(['merkle inclusion proof does not fold to the batch root'])
109
+ })
110
+
111
+ // Both are sound; neither is being called wrong. The point is only that a choice had
112
+ // to be made and that it is checkable.
113
+ it('both readings are domain-separated and second-preimage resistant', function () {
114
+ var leafBytes = Buffer.alloc(64, 7)
115
+ Merkle.hashLeaf(leafBytes).toString('hex')
116
+ .should.not.equal(Merkle.hashNode(Buffer.alloc(32, 7), Buffer.alloc(32, 7)).toString('hex'))
117
+ })
118
+ })
119
+
120
+ describe('the verdict is a strict report, not a truthy object', function () {
121
+ it('reports valid:false rather than throwing on a malformed merkle object', function () {
122
+ var report = NotaryHash.verifyBatchInclusion({
123
+ mode: NotaryScript.MODE.BATCH,
124
+ proofHash: PROOF_HASHES[0].toString('hex'),
125
+ merkle: { root: 'not-hex', leafIndex: 0, leafCount: 1, path: [] }
126
+ })
127
+ report.valid.should.equal(false)
128
+ report.errors.length.should.be.above(0)
129
+ })
130
+
131
+ it('rejects a non-batch certificate by mode', function () {
132
+ var report = NotaryHash.verifyBatchInclusion({
133
+ mode: NotaryScript.MODE.FULL,
134
+ proofHash: PROOF_HASHES[0].toString('hex')
135
+ })
136
+ report.valid.should.equal(false)
137
+ report.errors.should.deep.equal(['certificate is not in batch mode'])
138
+ })
139
+ })
140
+ })
@@ -0,0 +1,282 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // The BRC-220 batch-mode golden vector.
6
+ //
7
+ // BRC-220 writes the batch leaf as `SHA256(0x00 ‖ d)` and never says what `d` is. Prose
8
+ // cannot reliably close that gap — two implementers read the same sentence and chose
9
+ // differently — so this file holds a concrete vector that any implementation can check
10
+ // itself against.
11
+ //
12
+ // What this vector is: a CONFORMANCE TARGET produced by this library. It is not external
13
+ // validation, and nothing here proves our reading is the one the spec intended. What makes
14
+ // it worth having is that the tree underneath it IS externally anchored — the RFC 6962
15
+ // construction is checked against the published Certificate Transparency roots in
16
+ // merkle.js. Given a tree that provably matches RFC 6962, this vector pins the one thing
17
+ // the spec leaves open.
18
+ //
19
+ // These tests rebuild every value from the recorded inputs rather than reading the
20
+ // recorded outputs back. A test that asserts `vector.root === vector.root` would pass
21
+ // forever while proving nothing, which is the failure mode this repository has fixed most
22
+ // often.
23
+ //
24
+ // See docs/BRC220_BATCH_LEAF_AMENDMENT.md and tools/gen-brc220-batch-vector.js.
25
+
26
+ require('chai').should()
27
+ var vector = require('../data/brc220-batch-vector.json')
28
+ var Hash = require('../../lib/crypto/hash')
29
+ var Encoding = require('../../lib/notaryhash/encoding')
30
+ var Merkle = require('../../lib/notaryhash/merkle')
31
+ var Suites = require('../../lib/notaryhash/suites')
32
+ var NotaryHash = require('../../lib/notaryhash')
33
+ var NotaryScript = require('../../lib/notaryhash/script')
34
+ var BN = require('../../lib/crypto/bn')
35
+ var PrivateKey = require('../../lib/privatekey')
36
+ // Deliberately a SECOND implementation: see the independent-verification test below.
37
+ var { secp256k1 } = require('@noble/curves/secp256k1.js')
38
+
39
+ function hex (s) { return Buffer.from(s, 'hex') }
40
+
41
+ describe('BRC-220 batch golden vector', function () {
42
+ describe('the vector describes the shape it claims', function () {
43
+ it('is five leaves, which is deliberate', function () {
44
+ // n=5 splits 4/1 under RFC 6962 (largest power of two BELOW n), where a midpoint
45
+ // split would give 2/3 and a different root. Odd n also exercises the rule that the
46
+ // last leaf is never duplicated. A power-of-two vector would catch neither.
47
+ vector.leafCount.should.equal(5)
48
+ vector.proofs.length.should.equal(5)
49
+ Merkle.largestPowerOfTwoBelow(5).should.equal(4)
50
+ })
51
+
52
+ it('names its tree and leaf datum explicitly', function () {
53
+ vector.tree.should.equal('RFC 6962')
54
+ vector.leafDatum.should.equal('proofHash')
55
+ })
56
+
57
+ // The 4/1 split shows up in the path lengths: the lone right-hand leaf needs one
58
+ // sibling, the four on the left need three. If a future change altered the split rule,
59
+ // this would move before any root did.
60
+ it('has path lengths matching a 4/1 split', function () {
61
+ vector.proofs.map(function (p) { return p.path.length })
62
+ .should.deep.equal([3, 3, 3, 3, 1])
63
+ })
64
+ })
65
+
66
+ describe('every input is derived, so the vector is reproducible', function () {
67
+ it('rebuilds each payloadHash from its recorded preimage', function () {
68
+ vector.proofs.forEach(function (p) {
69
+ Hash.sha256(Buffer.from(p.payloadPreimage, 'utf8')).toString('hex')
70
+ .should.equal(p.payloadHash, 'leaf ' + p.leafIndex)
71
+ })
72
+ })
73
+
74
+ it('records the preimage labels the generator actually uses', function () {
75
+ vector.proofs.forEach(function (p, i) {
76
+ p.keyPreimage.should.equal('BRC-220/batch-vector/key/' + i)
77
+ p.payloadPreimage.should.equal('BRC-220/batch-vector/payload/' + i)
78
+ })
79
+ })
80
+
81
+ it('uses raw encoding: 64-byte signatures and 33-byte compressed keys', function () {
82
+ vector.proofs.forEach(function (p) {
83
+ p.encoding.should.equal('raw')
84
+ hex(p.signature).length.should.equal(64, 'leaf ' + p.leafIndex + ' signature')
85
+ hex(p.publicKey).length.should.equal(33, 'leaf ' + p.leafIndex + ' publicKey')
86
+ })
87
+ })
88
+
89
+ it('rebuilds createdAtUnix from the recorded ISO timestamp', function () {
90
+ vector.proofs.forEach(function (p) {
91
+ Encoding.toUnixSeconds(p.createdAt).should.equal(p.createdAtUnix)
92
+ })
93
+ })
94
+ })
95
+
96
+ describe('every proof stands on its own', function () {
97
+ // Each signature must verify independently. Without this the vector could pin a tree
98
+ // built over five values that are not valid proofs at all.
99
+ it('verifies all five signatures under the named suite', function () {
100
+ vector.proofs.forEach(function (p) {
101
+ Suites.verify(p.algorithm, hex(p.payloadHash), hex(p.signature), hex(p.publicKey))
102
+ .should.equal(true, 'leaf ' + p.leafIndex + ' signature must verify')
103
+ })
104
+ })
105
+
106
+ // THE test this file most needs, and the one it originally lacked.
107
+ //
108
+ // The assertion above uses our own suite, and the generator uses our own signer, so
109
+ // the two agree by construction. The first version of this vector was generated with
110
+ // `endian: 'little'` — matching a verifier bug — and every signature in it was
111
+ // unverifiable by any other implementation. Both tests above still passed, because
112
+ // both sides of the comparison were wrong in the same direction.
113
+ //
114
+ // @noble/curves shares no signing or verification code with lib/crypto/ecdsa.js. If
115
+ // this vector is ever regenerated by a signer that disagrees with the rest of the
116
+ // world, this is the assertion that says so.
117
+ //
118
+ // { prehash: false } is REQUIRED: noble v2 hashes its input by default, so omitting
119
+ // it would compare our signature over `payloadHash` against noble's over
120
+ // `sha256(payloadHash)` — a mismatch that looks exactly like the bug this guards.
121
+ it('verifies all five signatures under an INDEPENDENT implementation', function () {
122
+ vector.proofs.forEach(function (p) {
123
+ secp256k1.verify(hex(p.signature), hex(p.payloadHash), hex(p.publicKey), { prehash: false })
124
+ .should.equal(true,
125
+ 'leaf ' + p.leafIndex + ' signature must verify under @noble/curves, not just ours')
126
+ })
127
+ })
128
+
129
+ it('derives each public key from the recorded key preimage', function () {
130
+ // Ties the published public keys back to their labelled preimages, so a third party
131
+ // can reproduce the keys and not merely check the signatures against them.
132
+ vector.proofs.forEach(function (p) {
133
+ var priv = PrivateKey.fromObject({
134
+ bn: BN.fromBuffer(Hash.sha256(Buffer.from(p.keyPreimage, 'utf8'))),
135
+ compressed: true,
136
+ network: 'livenet'
137
+ })
138
+ priv.publicKey.toBuffer().toString('hex')
139
+ .should.equal(p.publicKey, 'leaf ' + p.leafIndex + ' public key must derive from its preimage')
140
+ })
141
+ })
142
+
143
+ it('rebuilds each canonicalBytes from the recorded fields', function () {
144
+ vector.proofs.forEach(function (p) {
145
+ Encoding.canonicalBytes({
146
+ algorithm: p.algorithm,
147
+ hashAlgorithm: p.hashAlgorithm,
148
+ payloadHash: hex(p.payloadHash),
149
+ publicKey: hex(p.publicKey),
150
+ signature: hex(p.signature),
151
+ createdAtUnix: p.createdAtUnix
152
+ }).toString('hex').should.equal(p.canonicalBytes, 'leaf ' + p.leafIndex)
153
+ })
154
+ })
155
+
156
+ it('rebuilds each proofHash as SHA-256 of those canonical bytes', function () {
157
+ vector.proofs.forEach(function (p) {
158
+ Hash.sha256(hex(p.canonicalBytes)).toString('hex')
159
+ .should.equal(p.proofHash, 'leaf ' + p.leafIndex)
160
+ })
161
+ })
162
+ })
163
+
164
+ describe('the leaf definition the spec leaves open', function () {
165
+ it('rebuilds each leaf as SHA-256(0x00 || proofHash)', function () {
166
+ vector.proofs.forEach(function (p) {
167
+ Merkle.hashLeaf(hex(p.proofHash)).toString('hex')
168
+ .should.equal(p.leafHash, 'leaf ' + p.leafIndex)
169
+ })
170
+ })
171
+
172
+ it('rebuilds the root from the five proofHash values', function () {
173
+ Merkle.root(vector.proofs.map(function (p) { return hex(p.proofHash) }))
174
+ .toString('hex').should.equal(vector.root)
175
+ })
176
+
177
+ it('rebuilds every inclusion path and folds each back to the root', function () {
178
+ var leaves = vector.proofs.map(function (p) { return hex(p.proofHash) })
179
+ vector.proofs.forEach(function (p, i) {
180
+ Merkle.path(leaves, i).map(function (n) { return n.toString('hex') })
181
+ .should.deep.equal(p.path, 'path for leaf ' + i)
182
+ Merkle.verifyInclusion(leaves[i], i, vector.leafCount,
183
+ p.path.map(hex), hex(vector.root)).should.equal(true, 'fold for leaf ' + i)
184
+ })
185
+ })
186
+
187
+ it('encodes leafCount as u32be for the on-chain record', function () {
188
+ var buf = Buffer.alloc(4)
189
+ buf.writeUInt32BE(vector.leafCount, 0)
190
+ buf.toString('hex').should.equal(vector.onChainRecordTail.leafCountU32be)
191
+ vector.onChainRecordTail.merkleRoot.should.equal(vector.root)
192
+ })
193
+ })
194
+
195
+ describe('the rejected reading is recorded and really does diverge', function () {
196
+ it('reproduces the canonicalBytes-leaf root the vector records', function () {
197
+ Merkle.root(vector.proofs.map(function (p) { return hex(p.canonicalBytes) }))
198
+ .toString('hex').should.equal(vector.rejectedReading.root)
199
+ })
200
+
201
+ // The whole reason the vector exists. Equal in strength, different root.
202
+ it('is a different root from the proofHash reading', function () {
203
+ vector.root.should.not.equal(vector.rejectedReading.root)
204
+ })
205
+
206
+ it('records each leaf hash under both readings, and they differ', function () {
207
+ vector.proofs.forEach(function (p) {
208
+ Merkle.hashLeaf(hex(p.canonicalBytes)).toString('hex')
209
+ .should.equal(p.leafHashUnderRejectedReading, 'leaf ' + p.leafIndex)
210
+ p.leafHash.should.not.equal(p.leafHashUnderRejectedReading)
211
+ })
212
+ })
213
+ })
214
+
215
+ // The amendment publishes these values as the text to put in the spec. A document that
216
+ // has drifted from the artifact it describes is worse than no document, and nothing
217
+ // else would notice: the doc is prose, the vector is JSON, and they are edited
218
+ // separately.
219
+ describe('the amendment document publishes the same values', function () {
220
+ var fs = require('fs')
221
+ var path = require('path')
222
+ var doc = fs.readFileSync(
223
+ path.join(__dirname, '..', '..', 'docs', 'BRC220_BATCH_LEAF_AMENDMENT.md'), 'utf8')
224
+
225
+ it('publishes the root', function () {
226
+ doc.indexOf(vector.root).should.be.above(-1, 'root missing from the amendment')
227
+ })
228
+
229
+ it('publishes the rejected reading root, so the divergence is diagnosable', function () {
230
+ doc.indexOf(vector.rejectedReading.root)
231
+ .should.be.above(-1, 'rejected reading root missing from the amendment')
232
+ })
233
+
234
+ it('publishes every proofHash', function () {
235
+ vector.proofs.forEach(function (p) {
236
+ doc.indexOf(p.proofHash).should.be.above(-1,
237
+ 'proofHash for leaf ' + p.leafIndex + ' missing from the amendment')
238
+ })
239
+ })
240
+
241
+ it('publishes the leaf-4 audit path and the u32be leaf count', function () {
242
+ doc.indexOf(vector.proofs[4].path[0]).should.be.above(-1, 'leaf 4 path missing')
243
+ doc.indexOf(vector.onChainRecordTail.leafCountU32be).should.be.above(-1,
244
+ 'u32be leaf count missing')
245
+ })
246
+ })
247
+
248
+ describe('the library verifies certificates built from this vector', function () {
249
+ it('accepts a batch certificate for every leaf', function () {
250
+ vector.proofs.forEach(function (p, i) {
251
+ var report = NotaryHash.verifyBatchInclusion({
252
+ mode: NotaryScript.MODE.BATCH,
253
+ proofHash: p.proofHash,
254
+ merkle: {
255
+ root: vector.root,
256
+ leafIndex: i,
257
+ leafCount: vector.leafCount,
258
+ path: p.path
259
+ }
260
+ })
261
+ report.valid.should.equal(true, 'leaf ' + i)
262
+ report.errors.should.deep.equal([])
263
+ })
264
+ })
265
+
266
+ // An implementation that chose the other reading produces exactly this, and this is
267
+ // what it looks like when it reaches our verifier.
268
+ it('rejects a certificate carrying the rejected reading root', function () {
269
+ var report = NotaryHash.verifyBatchInclusion({
270
+ mode: NotaryScript.MODE.BATCH,
271
+ proofHash: vector.proofs[0].proofHash,
272
+ merkle: {
273
+ root: vector.rejectedReading.root,
274
+ leafIndex: 0,
275
+ leafCount: vector.leafCount,
276
+ path: vector.proofs[0].path
277
+ }
278
+ })
279
+ report.valid.should.equal(false)
280
+ })
281
+ })
282
+ })
@@ -0,0 +1,112 @@
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
+ })
@@ -41,8 +41,11 @@ describe('BRC-220 verification', function () {
41
41
  publicKey = key.toPublicKey().toBuffer()
42
42
  payloadHash = Hash.sha256(Buffer.from('the document nobody sees'))
43
43
 
44
- // The signer signs the 32-byte payloadHash DIRECTLY — spec §Algorithms.
45
- var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: payloadHash, endian: 'little', privkey: key })
44
+ // The signer signs the 32-byte payloadHash DIRECTLY — spec §Algorithms. No endian
45
+ // option: the digest IS the scalar, big-endian. Until 8.3.1 this line passed
46
+ // `endian: 'little'` while the comment above it said "directly", and the suite
47
+ // reversed to match, so the two agreed with each other and with nothing else.
48
+ var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: payloadHash, privkey: key })
46
49
  ecdsa.sign()
47
50
  signature = rawSig(ecdsa.sig)
48
51
 
@@ -223,16 +223,19 @@ describe('Ordinals inscriptions', function () {
223
223
  })
224
224
  })
225
225
 
226
- // The case that motivated making the script-size cap configurable: transferring a
227
- // large inscription. The envelope is inert, so the spend is an ordinary P2PKH — but
228
- // the interpreter could not even load the script, because total script size was a
229
- // hard-coded 10,000 bytes that useGenesisLimits() did not lift.
226
+ // Transferring a large inscription. The envelope is inert, so the spend is an
227
+ // ordinary P2PKH — but under pre-Genesis rules the interpreter could not even load
228
+ // the script, because total script size was capped at 10,000 bytes.
230
229
  describe('transferring a large inscription', function () {
231
230
  var I = bsv.Script.Interpreter
232
231
  var Sighash = bsv.Transaction.Sighash
233
232
  var BN = bsv.crypto.BN
234
233
  var SIGHASH = bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID
234
+ // Pre-Genesis: no era bit, so the 520-byte element cap and the 10,000-byte script
235
+ // cap both still apply. Kept deliberately, to show what they reject.
235
236
  var FLAGS = I.SCRIPT_VERIFY_STRICTENC | I.SCRIPT_ENABLE_SIGHASH_FORKID
237
+ // Current mainnet, where Genesis lifted both.
238
+ var MAINNET = I.mainnetFlags()
236
239
  var saved
237
240
 
238
241
  beforeEach(function () { saved = I.getLimits() })
@@ -265,35 +268,43 @@ describe('Ordinals inscriptions', function () {
265
268
  return { lock: lock, unlock: unlock, spend: spend, sig: sig, owner: owner }
266
269
  }
267
270
 
268
- function evaluate (t) {
271
+ // Both caps are derived from the ERA flags, not from the limit statics: see
272
+ // Interpreter#maxScriptElementSize and #maxScriptSize, each of which branches on
273
+ // isAfterGenesis(). Passing the era is therefore the whole fix — these tests used
274
+ // to call SmartContract.enableGenesis(), which raised process-wide statics instead
275
+ // and had to be undone in an afterEach or it changed the rules for every test that
276
+ // ran later.
277
+ function evaluate (t, flags) {
269
278
  var interp = new I()
270
- var ok = interp.verify(t.unlock, t.lock, t.spend, 0, FLAGS, new BN(1))
279
+ var ok = interp.verify(t.unlock, t.lock, t.spend, 0,
280
+ flags === undefined ? MAINNET : flags, new BN(1))
271
281
  return { ok: ok, err: interp.errstr }
272
282
  }
273
283
 
274
284
  it('cannot be evaluated under pre-Genesis limits — two separate caps bite', function () {
285
+ // Explicitly pre-Genesis: FLAGS carries no era bit, so both caps still apply.
275
286
  // Under 10 KB the script loads and the 520-byte push cap rejects the content...
276
- var small = evaluate(transfer(3))
287
+ var small = evaluate(transfer(3), FLAGS)
277
288
  small.ok.should.equal(false)
278
289
  small.err.should.equal('SCRIPT_ERR_PUSH_SIZE')
279
290
  // ...over 10 KB it never gets that far: evaluate() refuses the script outright.
280
- // This is the cap useGenesisLimits() could not lift.
281
- var big = evaluate(transfer(50))
291
+ var big = evaluate(transfer(50), FLAGS)
282
292
  big.ok.should.equal(false)
283
293
  big.err.should.equal('SCRIPT_ERR_SCRIPT_SIZE')
284
294
  })
285
295
 
286
- it('evaluates once Genesis limits are enabled', function () {
287
- bsv.SmartContract.enableGenesis()
296
+ it('evaluates under mainnet consensus, with the limit statics untouched', function () {
297
+ var before = JSON.stringify(I.getLimits())
288
298
  var t = transfer(50)
289
- t.lock.toBuffer().length.should.be.above(10000) // past the old hard-coded cap
299
+ t.lock.toBuffer().length.should.be.above(10000) // past the pre-Genesis cap
290
300
  evaluate(t).ok.should.equal(true)
301
+ // The point of the change: no process-wide mutation was needed to get here.
302
+ JSON.stringify(I.getLimits()).should.equal(before)
291
303
  })
292
304
 
293
305
  it('agrees with a direct signature check against the sighash', function () {
294
306
  // The fallback used when the interpreter cannot run: verify the signature over
295
307
  // the sighash directly. It must agree with the interpreter where both work.
296
- bsv.SmartContract.enableGenesis()
297
308
  var t = transfer(50)
298
309
  var hash = Sighash.sighash(t.spend, SIGHASH, 0, t.lock, new BN(1), FLAGS)
299
310
  var sigOk = bsv.crypto.ECDSA.verify(hash, t.sig, t.owner.toPublicKey(), 'little')
@@ -302,7 +313,6 @@ describe('Ordinals inscriptions', function () {
302
313
  })
303
314
 
304
315
  it('rejects a signature made over the base lock instead of the full script', function () {
305
- bsv.SmartContract.enableGenesis()
306
316
  var t = transfer(50)
307
317
  var baseLock = bsv.Script.buildPublicKeyHashOut(t.owner.toAddress())
308
318
  var wrong = Sighash.sign(t.spend, t.owner, SIGHASH, 0, baseLock, new BN(1), FLAGS)