@smartledger/bsv 8.2.0 → 8.3.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.
@@ -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
+ })
@@ -0,0 +1,181 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // RFC 6962 Merkle trees, for BRC-220 batch mode.
6
+ //
7
+ // The first version of foldPath here descended from the root by splitting at the largest
8
+ // power of two, mirroring how root() builds the tree. It round-tripped perfectly for
9
+ // powers of two and failed for 21 of the 45 leaves in trees of size 1..9 — which is
10
+ // exactly why the published vectors below matter more than any round-trip test. A tree
11
+ // that agrees with itself is not evidence.
12
+
13
+ require('chai').should()
14
+ var Merkle = require('../../lib/notaryhash/merkle')
15
+ var Hash = require('../../lib/crypto/hash')
16
+
17
+ // The canonical RFC 6962 / Certificate Transparency inputs.
18
+ var D = ['', '00', '10', '2021', '3031', '40414243', '5051525354555657',
19
+ '606162636465666768696a6b6c6d6e6f'].map(function (h) { return Buffer.from(h, 'hex') })
20
+
21
+ describe('RFC 6962 Merkle tree', function () {
22
+ describe('published vectors', function () {
23
+ // External validation. Everything else in this file could be internally consistent
24
+ // and wrong; these numbers come from outside this codebase.
25
+ var EXPECTED = {
26
+ 0: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
27
+ 1: '6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d',
28
+ 2: 'fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125',
29
+ 4: 'd37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7',
30
+ 7: 'ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c',
31
+ 8: '5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328'
32
+ }
33
+
34
+ Object.keys(EXPECTED).forEach(function (n) {
35
+ it('MTH of ' + n + ' leaves matches the published root', function () {
36
+ Merkle.root(D.slice(0, Number(n))).toString('hex').should.equal(EXPECTED[n])
37
+ })
38
+ })
39
+ })
40
+
41
+ describe('construction', function () {
42
+ it('hashes the empty tree as SHA-256 of the empty string, not zeroes', function () {
43
+ Merkle.root([]).toString('hex').should.equal(Hash.sha256(Buffer.alloc(0)).toString('hex'))
44
+ Merkle.root([]).toString('hex').should.not.equal('00'.repeat(32))
45
+ })
46
+
47
+ it('domain-separates leaves from internal nodes', function () {
48
+ Merkle.hashLeaf(Buffer.from('ab', 'hex')).toString('hex')
49
+ .should.equal(Hash.sha256(Buffer.from('00ab', 'hex')).toString('hex'))
50
+ Merkle.hashNode(Buffer.alloc(32, 1), Buffer.alloc(32, 2)).toString('hex')
51
+ .should.equal(Hash.sha256(Buffer.concat([
52
+ Buffer.from([0x01]), Buffer.alloc(32, 1), Buffer.alloc(32, 2)
53
+ ])).toString('hex'))
54
+ })
55
+
56
+ // Without the 0x00/0x01 prefixes an internal node could be reinterpreted as a leaf,
57
+ // which is the second-preimage attack Bitcoin's tree is open to.
58
+ it('gives a leaf and a node over the same bytes different hashes', function () {
59
+ var payload = Buffer.alloc(64, 7)
60
+ Merkle.hashLeaf(payload).toString('hex')
61
+ .should.not.equal(Merkle.hashNode(Buffer.alloc(32, 7), Buffer.alloc(32, 7)).toString('hex'))
62
+ })
63
+
64
+ // The split point, not the midpoint. For n=5 the RFC splits 4/1.
65
+ it('splits at the largest power of two below n', function () {
66
+ var expected = { 2: 1, 3: 2, 4: 2, 5: 4, 6: 4, 7: 4, 8: 4, 9: 8, 16: 8, 17: 16 }
67
+ Object.keys(expected).forEach(function (n) {
68
+ Merkle.largestPowerOfTwoBelow(Number(n)).should.equal(expected[n])
69
+ })
70
+ })
71
+ })
72
+
73
+ // The property the plan named as the one that would ship silently: a Bitcoin-style
74
+ // tree over the same leaves must produce a DIFFERENT root.
75
+ describe('is not the Bitcoin tree', function () {
76
+ function bitcoinStyleRoot (leaves) {
77
+ var level = leaves.map(function (l) { return Hash.sha256sha256(l) })
78
+ while (level.length > 1) {
79
+ var next = []
80
+ for (var i = 0; i < level.length; i += 2) {
81
+ var left = level[i]
82
+ var right = i + 1 < level.length ? level[i + 1] : left // duplicated
83
+ next.push(Hash.sha256sha256(Buffer.concat([left, right])))
84
+ }
85
+ level = next
86
+ }
87
+ return level[0]
88
+ }
89
+
90
+ it('produces a different root over the same leaves', function () {
91
+ var leaves = D.slice(0, 4)
92
+ Merkle.root(leaves).toString('hex')
93
+ .should.not.equal(bitcoinStyleRoot(leaves).toString('hex'))
94
+ })
95
+
96
+ it('differs for an odd leaf count, where Bitcoin duplicates and RFC 6962 does not', function () {
97
+ var leaves = D.slice(0, 3)
98
+ Merkle.root(leaves).toString('hex')
99
+ .should.not.equal(bitcoinStyleRoot(leaves).toString('hex'))
100
+ })
101
+ })
102
+
103
+ describe('inclusion proofs', function () {
104
+ it('round-trips every leaf for every tree size 1..33', function () {
105
+ var failures = []
106
+ for (var n = 1; n <= 33; n++) {
107
+ var leaves = []
108
+ for (var i = 0; i < n; i++) leaves.push(Buffer.from([i & 0xff, n & 0xff]))
109
+ var root = Merkle.root(leaves)
110
+ for (var j = 0; j < n; j++) {
111
+ if (!Merkle.verifyInclusion(leaves[j], j, n, Merkle.path(leaves, j), root)) {
112
+ failures.push('n=' + n + ' i=' + j)
113
+ }
114
+ }
115
+ }
116
+ failures.should.deep.equal([])
117
+ })
118
+
119
+ it('rejects a proof for the wrong leaf', function () {
120
+ var leaves = D.slice(0, 8)
121
+ var root = Merkle.root(leaves)
122
+ Merkle.verifyInclusion(D[1], 0, 8, Merkle.path(leaves, 0), root).should.equal(false)
123
+ })
124
+
125
+ it('rejects a proof at the wrong index', function () {
126
+ var leaves = D.slice(0, 8)
127
+ var root = Merkle.root(leaves)
128
+ Merkle.verifyInclusion(leaves[3], 4, 8, Merkle.path(leaves, 3), root).should.equal(false)
129
+ })
130
+
131
+ // leafCount changes the tree shape — but only for SOME indices. On an 8-leaf tree
132
+ // claimed as 7, indices 0..5 fold identically and only 6 and 7 differ, because the
133
+ // left region of the tree is the same shape either way.
134
+ //
135
+ // That is a property worth knowing rather than a defect: it means an inclusion proof
136
+ // alone cannot be trusted to detect a wrong leafCount, which is why
137
+ // NotaryHash.recordMatchesCertificate compares the certificate's count against the
138
+ // authoritative u32be in the on-chain record instead of assuming the fold implies it.
139
+ it('rejects a wrong leafCount where the shape differs', function () {
140
+ var leaves = D.slice(0, 8)
141
+ var root = Merkle.root(leaves)
142
+ Merkle.verifyInclusion(leaves[6], 6, 7, Merkle.path(leaves, 6), root).should.equal(false)
143
+ Merkle.verifyInclusion(leaves[7], 7, 7, Merkle.path(leaves, 7), root).should.equal(false)
144
+ })
145
+
146
+ it('does NOT catch a wrong leafCount where the shape is the same', function () {
147
+ var leaves = D.slice(0, 8)
148
+ var root = Merkle.root(leaves)
149
+ // Documented, not desired. The on-chain leafCount is what closes this.
150
+ Merkle.verifyInclusion(leaves[3], 3, 7, Merkle.path(leaves, 3), root).should.equal(true)
151
+ })
152
+
153
+ it('rejects a truncated or padded path', function () {
154
+ var leaves = D.slice(0, 8)
155
+ var root = Merkle.root(leaves)
156
+ var path = Merkle.path(leaves, 3)
157
+ Merkle.verifyInclusion(leaves[3], 3, 8, path.slice(0, -1), root).should.equal(false)
158
+ Merkle.verifyInclusion(leaves[3], 3, 8, path.concat([Buffer.alloc(32)]), root).should.equal(false)
159
+ })
160
+
161
+ it('rejects a tampered sibling', function () {
162
+ var leaves = D.slice(0, 8)
163
+ var root = Merkle.root(leaves)
164
+ var path = Merkle.path(leaves, 3)
165
+ path[0] = Buffer.alloc(32, 0xff)
166
+ Merkle.verifyInclusion(leaves[3], 3, 8, path, root).should.equal(false)
167
+ })
168
+
169
+ it('returns a strict boolean and never throws on malformed input', function () {
170
+ Merkle.verifyInclusion(D[0], 0, 1, [], Merkle.root([D[0]])).should.be.a('boolean')
171
+ Merkle.verifyInclusion(null, 0, 1, [], Buffer.alloc(32)).should.equal(false)
172
+ Merkle.verifyInclusion(D[0], 5, 1, [], Buffer.alloc(32)).should.equal(false)
173
+ Merkle.verifyInclusion(D[0], 0, 1, [], 'not-a-buffer').should.equal(false)
174
+ })
175
+
176
+ it('gives an empty path for a single-leaf tree', function () {
177
+ Merkle.path([D[0]], 0).should.deep.equal([])
178
+ Merkle.verifyInclusion(D[0], 0, 1, [], Merkle.hashLeaf(D[0])).should.equal(true)
179
+ })
180
+ })
181
+ })
@@ -0,0 +1,270 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // BRC-220 on-chain record. As with the encoding module, the property that matters is that
6
+ // another implementation reading the spec produces and accepts the same bytes — so the
7
+ // tests assert the push layout the spec dictates rather than only round-tripping our own
8
+ // output, which would pass even if every field were in the wrong place.
9
+
10
+ require('chai').should()
11
+ var bsv = require('../..')
12
+ var NS = require('../../lib/notaryhash/script')
13
+ var Hash = require('../../lib/crypto/hash')
14
+ var Opcode = require('../../lib/opcode')
15
+
16
+ var FULL = {
17
+ mode: NS.MODE.FULL,
18
+ algorithm: 'ECDSA-secp256k1',
19
+ hashAlgorithm: 'SHA-256',
20
+ payloadHash: Buffer.alloc(32, 0x11),
21
+ proofHash: Buffer.alloc(32, 0x22),
22
+ publicKey: Buffer.alloc(33, 0x02),
23
+ signature: Buffer.alloc(64, 0x30)
24
+ }
25
+
26
+ // The spec counts PUSH indices, which start after OP_FALSE OP_RETURN. Script chunks
27
+ // include those two opcodes, so push N is chunk N+2.
28
+ function pushes (script) {
29
+ return script.chunks.slice(2)
30
+ }
31
+
32
+ describe('BRC-220 on-chain record', function () {
33
+ describe('push layout', function () {
34
+ it('is OP_FALSE OP_RETURN followed by the pushes', function () {
35
+ var s = NS.build(FULL)
36
+ s.chunks[0].opcodenum.should.equal(Opcode.OP_FALSE)
37
+ s.chunks[1].opcodenum.should.equal(Opcode.OP_RETURN)
38
+ })
39
+
40
+ it('puts the prefix at push index 0, as 10 ASCII bytes', function () {
41
+ var p = pushes(NS.build(FULL))
42
+ p[0].buf.toString('ascii').should.equal('NOTARYHASH')
43
+ p[0].buf.length.should.equal(10)
44
+ p[0].buf.toString('hex').should.equal('4e4f5441525948415348')
45
+ })
46
+
47
+ // The spec discriminates on "the mode/kind byte at push index 2". If this drifts,
48
+ // every other implementation reads the wrong field.
49
+ it('puts the version at push index 1 and the mode at push index 2', function () {
50
+ var p = pushes(NS.build(FULL))
51
+ p[1].buf.length.should.equal(1)
52
+ p[1].buf[0].should.equal(1)
53
+ p[2].buf.length.should.equal(1)
54
+ p[2].buf[0].should.equal(NS.MODE.FULL)
55
+ })
56
+
57
+ it('orders full-mode fields as the spec lists them', function () {
58
+ var p = pushes(NS.build(FULL))
59
+ p.length.should.equal(9)
60
+ p[3].buf.toString('utf8').should.equal('ECDSA-secp256k1')
61
+ p[4].buf.toString('utf8').should.equal('SHA-256')
62
+ p[5].buf.toString('hex').should.equal(FULL.payloadHash.toString('hex'))
63
+ p[6].buf.toString('hex').should.equal(FULL.proofHash.toString('hex'))
64
+ p[7].buf.toString('hex').should.equal(FULL.publicKey.toString('hex'))
65
+ p[8].buf.toString('hex').should.equal(FULL.signature.toString('hex'))
66
+ })
67
+
68
+ it('lays batch mode out as root then u32be count', function () {
69
+ var p = pushes(NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32, 0xaa), leafCount: 12345 }))
70
+ p.length.should.equal(5)
71
+ p[2].buf[0].should.equal(NS.MODE.BATCH)
72
+ p[3].buf.length.should.equal(32)
73
+ p[4].buf.length.should.equal(4)
74
+ p[4].buf.readUInt32BE(0).should.equal(12345)
75
+ })
76
+
77
+ // The canonical proof bytes use lp() because they are one flat string; here the push
78
+ // opcode already carries the length. Applying lp() as well would double-encode.
79
+ it('does not length-prefix inside the pushes', function () {
80
+ var p = pushes(NS.build(FULL))
81
+ // A doubly-encoded payloadHash would be 4 + 32 bytes.
82
+ p[5].buf.length.should.equal(32)
83
+ p[3].buf.length.should.equal('ECDSA-secp256k1'.length)
84
+ })
85
+ })
86
+
87
+ describe('hybrid mode', function () {
88
+ it('puts SHA-256 of the key and signature on chain', function () {
89
+ var p = pushes(NS.build(Object.assign({}, FULL, { mode: NS.MODE.HYBRID })))
90
+ p[7].buf.toString('hex').should.equal(Hash.sha256(FULL.publicKey).toString('hex'))
91
+ p[8].buf.toString('hex').should.equal(Hash.sha256(FULL.signature).toString('hex'))
92
+ p[7].buf.length.should.equal(32)
93
+ p[8].buf.length.should.equal(32)
94
+ })
95
+
96
+ // The caller always supplies the FULL key and signature; hashing is this module's job.
97
+ // Accepting a pre-hashed blob would produce a record that looks right and cannot be
98
+ // reconciled with the certificate, and nothing downstream would notice.
99
+ it('hashes internally rather than trusting a pre-hashed input', function () {
100
+ var preHashed = Object.assign({}, FULL, {
101
+ mode: NS.MODE.HYBRID,
102
+ publicKey: Hash.sha256(FULL.publicKey)
103
+ })
104
+ var p = pushes(NS.build(preHashed))
105
+ // Hashed again, so it does NOT equal the value that was passed in.
106
+ p[7].buf.toString('hex').should.not.equal(Hash.sha256(FULL.publicKey).toString('hex'))
107
+ })
108
+
109
+ it('parses to hashes, never to raw blobs', function () {
110
+ var parsed = NS.parse(NS.build(Object.assign({}, FULL, { mode: NS.MODE.HYBRID })))
111
+ parsed.publicKeyHash.toString('hex').should.equal(Hash.sha256(FULL.publicKey).toString('hex'))
112
+ ;(parsed.publicKey === undefined).should.equal(true)
113
+ ;(parsed.signature === undefined).should.equal(true)
114
+ })
115
+ })
116
+
117
+ describe('round trip', function () {
118
+ it('preserves every full-mode field', function () {
119
+ var p = NS.parse(NS.build(FULL))
120
+ p.mode.should.equal(NS.MODE.FULL)
121
+ p.version.should.equal(1)
122
+ p.algorithm.should.equal(FULL.algorithm)
123
+ p.hashAlgorithm.should.equal(FULL.hashAlgorithm)
124
+ p.payloadHash.toString('hex').should.equal(FULL.payloadHash.toString('hex'))
125
+ p.proofHash.toString('hex').should.equal(FULL.proofHash.toString('hex'))
126
+ p.publicKey.toString('hex').should.equal(FULL.publicKey.toString('hex'))
127
+ p.signature.toString('hex').should.equal(FULL.signature.toString('hex'))
128
+ })
129
+
130
+ it('preserves batch fields', function () {
131
+ var p = NS.parse(NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32, 0xbb), leafCount: 1 }))
132
+ p.merkleRoot.toString('hex').should.equal(Buffer.alloc(32, 0xbb).toString('hex'))
133
+ p.leafCount.should.equal(1)
134
+ })
135
+
136
+ it('round-trips through hex', function () {
137
+ var hex = NS.build(FULL).toHex()
138
+ NS.parse(hex).algorithm.should.equal(FULL.algorithm)
139
+ })
140
+
141
+ it('carries a post-quantum-sized signature in full mode', function () {
142
+ var big = Object.assign({}, FULL, { signature: Buffer.alloc(7856, 0x5a) })
143
+ NS.parse(NS.build(big)).signature.length.should.equal(7856)
144
+ })
145
+ })
146
+
147
+ describe('parser rejects', function () {
148
+ function corrupt (fn) {
149
+ var s = NS.build(FULL)
150
+ fn(s)
151
+ return s
152
+ }
153
+
154
+ // Long enough to pass the length check, so the OP_FALSE check is what rejects it.
155
+ it('a record that does not open with OP_FALSE', function () {
156
+ var s = corrupt(function (s) { s.chunks[0] = { opcodenum: Opcode.OP_1 } })
157
+ ;(function () { NS.parse(s) }).should.throw(/OP_FALSE/)
158
+ })
159
+
160
+ it('a record with no OP_RETURN', function () {
161
+ var s = corrupt(function (s) { s.chunks[1] = { opcodenum: Opcode.OP_1 } })
162
+ ;(function () { NS.parse(s) }).should.throw(/OP_RETURN/)
163
+ })
164
+
165
+ it('a foreign protocol prefix', function () {
166
+ var s = corrupt(function (s) { s.chunks[2].buf = Buffer.from('SOMETHINGELSE', 'ascii') })
167
+ ;(function () { NS.parse(s) }).should.throw(/prefix/)
168
+ })
169
+
170
+ it('an unsupported version', function () {
171
+ var s = corrupt(function (s) { s.chunks[3].buf = Buffer.from([9]) })
172
+ ;(function () { NS.parse(s) }).should.throw(/version: 9/)
173
+ })
174
+
175
+ it('an unknown mode', function () {
176
+ var s = corrupt(function (s) { s.chunks[4].buf = Buffer.from([7]) })
177
+ ;(function () { NS.parse(s) }).should.throw(/unknown NotaryHash mode: 7/)
178
+ })
179
+
180
+ it('the wrong number of pushes', function () {
181
+ var s = corrupt(function (s) { s.chunks.pop() })
182
+ ;(function () { NS.parse(s) }).should.throw(/exactly 11 pushes/)
183
+ })
184
+
185
+ it('a payloadHash that is not 32 bytes', function () {
186
+ var s = corrupt(function (s) { s.chunks[7].buf = Buffer.alloc(31, 1) })
187
+ ;(function () { NS.parse(s) }).should.throw(/payloadHash must be exactly 32/)
188
+ })
189
+
190
+ it('a leafCount that is not 4 bytes', function () {
191
+ var s = NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32, 1), leafCount: 5 })
192
+ s.chunks[6].buf = Buffer.alloc(3, 0)
193
+ ;(function () { NS.parse(s) }).should.throw(/leafCount must be 4 bytes/)
194
+ })
195
+
196
+ it('too few pushes to be a record at all', function () {
197
+ ;(function () { NS.parse(new bsv.Script().add(Opcode.OP_FALSE).add(Opcode.OP_RETURN)) })
198
+ .should.throw(/too few pushes/)
199
+ })
200
+ })
201
+
202
+ describe('builder rejects', function () {
203
+ it('an unknown mode', function () {
204
+ ;(function () { NS.build(Object.assign({}, FULL, { mode: 5 })) }).should.throw(/mode must be/)
205
+ })
206
+
207
+ it('hex where raw bytes belong', function () {
208
+ ;(function () {
209
+ NS.build(Object.assign({}, FULL, { payloadHash: FULL.payloadHash.toString('hex') }))
210
+ }).should.throw(/must be a Buffer/)
211
+ })
212
+
213
+ it('a payloadHash of the wrong length', function () {
214
+ ;(function () {
215
+ NS.build(Object.assign({}, FULL, { payloadHash: Buffer.alloc(31) }))
216
+ }).should.throw(/exactly 32 bytes/)
217
+ })
218
+
219
+ it('a non-integer leafCount', function () {
220
+ ;(function () {
221
+ NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32), leafCount: 1.5 })
222
+ }).should.throw(/leafCount/)
223
+ })
224
+ })
225
+
226
+ // A builder that minimally encodes its pushes represents u8(1) as OP_1. Those records
227
+ // are conformant, and rejecting them would be an interop failure rather than a safety
228
+ // measure — the mode byte is a routing hint, and every field that matters is checked
229
+ // on its own terms.
230
+ describe('interop with minimally-encoded pushes', function () {
231
+ it('reads OP_1 / OP_0 as the version and mode bytes', function () {
232
+ var s = NS.build(FULL)
233
+ s.chunks[3] = { opcodenum: Opcode.OP_1 }
234
+ s.chunks[4] = { opcodenum: Opcode.OP_0 }
235
+ var p = NS.parse(s)
236
+ p.version.should.equal(1)
237
+ p.mode.should.equal(NS.MODE.FULL)
238
+ })
239
+
240
+ it('reads OP_2 as batch mode', function () {
241
+ var s = NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32, 3), leafCount: 2 })
242
+ s.chunks[4] = { opcodenum: Opcode.OP_2 }
243
+ NS.parse(s).mode.should.equal(NS.MODE.BATCH)
244
+ })
245
+ })
246
+
247
+ describe('isNotaryHash', function () {
248
+ it('accepts a record of each mode', function () {
249
+ NS.isNotaryHash(NS.build(FULL)).should.equal(true)
250
+ NS.isNotaryHash(NS.build(Object.assign({}, FULL, { mode: NS.MODE.HYBRID }))).should.equal(true)
251
+ NS.isNotaryHash(NS.build({ mode: NS.MODE.BATCH, merkleRoot: Buffer.alloc(32), leafCount: 0 }))
252
+ .should.equal(true)
253
+ })
254
+
255
+ it('rejects an unrelated output without throwing', function () {
256
+ NS.isNotaryHash(bsv.Script.buildPublicKeyHashOut(bsv.PrivateKey.fromRandom().toAddress()))
257
+ .should.equal(false)
258
+ NS.isNotaryHash('not-hex-at-all').should.equal(false)
259
+ })
260
+
261
+ // It is a scanning filter, not a validator. Saying so in a test stops it being used
262
+ // as one.
263
+ it('is only a filter — a truncated record still passes it but fails parse()', function () {
264
+ var s = NS.build(FULL)
265
+ s.chunks = s.chunks.slice(0, 6)
266
+ NS.isNotaryHash(s).should.equal(true)
267
+ ;(function () { NS.parse(s) }).should.throw()
268
+ })
269
+ })
270
+ })