@smartledger/bsv 9.8.0 → 9.9.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.
- package/CHANGELOG.md +94 -0
- package/README.md +19 -19
- package/bsv-gdaf.min.js +59 -59
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +59 -59
- package/bsv.d.ts +340 -0
- package/bsv.min.js +59 -59
- package/docs/AUDIT_SCOPE.md +6 -6
- package/docs/BRC220_BATCH_LEAF_AMENDMENT.md +16 -6
- package/docs/BRC220_ENCODING_AMENDMENT.md +30 -91
- package/docs/BRC220_PLAN.md +39 -34
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/audit-rfq/cure53.txt +1 -1
- package/docs/audit-rfq/ncc-group.txt +1 -1
- package/docs/audit-rfq/trail-of-bits.txt +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/lib/notaryhash/certificate.js +524 -79
- package/lib/notaryhash/index.js +100 -71
- package/lib/notaryhash/merkle.js +94 -0
- package/lib/notaryhash/script.js +8 -1
- package/lib/notaryhash/suites.js +17 -14
- package/package.json +1 -1
- package/tools/gen-brc220-batch-vector.js +7 -3
- package/version.js +1 -1
|
@@ -2,80 +2,307 @@
|
|
|
2
2
|
|
|
3
3
|
var JCS = require('../util/jcs')
|
|
4
4
|
var Encoding = require('./encoding')
|
|
5
|
+
var Merkle = require('./merkle')
|
|
5
6
|
var NotaryScript = require('./script')
|
|
7
|
+
var deprecate = require('../util/deprecate')
|
|
6
8
|
var $ = require('../util/preconditions')
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* BRC-220 certificate — the self-contained object a verifier is handed.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* payloadHash, publicKey, signature, encoding, proofHash, createdAt, anchor. A batched
|
|
13
|
-
* certificate additionally carries `merkle`.
|
|
13
|
+
* There are two JSON formats, and this module reads both.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* The REFERENCE format is the BRC-220 reference implementation's, field for field. A
|
|
16
|
+
* certificate exists to be checked by someone other than its issuer, and this is the
|
|
17
|
+
* format every other BRC-220 verifier reads:
|
|
18
|
+
*
|
|
19
|
+
* {
|
|
20
|
+
* protocol: 'NotaryHash',
|
|
21
|
+
* version: '1.0',
|
|
22
|
+
* mode: 'full' | 'hybrid', // how the proof sits on chain
|
|
23
|
+
* algorithm, hashAlgorithm,
|
|
24
|
+
* payloadHash: <hex>,
|
|
25
|
+
* publicKey, signature: <per encoding>, // FULL blobs, even in hybrid mode
|
|
26
|
+
* encoding: 'hex' | 'base64', // how publicKey and signature are written
|
|
27
|
+
* proofHash: <hex>,
|
|
28
|
+
* createdAt: <ISO 8601, whole seconds>,
|
|
29
|
+
* anchor: { type: 'direct' | 'batch', network, txid, vout, blockHeight, blockTime },
|
|
30
|
+
* merkle: { root, leafIndex, leafCount, path: [{ hash, side }] }, // iff batch
|
|
31
|
+
* spv: { rawTx, blockHash, blockHeight, merkleProof, format } // once mined
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* Batch is NOT a mode there. A batched proof is still full or hybrid; what makes it a
|
|
35
|
+
* batch is that its anchor holds a Merkle root rather than the proof itself, which is why
|
|
36
|
+
* the reference marks it on `anchor.type`.
|
|
37
|
+
*
|
|
38
|
+
* The LEGACY format is what 8.3.0–9.8.0 wrote: `version: 1`, the numeric on-chain mode
|
|
39
|
+
* byte, `encoding: "raw" | "der"`, `anchor: { txid, blockHeight }` and bare-hash Merkle
|
|
40
|
+
* paths. BRC-220 lists the required fields but not their JSON values, and those releases
|
|
41
|
+
* filled the gap with choices of their own, so the reference could verify none of their
|
|
42
|
+
* certificates and they could verify none of the reference's. The cryptography agreed
|
|
43
|
+
* throughout; only the JSON did not.
|
|
44
|
+
*
|
|
45
|
+
* Reading: every check normalises a legacy certificate onto the reference format first,
|
|
46
|
+
* so both verify.
|
|
47
|
+
*
|
|
48
|
+
* Writing: `build({ format: 'reference' })` writes the reference format. Through 9.x,
|
|
49
|
+
* omitting `format` still writes the legacy one, byte for byte as 9.8.0 did, and warns
|
|
50
|
+
* once — STABILITY.md does not let a minor change what an API returns. The default
|
|
51
|
+
* becomes 'reference' in 10.0.0.
|
|
52
|
+
*
|
|
53
|
+
* Two properties are load-bearing and both are asserted by tests:
|
|
16
54
|
*
|
|
17
55
|
* - The certificate carries the FULL publicKey and signature in every mode, including
|
|
18
56
|
* hybrid, where only their SHA-256 digests go on chain. That asymmetry is the point of
|
|
19
57
|
* hybrid mode: the chain stays small, the certificate stays complete.
|
|
20
|
-
* - The SPV envelope is ADDITIVE.
|
|
21
|
-
* proofHash
|
|
22
|
-
* proofHash being computed over the canonical proof bytes, not over the certificate
|
|
23
|
-
* JSON. A test asserts it rather than trusting the reasoning.
|
|
58
|
+
* - The SPV envelope is ADDITIVE. Attaching it never changes proofHash, because
|
|
59
|
+
* proofHash is over the canonical proof bytes, not over the certificate JSON.
|
|
24
60
|
*/
|
|
25
61
|
|
|
26
62
|
var Certificate = {}
|
|
27
63
|
|
|
28
64
|
Certificate.PROTOCOL = 'NotaryHash'
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The `version` the 9.x DEFAULT format writes — the 8.3.0–9.8.0 format. It becomes
|
|
68
|
+
* REFERENCE_VERSION in 10.0.0, with the default.
|
|
69
|
+
*/
|
|
29
70
|
Certificate.VERSION = 1
|
|
30
71
|
|
|
72
|
+
/** The `version` the reference format writes. */
|
|
73
|
+
Certificate.REFERENCE_VERSION = '1.0'
|
|
74
|
+
|
|
75
|
+
/** What `build()` writes. See the module comment. */
|
|
76
|
+
Certificate.FORMAT = { REFERENCE: 'reference', LEGACY: 'legacy' }
|
|
77
|
+
|
|
78
|
+
/** A reference-format certificate's `mode`. */
|
|
79
|
+
Certificate.MODE = { FULL: 'full', HYBRID: 'hybrid' }
|
|
80
|
+
|
|
31
81
|
/**
|
|
32
|
-
*
|
|
82
|
+
* `encoding` values. In the reference format, HEX or BASE64: how `publicKey` and
|
|
83
|
+
* `signature` are written into the JSON, and nothing about their bytes — a signature is
|
|
84
|
+
* whatever the signer produced, for ECDSA 64-byte `r || s` or DER, told apart by the
|
|
85
|
+
* bytes themselves. `payloadHash`, `proofHash` and the Merkle hashes are always hex.
|
|
33
86
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
87
|
+
* RAW and DER are the legacy format's values. They named the signature's byte form, and
|
|
88
|
+
* both were written out as hex.
|
|
89
|
+
*/
|
|
90
|
+
Certificate.ENCODING = { HEX: 'hex', BASE64: 'base64', RAW: 'raw', DER: 'der' }
|
|
91
|
+
|
|
92
|
+
Certificate.ANCHOR_TYPE = { DIRECT: 'direct', BATCH: 'batch' }
|
|
93
|
+
Certificate.DEFAULT_NETWORK = 'bsv-mainnet'
|
|
94
|
+
|
|
95
|
+
// What 8.3.0–9.8.0 wrote in `version`. Not exported under this name: in 9.x it is
|
|
96
|
+
// Certificate.VERSION, and a second constant for the same value invites the two to drift.
|
|
97
|
+
var LEGACY_VERSION = 1
|
|
98
|
+
|
|
99
|
+
// What each legacy numeric mode becomes. Legacy batch (2) was a mode; here it is a full
|
|
100
|
+
// proof on a batch anchor.
|
|
101
|
+
var LEGACY_MODE = { 0: 'full', 1: 'hybrid', 2: 'full' }
|
|
102
|
+
|
|
103
|
+
var HEX_RE = /^[0-9a-fA-F]*$/
|
|
104
|
+
// The reference decodes base64 with Buffer.from(value, 'base64'), which also takes the
|
|
105
|
+
// URL-safe alphabet and missing padding. Those are accepted here too, so nothing the
|
|
106
|
+
// reference reads is refused. What is NOT accepted is any other character: Buffer.from
|
|
107
|
+
// skips those silently, turning a corrupted field into different bytes instead of an
|
|
108
|
+
// error.
|
|
109
|
+
var BASE64_RE = /^[A-Za-z0-9+/_-]*={0,2}$/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decode a certificate string field to bytes.
|
|
113
|
+
*
|
|
114
|
+
* Hex takes an optional `0x` prefix, because the reference accepts one. Both forms are
|
|
115
|
+
* validated rather than handed to Buffer.from, which silently drops characters it does
|
|
116
|
+
* not recognise and would turn a malformed field into different bytes instead of an
|
|
117
|
+
* error.
|
|
37
118
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* inside proofHash.
|
|
119
|
+
* @param {String} value
|
|
120
|
+
* @param {String} encoding - 'hex' | 'base64'
|
|
121
|
+
* @param {String} [name] - for the error message
|
|
122
|
+
* @returns {Buffer}
|
|
43
123
|
*/
|
|
44
|
-
Certificate.
|
|
45
|
-
|
|
46
|
-
|
|
124
|
+
Certificate.decodeBytes = function (value, encoding, name) {
|
|
125
|
+
name = name || 'value'
|
|
126
|
+
if (typeof value !== 'string') throw new Error(name + ' must be a string')
|
|
127
|
+
if (encoding === Certificate.ENCODING.HEX) {
|
|
128
|
+
var clean = value.slice(0, 2) === '0x' ? value.slice(2) : value
|
|
129
|
+
if (clean.length % 2 !== 0 || !HEX_RE.test(clean)) throw new Error(name + ' must be a hex string')
|
|
130
|
+
return Buffer.from(clean, 'hex')
|
|
131
|
+
}
|
|
132
|
+
if (encoding === Certificate.ENCODING.BASE64) {
|
|
133
|
+
if (!BASE64_RE.test(value)) throw new Error(name + ' must be base64')
|
|
134
|
+
return Buffer.from(value, 'base64')
|
|
135
|
+
}
|
|
136
|
+
throw new Error('encoding must be "hex" or "base64", not ' + JSON.stringify(encoding))
|
|
47
137
|
}
|
|
48
138
|
|
|
49
|
-
function
|
|
139
|
+
function encodeBytes (buf, encoding, name) {
|
|
50
140
|
if (!Buffer.isBuffer(buf)) {
|
|
51
141
|
throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
|
|
52
142
|
}
|
|
143
|
+
return encoding === Certificate.ENCODING.BASE64 ? buf.toString('base64') : buf.toString('hex')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A 32-byte hash field as lowercase hex, from a Buffer or a hex string. */
|
|
147
|
+
function hashHex (value, name) {
|
|
148
|
+
var buf = Buffer.isBuffer(value) ? value : Certificate.decodeBytes(value, 'hex', name)
|
|
149
|
+
if (buf.length !== 32) throw new Error(name + ' must be 32 bytes')
|
|
53
150
|
return buf.toString('hex')
|
|
54
151
|
}
|
|
55
152
|
|
|
153
|
+
function resolveMode (mode) {
|
|
154
|
+
if (mode === Certificate.MODE.FULL || mode === Certificate.MODE.HYBRID) {
|
|
155
|
+
return { mode: mode, batch: false }
|
|
156
|
+
}
|
|
157
|
+
// The numeric on-chain mode bytes, which is what 8.3.0–9.8.0 took here.
|
|
158
|
+
if (mode === NotaryScript.MODE.FULL) return { mode: 'full', batch: false }
|
|
159
|
+
if (mode === NotaryScript.MODE.HYBRID) return { mode: 'hybrid', batch: false }
|
|
160
|
+
if (mode === NotaryScript.MODE.BATCH) return { mode: 'full', batch: true }
|
|
161
|
+
if (mode === 'batch') {
|
|
162
|
+
throw new Error('batch is an anchor type, not a mode: pass mode "full" or "hybrid" with a merkle proof')
|
|
163
|
+
}
|
|
164
|
+
throw new Error('mode is required: "full" or "hybrid"')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveEncoding (encoding) {
|
|
168
|
+
if (encoding === undefined) return Certificate.ENCODING.HEX
|
|
169
|
+
if (encoding === Certificate.ENCODING.HEX || encoding === Certificate.ENCODING.BASE64) {
|
|
170
|
+
return encoding
|
|
171
|
+
}
|
|
172
|
+
// The legacy values named the signature's BYTE format. Both were written as hex, so
|
|
173
|
+
// both mean "hex" in the reference format.
|
|
174
|
+
if (encoding === Certificate.ENCODING.RAW || encoding === Certificate.ENCODING.DER) {
|
|
175
|
+
return Certificate.ENCODING.HEX
|
|
176
|
+
}
|
|
177
|
+
throw new Error('encoding must be "hex" or "base64"')
|
|
178
|
+
}
|
|
179
|
+
|
|
56
180
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* does not match the fields beside it. A certificate whose stated proofHash disagrees
|
|
61
|
-
* with its own contents is exactly the artefact this protocol exists to make impossible,
|
|
62
|
-
* and accepting the value would let one be constructed by mistake.
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} params
|
|
65
|
-
* @param {Number} params.mode - NotaryScript.MODE.*
|
|
66
|
-
* @param {String} params.algorithm
|
|
67
|
-
* @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
|
|
68
|
-
* @param {Buffer} params.payloadHash - raw 32 bytes
|
|
69
|
-
* @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
|
|
70
|
-
* @param {Buffer} params.signature - raw bytes, FULL even in hybrid mode
|
|
71
|
-
* @param {String} [params.encoding='raw']
|
|
72
|
-
* @param {String|Date} [params.createdAt] - defaults to now
|
|
73
|
-
* @param {Object} params.anchor - { txid, blockHeight }
|
|
74
|
-
* @param {Object} [params.merkle] - batch mode: { root, leafIndex, leafCount, path }
|
|
75
|
-
* @returns {Object} certificate
|
|
181
|
+
* An audit path in the `{ hash, side }` form certificates carry, from either that form
|
|
182
|
+
* or the bare hashes 8.3.0–9.8.0 wrote. Bare hashes get their sides from the index and
|
|
183
|
+
* tree size, which is exactly how RFC 6962 determines them.
|
|
76
184
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
185
|
+
function sidedPath (path, leafIndex, leafCount) {
|
|
186
|
+
if (!Array.isArray(path)) throw new Error('merkle.path must be an array')
|
|
187
|
+
var sided = path.length > 0 && path.every(function (n) {
|
|
188
|
+
return n && typeof n === 'object' && !Buffer.isBuffer(n) && 'side' in n
|
|
189
|
+
})
|
|
190
|
+
if (sided || path.length === 0) {
|
|
191
|
+
return path.map(function (n, i) {
|
|
192
|
+
if (n.side !== 'left' && n.side !== 'right') {
|
|
193
|
+
throw new Error('merkle.path[' + i + '].side must be "left" or "right"')
|
|
194
|
+
}
|
|
195
|
+
return { hash: hashHex(n.hash, 'merkle.path[' + i + '].hash'), side: n.side }
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
var sides = Merkle.pathSides(leafIndex, leafCount)
|
|
199
|
+
if (sides.length !== path.length) {
|
|
200
|
+
throw new Error('merkle.path has ' + path.length + ' nodes; a tree of ' + leafCount +
|
|
201
|
+
' leaves needs ' + sides.length + ' for leaf ' + leafIndex)
|
|
202
|
+
}
|
|
203
|
+
return path.map(function (n, i) {
|
|
204
|
+
return { hash: hashHex(n, 'merkle.path[' + i + ']'), side: sides[i] }
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildMerkle (merkle) {
|
|
209
|
+
$.checkArgument(merkle && typeof merkle === 'object',
|
|
210
|
+
'batch certificates require a merkle inclusion proof')
|
|
211
|
+
$.checkArgument(Number.isInteger(merkle.leafIndex) && merkle.leafIndex >= 0,
|
|
212
|
+
'merkle.leafIndex must be a non-negative integer')
|
|
213
|
+
$.checkArgument(Number.isInteger(merkle.leafCount) && merkle.leafCount > merkle.leafIndex,
|
|
214
|
+
'merkle.leafCount must be an integer greater than leafIndex')
|
|
215
|
+
return {
|
|
216
|
+
root: hashHex(merkle.root, 'merkle.root'),
|
|
217
|
+
leafIndex: merkle.leafIndex,
|
|
218
|
+
leafCount: merkle.leafCount,
|
|
219
|
+
path: sidedPath(merkle.path, merkle.leafIndex, merkle.leafCount)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildAnchor (anchor, batch) {
|
|
224
|
+
$.checkArgument(anchor && typeof anchor === 'object', 'anchor is required')
|
|
225
|
+
$.checkArgument(typeof anchor.txid === 'string', 'anchor.txid must be a string')
|
|
226
|
+
var type = anchor.type || (batch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT)
|
|
227
|
+
$.checkArgument(type === Certificate.ANCHOR_TYPE.DIRECT || type === Certificate.ANCHOR_TYPE.BATCH,
|
|
228
|
+
'anchor.type must be "direct" or "batch"')
|
|
229
|
+
$.checkArgument((type === Certificate.ANCHOR_TYPE.BATCH) === batch,
|
|
230
|
+
'anchor.type is "batch" exactly when a merkle inclusion proof is given')
|
|
231
|
+
return {
|
|
232
|
+
type: type,
|
|
233
|
+
network: anchor.network || Certificate.DEFAULT_NETWORK,
|
|
234
|
+
txid: anchor.txid,
|
|
235
|
+
vout: anchor.vout === undefined ? 0 : anchor.vout,
|
|
236
|
+
blockHeight: anchor.blockHeight === undefined ? null : anchor.blockHeight,
|
|
237
|
+
blockTime: anchor.blockTime === undefined ? null : anchor.blockTime
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The reference format. `createdAt` is written the way the reference writes it — the
|
|
243
|
+
* whole seconds that go into proofHash, as ISO 8601 — so two implementations given the
|
|
244
|
+
* same proof produce the same JSON.
|
|
245
|
+
*/
|
|
246
|
+
function buildReference (params) {
|
|
247
|
+
var resolved = resolveMode(params.mode)
|
|
248
|
+
var batch = resolved.batch || params.merkle !== undefined ||
|
|
249
|
+
!!(params.anchor && params.anchor.type === Certificate.ANCHOR_TYPE.BATCH)
|
|
250
|
+
if (batch) {
|
|
251
|
+
$.checkArgument(params.merkle && typeof params.merkle === 'object',
|
|
252
|
+
'batch certificates require a merkle inclusion proof')
|
|
253
|
+
}
|
|
254
|
+
var encoding = resolveEncoding(params.encoding)
|
|
255
|
+
var anchor = buildAnchor(params.anchor, batch)
|
|
256
|
+
|
|
257
|
+
var createdAtUnix = params.createdAtUnix !== undefined
|
|
258
|
+
? params.createdAtUnix
|
|
259
|
+
: Encoding.toUnixSeconds(params.createdAt === undefined ? new Date() : params.createdAt)
|
|
260
|
+
$.checkArgument(Number.isInteger(createdAtUnix) && createdAtUnix >= 0,
|
|
261
|
+
'createdAtUnix must be a non-negative whole number of seconds')
|
|
262
|
+
|
|
263
|
+
var proofHash = Encoding.proofHash({
|
|
264
|
+
algorithm: params.algorithm,
|
|
265
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
266
|
+
payloadHash: params.payloadHash,
|
|
267
|
+
publicKey: params.publicKey,
|
|
268
|
+
signature: params.signature,
|
|
269
|
+
createdAtUnix: createdAtUnix
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
var certificate = {
|
|
273
|
+
protocol: Certificate.PROTOCOL,
|
|
274
|
+
version: Certificate.REFERENCE_VERSION,
|
|
275
|
+
mode: resolved.mode,
|
|
276
|
+
algorithm: params.algorithm,
|
|
277
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
278
|
+
payloadHash: encodeBytes(params.payloadHash, 'hex', 'payloadHash'),
|
|
279
|
+
publicKey: encodeBytes(params.publicKey, encoding, 'publicKey'),
|
|
280
|
+
signature: encodeBytes(params.signature, encoding, 'signature'),
|
|
281
|
+
encoding: encoding,
|
|
282
|
+
proofHash: proofHash.toString('hex'),
|
|
283
|
+
createdAt: new Date(createdAtUnix * 1000).toISOString(),
|
|
284
|
+
anchor: anchor
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (batch) certificate.merkle = buildMerkle(params.merkle)
|
|
288
|
+
|
|
289
|
+
return certificate
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function legacyHex (buf, name) {
|
|
293
|
+
if (!Buffer.isBuffer(buf)) {
|
|
294
|
+
throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
|
|
295
|
+
}
|
|
296
|
+
return buf.toString('hex')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The legacy format: 9.8.0's build(), unchanged, so a caller on the 9.x default gets the
|
|
301
|
+
* same certificate byte for byte. test/notaryhash/certificate.js checks that against
|
|
302
|
+
* certificates the 9.8.0 code itself built. Do not tidy this — its quirks (the mode
|
|
303
|
+
* passed through as given, createdAt kept as supplied) are what 9.8.0 wrote.
|
|
304
|
+
*/
|
|
305
|
+
function buildLegacy (params) {
|
|
79
306
|
$.checkArgument(params.anchor && typeof params.anchor === 'object', 'anchor is required')
|
|
80
307
|
$.checkArgument(typeof params.anchor.txid === 'string', 'anchor.txid must be a string')
|
|
81
308
|
|
|
@@ -86,7 +313,6 @@ Certificate.build = function (params) {
|
|
|
86
313
|
var createdAt = params.createdAt
|
|
87
314
|
? (params.createdAt instanceof Date ? params.createdAt.toISOString() : params.createdAt)
|
|
88
315
|
: new Date().toISOString()
|
|
89
|
-
|
|
90
316
|
var createdAtUnix = Encoding.toUnixSeconds(createdAt)
|
|
91
317
|
|
|
92
318
|
var proofHash = Encoding.proofHash({
|
|
@@ -100,13 +326,13 @@ Certificate.build = function (params) {
|
|
|
100
326
|
|
|
101
327
|
var certificate = {
|
|
102
328
|
protocol: Certificate.PROTOCOL,
|
|
103
|
-
version:
|
|
329
|
+
version: LEGACY_VERSION,
|
|
104
330
|
mode: params.mode,
|
|
105
331
|
algorithm: params.algorithm,
|
|
106
332
|
hashAlgorithm: params.hashAlgorithm,
|
|
107
|
-
payloadHash:
|
|
108
|
-
publicKey:
|
|
109
|
-
signature:
|
|
333
|
+
payloadHash: legacyHex(params.payloadHash, 'payloadHash'),
|
|
334
|
+
publicKey: legacyHex(params.publicKey, 'publicKey'),
|
|
335
|
+
signature: legacyHex(params.signature, 'signature'),
|
|
110
336
|
encoding: encoding,
|
|
111
337
|
proofHash: proofHash.toString('hex'),
|
|
112
338
|
createdAt: createdAt,
|
|
@@ -130,35 +356,169 @@ Certificate.build = function (params) {
|
|
|
130
356
|
return certificate
|
|
131
357
|
}
|
|
132
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Build a certificate.
|
|
361
|
+
*
|
|
362
|
+
* `proofHash` is computed here rather than accepted, so a caller cannot supply one that
|
|
363
|
+
* does not match the fields beside it.
|
|
364
|
+
*
|
|
365
|
+
* @param {Object} params
|
|
366
|
+
* @param {String} [params.format] - 'reference' writes the BRC-220 reference format.
|
|
367
|
+
* Omitted, 9.x writes the 8.3.0–9.8.0 format and warns once; the default becomes
|
|
368
|
+
* 'reference' in 10.0.0. 'legacy' pins the old format without the notice.
|
|
369
|
+
* @param {String|Number} params.mode - reference: 'full' | 'hybrid' (the numeric
|
|
370
|
+
* NotaryScript.MODE values are still accepted; MODE.BATCH means a full proof on a
|
|
371
|
+
* batch anchor). legacy: a NotaryScript.MODE value.
|
|
372
|
+
* @param {String} params.algorithm
|
|
373
|
+
* @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
|
|
374
|
+
* @param {Buffer} params.payloadHash - raw 32 bytes
|
|
375
|
+
* @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
|
|
376
|
+
* @param {Buffer} params.signature - raw bytes as the signer produced them
|
|
377
|
+
* @param {String} [params.encoding] - reference: 'hex' (default) | 'base64'.
|
|
378
|
+
* legacy: 'raw' (default) | 'der'
|
|
379
|
+
* @param {String|Date} [params.createdAt] - defaults to now
|
|
380
|
+
* @param {Number} [params.createdAtUnix] - reference only; whole seconds
|
|
381
|
+
* @param {Object} params.anchor - reference: { txid, vout?, network?, blockHeight?,
|
|
382
|
+
* blockTime?, type? }. legacy: { txid, blockHeight }
|
|
383
|
+
* @param {Object} [params.merkle] - batch: { root, leafIndex, leafCount, path }. In the
|
|
384
|
+
* reference format path is Merkle.auditPath() output; bare Merkle.path() hashes are
|
|
385
|
+
* converted.
|
|
386
|
+
* @returns {Object} certificate
|
|
387
|
+
*/
|
|
388
|
+
Certificate.build = function (params) {
|
|
389
|
+
$.checkArgument(params && typeof params === 'object', 'params is required')
|
|
390
|
+
|
|
391
|
+
var format = params.format
|
|
392
|
+
if (format === undefined) {
|
|
393
|
+
// Per STABILITY.md: mark it in a minor, flip it in a major. 9.x does not change what
|
|
394
|
+
// build() returns, so the default stays the legacy format and the notice carries the
|
|
395
|
+
// migration — as LTP.Claim does for its canonicalization.
|
|
396
|
+
deprecate({
|
|
397
|
+
what: 'NotaryHash.Certificate.build() without a format',
|
|
398
|
+
since: '9.9.0',
|
|
399
|
+
removeIn: '10.0.0',
|
|
400
|
+
use: "NotaryHash.Certificate.build({ ...params, format: 'reference' })",
|
|
401
|
+
why: 'the default format is this library\'s own, which the BRC-220 reference implementation cannot verify'
|
|
402
|
+
})
|
|
403
|
+
return buildLegacy(params)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// An unrecognised value throws rather than falling back: a typo quietly selecting the
|
|
407
|
+
// legacy format would recreate the exact failure the format option exists to remove.
|
|
408
|
+
$.checkArgument(format === Certificate.FORMAT.REFERENCE || format === Certificate.FORMAT.LEGACY,
|
|
409
|
+
'Unknown certificate format: ' + format +
|
|
410
|
+
". Expected 'reference' or 'legacy' (NotaryHash.Certificate.FORMAT)")
|
|
411
|
+
|
|
412
|
+
return format === Certificate.FORMAT.REFERENCE ? buildReference(params) : buildLegacy(params)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Is this a certificate in the 8.3.0–9.8.0 format?
|
|
417
|
+
*
|
|
418
|
+
* `version: 1` marks one. So does a numeric `mode` with no version at all: the reference
|
|
419
|
+
* format never has a numeric mode, and 9.8.0's verifyBatchInclusion accepted such objects.
|
|
420
|
+
*
|
|
421
|
+
* @param {Object} certificate
|
|
422
|
+
* @returns {Boolean}
|
|
423
|
+
*/
|
|
424
|
+
Certificate.isLegacy = function (certificate) {
|
|
425
|
+
return !!certificate && typeof certificate === 'object' &&
|
|
426
|
+
(certificate.version === LEGACY_VERSION ||
|
|
427
|
+
(certificate.version === undefined && typeof certificate.mode === 'number'))
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Map a certificate onto the reference format.
|
|
432
|
+
*
|
|
433
|
+
* A legacy certificate is translated: `mode` 0/1/2 becomes 'full'/'hybrid'/'full' on a
|
|
434
|
+
* batch anchor, `encoding` "raw"/"der" becomes "hex" (both were hex), the anchor gains
|
|
435
|
+
* the fields the reference requires, and a bare-hash Merkle path gains its sides. A
|
|
436
|
+
* missing version stays missing, so validateShape still reports it. proofHash is
|
|
437
|
+
* untouched: it is over the canonical proof bytes, which the translation does not change.
|
|
438
|
+
*
|
|
439
|
+
* Anything else is returned as it was given, for validateShape to judge. Normalising
|
|
440
|
+
* only the format this library actually wrote keeps the two from blurring into a third
|
|
441
|
+
* that nobody writes.
|
|
442
|
+
*
|
|
443
|
+
* @param {Object} certificate
|
|
444
|
+
* @returns {Object} the reference-format certificate (a new object if translated)
|
|
445
|
+
*/
|
|
446
|
+
Certificate.normalize = function (certificate) {
|
|
447
|
+
if (!Certificate.isLegacy(certificate)) return certificate
|
|
448
|
+
|
|
449
|
+
var legacyBatch = certificate.mode === NotaryScript.MODE.BATCH
|
|
450
|
+
var out = Object.assign({}, certificate)
|
|
451
|
+
out.version = certificate.version === LEGACY_VERSION ? Certificate.REFERENCE_VERSION : certificate.version
|
|
452
|
+
out.mode = LEGACY_MODE[certificate.mode] !== undefined ? LEGACY_MODE[certificate.mode] : certificate.mode
|
|
453
|
+
if (certificate.encoding === Certificate.ENCODING.RAW || certificate.encoding === Certificate.ENCODING.DER) {
|
|
454
|
+
out.encoding = Certificate.ENCODING.HEX
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
var a = certificate.anchor && typeof certificate.anchor === 'object' ? certificate.anchor : {}
|
|
458
|
+
out.anchor = {
|
|
459
|
+
type: legacyBatch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT,
|
|
460
|
+
network: a.network || Certificate.DEFAULT_NETWORK,
|
|
461
|
+
txid: a.txid,
|
|
462
|
+
vout: a.vout === undefined ? 0 : a.vout,
|
|
463
|
+
blockHeight: a.blockHeight === undefined ? null : a.blockHeight,
|
|
464
|
+
blockTime: a.blockTime === undefined ? null : a.blockTime
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (certificate.merkle && typeof certificate.merkle === 'object') {
|
|
468
|
+
var m = certificate.merkle
|
|
469
|
+
var path = m.path
|
|
470
|
+
try {
|
|
471
|
+
path = sidedPath(m.path, m.leafIndex, m.leafCount)
|
|
472
|
+
} catch (e) {
|
|
473
|
+
// Left as it was: validateShape reports it, and verification then fails.
|
|
474
|
+
}
|
|
475
|
+
out.merkle = { root: m.root, leafIndex: m.leafIndex, leafCount: m.leafCount, path: path }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return out
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The raw proof fields a certificate's strings decode to — what the canonical proof
|
|
483
|
+
* bytes, the signature check and the on-chain record comparison all operate on.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object} certificate
|
|
486
|
+
* @returns {Object} { algorithm, hashAlgorithm, payloadHash, publicKey, signature, createdAtUnix }
|
|
487
|
+
*/
|
|
488
|
+
Certificate.toProofInput = function (certificate) {
|
|
489
|
+
$.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
|
|
490
|
+
var c = Certificate.normalize(certificate)
|
|
491
|
+
return {
|
|
492
|
+
algorithm: c.algorithm,
|
|
493
|
+
hashAlgorithm: c.hashAlgorithm,
|
|
494
|
+
payloadHash: Certificate.decodeBytes(c.payloadHash, 'hex', 'payloadHash'),
|
|
495
|
+
publicKey: Certificate.decodeBytes(c.publicKey, c.encoding, 'publicKey'),
|
|
496
|
+
signature: Certificate.decodeBytes(c.signature, c.encoding, 'signature'),
|
|
497
|
+
createdAtUnix: Encoding.toUnixSeconds(c.createdAt)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
133
501
|
/**
|
|
134
502
|
* Recompute the proofHash a certificate's own fields imply.
|
|
135
503
|
*
|
|
136
504
|
* This is validity check 2 of the three the spec requires, and it needs no network. It
|
|
137
|
-
* decodes the
|
|
138
|
-
* bytes, and
|
|
139
|
-
*
|
|
505
|
+
* decodes the string fields back to bytes first: the canonical proof bytes are over the
|
|
506
|
+
* raw bytes, and hashing the strings would produce a value that is wrong in a way that
|
|
507
|
+
* still looks like a hash.
|
|
140
508
|
*
|
|
141
509
|
* @param {Object} certificate
|
|
142
510
|
* @returns {Buffer} 32 bytes
|
|
143
511
|
*/
|
|
144
512
|
Certificate.recomputeProofHash = function (certificate) {
|
|
145
|
-
|
|
146
|
-
return Encoding.proofHash({
|
|
147
|
-
algorithm: certificate.algorithm,
|
|
148
|
-
hashAlgorithm: certificate.hashAlgorithm,
|
|
149
|
-
payloadHash: Buffer.from(certificate.payloadHash, 'hex'),
|
|
150
|
-
publicKey: Buffer.from(certificate.publicKey, 'hex'),
|
|
151
|
-
signature: Buffer.from(certificate.signature, 'hex'),
|
|
152
|
-
createdAtUnix: Encoding.toUnixSeconds(certificate.createdAt)
|
|
153
|
-
})
|
|
513
|
+
return Encoding.proofHash(Certificate.toProofInput(certificate))
|
|
154
514
|
}
|
|
155
515
|
|
|
156
516
|
/**
|
|
157
517
|
* Does the stated proofHash match the fields beside it?
|
|
158
518
|
*
|
|
159
519
|
* Strict boolean. Returns false rather than throwing on a malformed certificate, because
|
|
160
|
-
* "this certificate is not valid" is the honest answer to
|
|
161
|
-
*
|
|
520
|
+
* "this certificate is not valid" is the honest answer to one that cannot be parsed, and
|
|
521
|
+
* a caller writing `if (proofHashMatches(c))` must not get a truthy object.
|
|
162
522
|
*
|
|
163
523
|
* @param {Object} certificate
|
|
164
524
|
* @returns {Boolean}
|
|
@@ -166,7 +526,7 @@ Certificate.recomputeProofHash = function (certificate) {
|
|
|
166
526
|
Certificate.proofHashMatches = function (certificate) {
|
|
167
527
|
try {
|
|
168
528
|
if (!certificate || typeof certificate.proofHash !== 'string') return false
|
|
169
|
-
var stated =
|
|
529
|
+
var stated = Certificate.decodeBytes(certificate.proofHash, 'hex', 'proofHash')
|
|
170
530
|
if (stated.length !== 32) return false
|
|
171
531
|
return Certificate.recomputeProofHash(certificate).equals(stated)
|
|
172
532
|
} catch (e) {
|
|
@@ -180,46 +540,131 @@ Certificate.REQUIRED_FIELDS = [
|
|
|
180
540
|
'publicKey', 'signature', 'encoding', 'proofHash', 'createdAt', 'anchor'
|
|
181
541
|
]
|
|
182
542
|
|
|
543
|
+
function checkHash32 (value, name, problems) {
|
|
544
|
+
if (value === undefined) return
|
|
545
|
+
var clean = typeof value === 'string' && value.slice(0, 2) === '0x' ? value.slice(2) : value
|
|
546
|
+
if (typeof clean !== 'string' || !HEX_RE.test(clean) || clean.length % 2 !== 0) {
|
|
547
|
+
problems.push(name + ' must be a hex string')
|
|
548
|
+
} else if (clean.length !== 64) {
|
|
549
|
+
problems.push(name + ' must be 32 bytes (64 hex chars)')
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isIntegerOrNull (v) { return v === null || Number.isInteger(v) }
|
|
554
|
+
|
|
183
555
|
/**
|
|
184
556
|
* Check a certificate's SHAPE — that the required fields are present and well-formed.
|
|
185
557
|
*
|
|
186
558
|
* This is NOT verification. It says nothing about whether the signature is valid, whether
|
|
187
559
|
* the proofHash matches, or whether the anchor exists. It exists so that those checks can
|
|
188
560
|
* assume a parseable object, and it returns a list of problems rather than a boolean so
|
|
189
|
-
* the caller can report which field is wrong.
|
|
561
|
+
* the caller can report which field is wrong. A legacy certificate is normalised first,
|
|
562
|
+
* so it is judged in the reference format like any other.
|
|
190
563
|
*
|
|
191
564
|
* @param {Object} certificate
|
|
192
565
|
* @returns {Array<String>} problems; empty means the shape is fine
|
|
193
566
|
*/
|
|
194
567
|
Certificate.validateShape = function (certificate) {
|
|
195
|
-
var problems = []
|
|
196
|
-
|
|
197
568
|
if (!certificate || typeof certificate !== 'object') {
|
|
198
569
|
return ['certificate must be an object']
|
|
199
570
|
}
|
|
571
|
+
var c = Certificate.normalize(certificate)
|
|
572
|
+
var problems = []
|
|
200
573
|
|
|
201
574
|
Certificate.REQUIRED_FIELDS.forEach(function (field) {
|
|
202
|
-
if (
|
|
575
|
+
if (c[field] === undefined) problems.push('missing required field: ' + field)
|
|
203
576
|
})
|
|
204
577
|
|
|
205
|
-
if (
|
|
578
|
+
if (c.protocol !== undefined && c.protocol !== Certificate.PROTOCOL) {
|
|
206
579
|
problems.push('protocol must be "' + Certificate.PROTOCOL + '"')
|
|
207
580
|
}
|
|
208
|
-
if (
|
|
209
|
-
problems.push('unsupported version: ' +
|
|
581
|
+
if (c.version !== undefined && c.version !== Certificate.REFERENCE_VERSION) {
|
|
582
|
+
problems.push('unsupported version: ' + JSON.stringify(c.version))
|
|
583
|
+
}
|
|
584
|
+
if (c.mode !== undefined && c.mode !== Certificate.MODE.FULL && c.mode !== Certificate.MODE.HYBRID) {
|
|
585
|
+
problems.push('mode must be "full" or "hybrid"')
|
|
210
586
|
}
|
|
211
|
-
;[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
587
|
+
;['algorithm', 'hashAlgorithm'].forEach(function (field) {
|
|
588
|
+
if (c[field] !== undefined && (typeof c[field] !== 'string' || c[field].length === 0)) {
|
|
589
|
+
problems.push(field + ' must be a non-empty string')
|
|
590
|
+
}
|
|
591
|
+
})
|
|
592
|
+
checkHash32(c.payloadHash, 'payloadHash', problems)
|
|
593
|
+
checkHash32(c.proofHash, 'proofHash', problems)
|
|
594
|
+
|
|
595
|
+
if (c.encoding !== undefined) {
|
|
596
|
+
if (c.encoding !== Certificate.ENCODING.HEX && c.encoding !== Certificate.ENCODING.BASE64) {
|
|
597
|
+
problems.push('encoding must be "hex" or "base64"')
|
|
598
|
+
} else {
|
|
599
|
+
;['publicKey', 'signature'].forEach(function (field) {
|
|
600
|
+
if (c[field] === undefined) return
|
|
601
|
+
try {
|
|
602
|
+
if (Certificate.decodeBytes(c[field], c.encoding, field).length === 0) {
|
|
603
|
+
problems.push(field + ' must not be empty')
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
problems.push(field + ' is not valid ' + c.encoding)
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (c.createdAt !== undefined && (typeof c.createdAt !== 'string' || isNaN(new Date(c.createdAt).getTime()))) {
|
|
613
|
+
problems.push('createdAt must be an ISO 8601 date')
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (c.anchor !== undefined) {
|
|
617
|
+
var a = c.anchor
|
|
618
|
+
if (!a || typeof a !== 'object') {
|
|
619
|
+
problems.push('anchor must be an object')
|
|
620
|
+
} else {
|
|
621
|
+
if (a.type !== Certificate.ANCHOR_TYPE.DIRECT && a.type !== Certificate.ANCHOR_TYPE.BATCH) {
|
|
622
|
+
problems.push('anchor.type must be "direct" or "batch"')
|
|
218
623
|
}
|
|
219
|
-
|
|
624
|
+
if (typeof a.txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(a.txid)) {
|
|
625
|
+
problems.push('anchor.txid must be a 32-byte hex string')
|
|
626
|
+
}
|
|
627
|
+
if (a.vout !== undefined && !(Number.isInteger(a.vout) && a.vout >= 0)) {
|
|
628
|
+
problems.push('anchor.vout must be a non-negative integer')
|
|
629
|
+
}
|
|
630
|
+
if (a.blockHeight !== undefined && !isIntegerOrNull(a.blockHeight)) {
|
|
631
|
+
problems.push('anchor.blockHeight must be an integer or null')
|
|
632
|
+
}
|
|
633
|
+
if (a.blockTime !== undefined && !isIntegerOrNull(a.blockTime)) {
|
|
634
|
+
problems.push('anchor.blockTime must be an integer or null')
|
|
635
|
+
}
|
|
636
|
+
if (a.type === Certificate.ANCHOR_TYPE.BATCH && c.merkle === undefined) {
|
|
637
|
+
problems.push('batch certificates require a merkle inclusion proof')
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
220
641
|
|
|
221
|
-
if (
|
|
222
|
-
|
|
642
|
+
if (c.merkle !== undefined) {
|
|
643
|
+
var m = c.merkle
|
|
644
|
+
if (!m || typeof m !== 'object') {
|
|
645
|
+
problems.push('merkle must be an object')
|
|
646
|
+
} else {
|
|
647
|
+
checkHash32(m.root, 'merkle.root', problems)
|
|
648
|
+
if (!(Number.isInteger(m.leafIndex) && m.leafIndex >= 0)) {
|
|
649
|
+
problems.push('merkle.leafIndex must be a non-negative integer')
|
|
650
|
+
}
|
|
651
|
+
if (!(Number.isInteger(m.leafCount) && m.leafCount >= 1)) {
|
|
652
|
+
problems.push('merkle.leafCount must be a positive integer')
|
|
653
|
+
} else if (Number.isInteger(m.leafIndex) && m.leafIndex >= m.leafCount) {
|
|
654
|
+
problems.push('merkle.leafIndex must be less than leafCount')
|
|
655
|
+
}
|
|
656
|
+
if (!Array.isArray(m.path)) {
|
|
657
|
+
problems.push('merkle.path must be an array')
|
|
658
|
+
} else {
|
|
659
|
+
m.path.forEach(function (n, i) {
|
|
660
|
+
if (!n || typeof n !== 'object' || (n.side !== 'left' && n.side !== 'right')) {
|
|
661
|
+
problems.push('merkle.path[' + i + '] must be { hash, side: "left" | "right" }')
|
|
662
|
+
} else {
|
|
663
|
+
checkHash32(n.hash, 'merkle.path[' + i + '].hash', problems)
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
}
|
|
223
668
|
}
|
|
224
669
|
|
|
225
670
|
return problems
|