@smartledger/bsv 9.8.0 → 9.10.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 +129 -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 +345 -0
- package/bsv.min.js +59 -59
- package/docs/AUDIT_SCOPE.md +6 -6
- package/docs/BRC220_BATCH_LEAF_AMENDMENT.md +23 -8
- package/docs/BRC220_CERTIFICATE_FIELDS_AMENDMENT.md +246 -0
- package/docs/BRC220_ENCODING_AMENDMENT.md +40 -100
- 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 +528 -79
- package/lib/notaryhash/index.js +110 -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,311 @@
|
|
|
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
|
+
// Both RFC 4648 alphabets (§4 standard, §5 URL-safe) and missing padding are accepted, as
|
|
105
|
+
// the reference accepts them. What is NOT accepted is anything that is not base64: a
|
|
106
|
+
// character outside the alphabet, padding out of place, or a length no byte string
|
|
107
|
+
// encodes. Buffer.from skips the first and truncates the last, turning a corrupted field
|
|
108
|
+
// into different bytes instead of an 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
|
+
var body = value.replace(/=+$/, '')
|
|
135
|
+
if ((body.length !== value.length && value.length % 4 !== 0) || body.length % 4 === 1) {
|
|
136
|
+
throw new Error(name + ' is not a valid base64 length')
|
|
137
|
+
}
|
|
138
|
+
return Buffer.from(value, 'base64')
|
|
139
|
+
}
|
|
140
|
+
throw new Error('encoding must be "hex" or "base64", not ' + JSON.stringify(encoding))
|
|
47
141
|
}
|
|
48
142
|
|
|
49
|
-
function
|
|
143
|
+
function encodeBytes (buf, encoding, name) {
|
|
50
144
|
if (!Buffer.isBuffer(buf)) {
|
|
51
145
|
throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
|
|
52
146
|
}
|
|
147
|
+
return encoding === Certificate.ENCODING.BASE64 ? buf.toString('base64') : buf.toString('hex')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A 32-byte hash field as lowercase hex, from a Buffer or a hex string. */
|
|
151
|
+
function hashHex (value, name) {
|
|
152
|
+
var buf = Buffer.isBuffer(value) ? value : Certificate.decodeBytes(value, 'hex', name)
|
|
153
|
+
if (buf.length !== 32) throw new Error(name + ' must be 32 bytes')
|
|
53
154
|
return buf.toString('hex')
|
|
54
155
|
}
|
|
55
156
|
|
|
157
|
+
function resolveMode (mode) {
|
|
158
|
+
if (mode === Certificate.MODE.FULL || mode === Certificate.MODE.HYBRID) {
|
|
159
|
+
return { mode: mode, batch: false }
|
|
160
|
+
}
|
|
161
|
+
// The numeric on-chain mode bytes, which is what 8.3.0–9.8.0 took here.
|
|
162
|
+
if (mode === NotaryScript.MODE.FULL) return { mode: 'full', batch: false }
|
|
163
|
+
if (mode === NotaryScript.MODE.HYBRID) return { mode: 'hybrid', batch: false }
|
|
164
|
+
if (mode === NotaryScript.MODE.BATCH) return { mode: 'full', batch: true }
|
|
165
|
+
if (mode === 'batch') {
|
|
166
|
+
throw new Error('batch is an anchor type, not a mode: pass mode "full" or "hybrid" with a merkle proof')
|
|
167
|
+
}
|
|
168
|
+
throw new Error('mode is required: "full" or "hybrid"')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveEncoding (encoding) {
|
|
172
|
+
if (encoding === undefined) return Certificate.ENCODING.HEX
|
|
173
|
+
if (encoding === Certificate.ENCODING.HEX || encoding === Certificate.ENCODING.BASE64) {
|
|
174
|
+
return encoding
|
|
175
|
+
}
|
|
176
|
+
// The legacy values named the signature's BYTE format. Both were written as hex, so
|
|
177
|
+
// both mean "hex" in the reference format.
|
|
178
|
+
if (encoding === Certificate.ENCODING.RAW || encoding === Certificate.ENCODING.DER) {
|
|
179
|
+
return Certificate.ENCODING.HEX
|
|
180
|
+
}
|
|
181
|
+
throw new Error('encoding must be "hex" or "base64"')
|
|
182
|
+
}
|
|
183
|
+
|
|
56
184
|
/**
|
|
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
|
|
185
|
+
* An audit path in the `{ hash, side }` form certificates carry, from either that form
|
|
186
|
+
* or the bare hashes 8.3.0–9.8.0 wrote. Bare hashes get their sides from the index and
|
|
187
|
+
* tree size, which is exactly how RFC 6962 determines them.
|
|
76
188
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
189
|
+
function sidedPath (path, leafIndex, leafCount) {
|
|
190
|
+
if (!Array.isArray(path)) throw new Error('merkle.path must be an array')
|
|
191
|
+
var sided = path.length > 0 && path.every(function (n) {
|
|
192
|
+
return n && typeof n === 'object' && !Buffer.isBuffer(n) && 'side' in n
|
|
193
|
+
})
|
|
194
|
+
if (sided || path.length === 0) {
|
|
195
|
+
return path.map(function (n, i) {
|
|
196
|
+
if (n.side !== 'left' && n.side !== 'right') {
|
|
197
|
+
throw new Error('merkle.path[' + i + '].side must be "left" or "right"')
|
|
198
|
+
}
|
|
199
|
+
return { hash: hashHex(n.hash, 'merkle.path[' + i + '].hash'), side: n.side }
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
var sides = Merkle.pathSides(leafIndex, leafCount)
|
|
203
|
+
if (sides.length !== path.length) {
|
|
204
|
+
throw new Error('merkle.path has ' + path.length + ' nodes; a tree of ' + leafCount +
|
|
205
|
+
' leaves needs ' + sides.length + ' for leaf ' + leafIndex)
|
|
206
|
+
}
|
|
207
|
+
return path.map(function (n, i) {
|
|
208
|
+
return { hash: hashHex(n, 'merkle.path[' + i + ']'), side: sides[i] }
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildMerkle (merkle) {
|
|
213
|
+
$.checkArgument(merkle && typeof merkle === 'object',
|
|
214
|
+
'batch certificates require a merkle inclusion proof')
|
|
215
|
+
$.checkArgument(Number.isInteger(merkle.leafIndex) && merkle.leafIndex >= 0,
|
|
216
|
+
'merkle.leafIndex must be a non-negative integer')
|
|
217
|
+
$.checkArgument(Number.isInteger(merkle.leafCount) && merkle.leafCount > merkle.leafIndex,
|
|
218
|
+
'merkle.leafCount must be an integer greater than leafIndex')
|
|
219
|
+
return {
|
|
220
|
+
root: hashHex(merkle.root, 'merkle.root'),
|
|
221
|
+
leafIndex: merkle.leafIndex,
|
|
222
|
+
leafCount: merkle.leafCount,
|
|
223
|
+
path: sidedPath(merkle.path, merkle.leafIndex, merkle.leafCount)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildAnchor (anchor, batch) {
|
|
228
|
+
$.checkArgument(anchor && typeof anchor === 'object', 'anchor is required')
|
|
229
|
+
$.checkArgument(typeof anchor.txid === 'string', 'anchor.txid must be a string')
|
|
230
|
+
var type = anchor.type || (batch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT)
|
|
231
|
+
$.checkArgument(type === Certificate.ANCHOR_TYPE.DIRECT || type === Certificate.ANCHOR_TYPE.BATCH,
|
|
232
|
+
'anchor.type must be "direct" or "batch"')
|
|
233
|
+
$.checkArgument((type === Certificate.ANCHOR_TYPE.BATCH) === batch,
|
|
234
|
+
'anchor.type is "batch" exactly when a merkle inclusion proof is given')
|
|
235
|
+
return {
|
|
236
|
+
type: type,
|
|
237
|
+
network: anchor.network || Certificate.DEFAULT_NETWORK,
|
|
238
|
+
txid: anchor.txid,
|
|
239
|
+
vout: anchor.vout === undefined ? 0 : anchor.vout,
|
|
240
|
+
blockHeight: anchor.blockHeight === undefined ? null : anchor.blockHeight,
|
|
241
|
+
blockTime: anchor.blockTime === undefined ? null : anchor.blockTime
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The reference format. `createdAt` is written the way the reference writes it — the
|
|
247
|
+
* whole seconds that go into proofHash, as ISO 8601 — so two implementations given the
|
|
248
|
+
* same proof produce the same JSON.
|
|
249
|
+
*/
|
|
250
|
+
function buildReference (params) {
|
|
251
|
+
var resolved = resolveMode(params.mode)
|
|
252
|
+
var batch = resolved.batch || params.merkle !== undefined ||
|
|
253
|
+
!!(params.anchor && params.anchor.type === Certificate.ANCHOR_TYPE.BATCH)
|
|
254
|
+
if (batch) {
|
|
255
|
+
$.checkArgument(params.merkle && typeof params.merkle === 'object',
|
|
256
|
+
'batch certificates require a merkle inclusion proof')
|
|
257
|
+
}
|
|
258
|
+
var encoding = resolveEncoding(params.encoding)
|
|
259
|
+
var anchor = buildAnchor(params.anchor, batch)
|
|
260
|
+
|
|
261
|
+
var createdAtUnix = params.createdAtUnix !== undefined
|
|
262
|
+
? params.createdAtUnix
|
|
263
|
+
: Encoding.toUnixSeconds(params.createdAt === undefined ? new Date() : params.createdAt)
|
|
264
|
+
$.checkArgument(Number.isInteger(createdAtUnix) && createdAtUnix >= 0,
|
|
265
|
+
'createdAtUnix must be a non-negative whole number of seconds')
|
|
266
|
+
|
|
267
|
+
var proofHash = Encoding.proofHash({
|
|
268
|
+
algorithm: params.algorithm,
|
|
269
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
270
|
+
payloadHash: params.payloadHash,
|
|
271
|
+
publicKey: params.publicKey,
|
|
272
|
+
signature: params.signature,
|
|
273
|
+
createdAtUnix: createdAtUnix
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
var certificate = {
|
|
277
|
+
protocol: Certificate.PROTOCOL,
|
|
278
|
+
version: Certificate.REFERENCE_VERSION,
|
|
279
|
+
mode: resolved.mode,
|
|
280
|
+
algorithm: params.algorithm,
|
|
281
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
282
|
+
payloadHash: encodeBytes(params.payloadHash, 'hex', 'payloadHash'),
|
|
283
|
+
publicKey: encodeBytes(params.publicKey, encoding, 'publicKey'),
|
|
284
|
+
signature: encodeBytes(params.signature, encoding, 'signature'),
|
|
285
|
+
encoding: encoding,
|
|
286
|
+
proofHash: proofHash.toString('hex'),
|
|
287
|
+
createdAt: new Date(createdAtUnix * 1000).toISOString(),
|
|
288
|
+
anchor: anchor
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (batch) certificate.merkle = buildMerkle(params.merkle)
|
|
292
|
+
|
|
293
|
+
return certificate
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function legacyHex (buf, name) {
|
|
297
|
+
if (!Buffer.isBuffer(buf)) {
|
|
298
|
+
throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
|
|
299
|
+
}
|
|
300
|
+
return buf.toString('hex')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The legacy format: 9.8.0's build(), unchanged, so a caller on the 9.x default gets the
|
|
305
|
+
* same certificate byte for byte. test/notaryhash/certificate.js checks that against
|
|
306
|
+
* certificates the 9.8.0 code itself built. Do not tidy this — its quirks (the mode
|
|
307
|
+
* passed through as given, createdAt kept as supplied) are what 9.8.0 wrote.
|
|
308
|
+
*/
|
|
309
|
+
function buildLegacy (params) {
|
|
79
310
|
$.checkArgument(params.anchor && typeof params.anchor === 'object', 'anchor is required')
|
|
80
311
|
$.checkArgument(typeof params.anchor.txid === 'string', 'anchor.txid must be a string')
|
|
81
312
|
|
|
@@ -86,7 +317,6 @@ Certificate.build = function (params) {
|
|
|
86
317
|
var createdAt = params.createdAt
|
|
87
318
|
? (params.createdAt instanceof Date ? params.createdAt.toISOString() : params.createdAt)
|
|
88
319
|
: new Date().toISOString()
|
|
89
|
-
|
|
90
320
|
var createdAtUnix = Encoding.toUnixSeconds(createdAt)
|
|
91
321
|
|
|
92
322
|
var proofHash = Encoding.proofHash({
|
|
@@ -100,13 +330,13 @@ Certificate.build = function (params) {
|
|
|
100
330
|
|
|
101
331
|
var certificate = {
|
|
102
332
|
protocol: Certificate.PROTOCOL,
|
|
103
|
-
version:
|
|
333
|
+
version: LEGACY_VERSION,
|
|
104
334
|
mode: params.mode,
|
|
105
335
|
algorithm: params.algorithm,
|
|
106
336
|
hashAlgorithm: params.hashAlgorithm,
|
|
107
|
-
payloadHash:
|
|
108
|
-
publicKey:
|
|
109
|
-
signature:
|
|
337
|
+
payloadHash: legacyHex(params.payloadHash, 'payloadHash'),
|
|
338
|
+
publicKey: legacyHex(params.publicKey, 'publicKey'),
|
|
339
|
+
signature: legacyHex(params.signature, 'signature'),
|
|
110
340
|
encoding: encoding,
|
|
111
341
|
proofHash: proofHash.toString('hex'),
|
|
112
342
|
createdAt: createdAt,
|
|
@@ -130,35 +360,169 @@ Certificate.build = function (params) {
|
|
|
130
360
|
return certificate
|
|
131
361
|
}
|
|
132
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Build a certificate.
|
|
365
|
+
*
|
|
366
|
+
* `proofHash` is computed here rather than accepted, so a caller cannot supply one that
|
|
367
|
+
* does not match the fields beside it.
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} params
|
|
370
|
+
* @param {String} [params.format] - 'reference' writes the BRC-220 reference format.
|
|
371
|
+
* Omitted, 9.x writes the 8.3.0–9.8.0 format and warns once; the default becomes
|
|
372
|
+
* 'reference' in 10.0.0. 'legacy' pins the old format without the notice.
|
|
373
|
+
* @param {String|Number} params.mode - reference: 'full' | 'hybrid' (the numeric
|
|
374
|
+
* NotaryScript.MODE values are still accepted; MODE.BATCH means a full proof on a
|
|
375
|
+
* batch anchor). legacy: a NotaryScript.MODE value.
|
|
376
|
+
* @param {String} params.algorithm
|
|
377
|
+
* @param {String} params.hashAlgorithm - 'SHA-256' for every algorithm the spec lists
|
|
378
|
+
* @param {Buffer} params.payloadHash - raw 32 bytes
|
|
379
|
+
* @param {Buffer} params.publicKey - raw bytes, FULL even in hybrid mode
|
|
380
|
+
* @param {Buffer} params.signature - raw bytes as the signer produced them
|
|
381
|
+
* @param {String} [params.encoding] - reference: 'hex' (default) | 'base64'.
|
|
382
|
+
* legacy: 'raw' (default) | 'der'
|
|
383
|
+
* @param {String|Date} [params.createdAt] - defaults to now
|
|
384
|
+
* @param {Number} [params.createdAtUnix] - reference only; whole seconds
|
|
385
|
+
* @param {Object} params.anchor - reference: { txid, vout?, network?, blockHeight?,
|
|
386
|
+
* blockTime?, type? }. legacy: { txid, blockHeight }
|
|
387
|
+
* @param {Object} [params.merkle] - batch: { root, leafIndex, leafCount, path }. In the
|
|
388
|
+
* reference format path is Merkle.auditPath() output; bare Merkle.path() hashes are
|
|
389
|
+
* converted.
|
|
390
|
+
* @returns {Object} certificate
|
|
391
|
+
*/
|
|
392
|
+
Certificate.build = function (params) {
|
|
393
|
+
$.checkArgument(params && typeof params === 'object', 'params is required')
|
|
394
|
+
|
|
395
|
+
var format = params.format
|
|
396
|
+
if (format === undefined) {
|
|
397
|
+
// Per STABILITY.md: mark it in a minor, flip it in a major. 9.x does not change what
|
|
398
|
+
// build() returns, so the default stays the legacy format and the notice carries the
|
|
399
|
+
// migration — as LTP.Claim does for its canonicalization.
|
|
400
|
+
deprecate({
|
|
401
|
+
what: 'NotaryHash.Certificate.build() without a format',
|
|
402
|
+
since: '9.9.0',
|
|
403
|
+
removeIn: '10.0.0',
|
|
404
|
+
use: "NotaryHash.Certificate.build({ ...params, format: 'reference' })",
|
|
405
|
+
why: 'the default format is this library\'s own, which the BRC-220 reference implementation cannot verify'
|
|
406
|
+
})
|
|
407
|
+
return buildLegacy(params)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// An unrecognised value throws rather than falling back: a typo quietly selecting the
|
|
411
|
+
// legacy format would recreate the exact failure the format option exists to remove.
|
|
412
|
+
$.checkArgument(format === Certificate.FORMAT.REFERENCE || format === Certificate.FORMAT.LEGACY,
|
|
413
|
+
'Unknown certificate format: ' + format +
|
|
414
|
+
". Expected 'reference' or 'legacy' (NotaryHash.Certificate.FORMAT)")
|
|
415
|
+
|
|
416
|
+
return format === Certificate.FORMAT.REFERENCE ? buildReference(params) : buildLegacy(params)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Is this a certificate in the 8.3.0–9.8.0 format?
|
|
421
|
+
*
|
|
422
|
+
* `version: 1` marks one. So does a numeric `mode` with no version at all: the reference
|
|
423
|
+
* format never has a numeric mode, and 9.8.0's verifyBatchInclusion accepted such objects.
|
|
424
|
+
*
|
|
425
|
+
* @param {Object} certificate
|
|
426
|
+
* @returns {Boolean}
|
|
427
|
+
*/
|
|
428
|
+
Certificate.isLegacy = function (certificate) {
|
|
429
|
+
return !!certificate && typeof certificate === 'object' &&
|
|
430
|
+
(certificate.version === LEGACY_VERSION ||
|
|
431
|
+
(certificate.version === undefined && typeof certificate.mode === 'number'))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Map a certificate onto the reference format.
|
|
436
|
+
*
|
|
437
|
+
* A legacy certificate is translated: `mode` 0/1/2 becomes 'full'/'hybrid'/'full' on a
|
|
438
|
+
* batch anchor, `encoding` "raw"/"der" becomes "hex" (both were hex), the anchor gains
|
|
439
|
+
* the fields the reference requires, and a bare-hash Merkle path gains its sides. A
|
|
440
|
+
* missing version stays missing, so validateShape still reports it. proofHash is
|
|
441
|
+
* untouched: it is over the canonical proof bytes, which the translation does not change.
|
|
442
|
+
*
|
|
443
|
+
* Anything else is returned as it was given, for validateShape to judge. Normalising
|
|
444
|
+
* only the format this library actually wrote keeps the two from blurring into a third
|
|
445
|
+
* that nobody writes.
|
|
446
|
+
*
|
|
447
|
+
* @param {Object} certificate
|
|
448
|
+
* @returns {Object} the reference-format certificate (a new object if translated)
|
|
449
|
+
*/
|
|
450
|
+
Certificate.normalize = function (certificate) {
|
|
451
|
+
if (!Certificate.isLegacy(certificate)) return certificate
|
|
452
|
+
|
|
453
|
+
var legacyBatch = certificate.mode === NotaryScript.MODE.BATCH
|
|
454
|
+
var out = Object.assign({}, certificate)
|
|
455
|
+
out.version = certificate.version === LEGACY_VERSION ? Certificate.REFERENCE_VERSION : certificate.version
|
|
456
|
+
out.mode = LEGACY_MODE[certificate.mode] !== undefined ? LEGACY_MODE[certificate.mode] : certificate.mode
|
|
457
|
+
if (certificate.encoding === Certificate.ENCODING.RAW || certificate.encoding === Certificate.ENCODING.DER) {
|
|
458
|
+
out.encoding = Certificate.ENCODING.HEX
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
var a = certificate.anchor && typeof certificate.anchor === 'object' ? certificate.anchor : {}
|
|
462
|
+
out.anchor = {
|
|
463
|
+
type: legacyBatch ? Certificate.ANCHOR_TYPE.BATCH : Certificate.ANCHOR_TYPE.DIRECT,
|
|
464
|
+
network: a.network || Certificate.DEFAULT_NETWORK,
|
|
465
|
+
txid: a.txid,
|
|
466
|
+
vout: a.vout === undefined ? 0 : a.vout,
|
|
467
|
+
blockHeight: a.blockHeight === undefined ? null : a.blockHeight,
|
|
468
|
+
blockTime: a.blockTime === undefined ? null : a.blockTime
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (certificate.merkle && typeof certificate.merkle === 'object') {
|
|
472
|
+
var m = certificate.merkle
|
|
473
|
+
var path = m.path
|
|
474
|
+
try {
|
|
475
|
+
path = sidedPath(m.path, m.leafIndex, m.leafCount)
|
|
476
|
+
} catch (e) {
|
|
477
|
+
// Left as it was: validateShape reports it, and verification then fails.
|
|
478
|
+
}
|
|
479
|
+
out.merkle = { root: m.root, leafIndex: m.leafIndex, leafCount: m.leafCount, path: path }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return out
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The raw proof fields a certificate's strings decode to — what the canonical proof
|
|
487
|
+
* bytes, the signature check and the on-chain record comparison all operate on.
|
|
488
|
+
*
|
|
489
|
+
* @param {Object} certificate
|
|
490
|
+
* @returns {Object} { algorithm, hashAlgorithm, payloadHash, publicKey, signature, createdAtUnix }
|
|
491
|
+
*/
|
|
492
|
+
Certificate.toProofInput = function (certificate) {
|
|
493
|
+
$.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
|
|
494
|
+
var c = Certificate.normalize(certificate)
|
|
495
|
+
return {
|
|
496
|
+
algorithm: c.algorithm,
|
|
497
|
+
hashAlgorithm: c.hashAlgorithm,
|
|
498
|
+
payloadHash: Certificate.decodeBytes(c.payloadHash, 'hex', 'payloadHash'),
|
|
499
|
+
publicKey: Certificate.decodeBytes(c.publicKey, c.encoding, 'publicKey'),
|
|
500
|
+
signature: Certificate.decodeBytes(c.signature, c.encoding, 'signature'),
|
|
501
|
+
createdAtUnix: Encoding.toUnixSeconds(c.createdAt)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
133
505
|
/**
|
|
134
506
|
* Recompute the proofHash a certificate's own fields imply.
|
|
135
507
|
*
|
|
136
508
|
* This is validity check 2 of the three the spec requires, and it needs no network. It
|
|
137
|
-
* decodes the
|
|
138
|
-
* bytes, and
|
|
139
|
-
*
|
|
509
|
+
* decodes the string fields back to bytes first: the canonical proof bytes are over the
|
|
510
|
+
* raw bytes, and hashing the strings would produce a value that is wrong in a way that
|
|
511
|
+
* still looks like a hash.
|
|
140
512
|
*
|
|
141
513
|
* @param {Object} certificate
|
|
142
514
|
* @returns {Buffer} 32 bytes
|
|
143
515
|
*/
|
|
144
516
|
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
|
-
})
|
|
517
|
+
return Encoding.proofHash(Certificate.toProofInput(certificate))
|
|
154
518
|
}
|
|
155
519
|
|
|
156
520
|
/**
|
|
157
521
|
* Does the stated proofHash match the fields beside it?
|
|
158
522
|
*
|
|
159
523
|
* Strict boolean. Returns false rather than throwing on a malformed certificate, because
|
|
160
|
-
* "this certificate is not valid" is the honest answer to
|
|
161
|
-
*
|
|
524
|
+
* "this certificate is not valid" is the honest answer to one that cannot be parsed, and
|
|
525
|
+
* a caller writing `if (proofHashMatches(c))` must not get a truthy object.
|
|
162
526
|
*
|
|
163
527
|
* @param {Object} certificate
|
|
164
528
|
* @returns {Boolean}
|
|
@@ -166,7 +530,7 @@ Certificate.recomputeProofHash = function (certificate) {
|
|
|
166
530
|
Certificate.proofHashMatches = function (certificate) {
|
|
167
531
|
try {
|
|
168
532
|
if (!certificate || typeof certificate.proofHash !== 'string') return false
|
|
169
|
-
var stated =
|
|
533
|
+
var stated = Certificate.decodeBytes(certificate.proofHash, 'hex', 'proofHash')
|
|
170
534
|
if (stated.length !== 32) return false
|
|
171
535
|
return Certificate.recomputeProofHash(certificate).equals(stated)
|
|
172
536
|
} catch (e) {
|
|
@@ -180,46 +544,131 @@ Certificate.REQUIRED_FIELDS = [
|
|
|
180
544
|
'publicKey', 'signature', 'encoding', 'proofHash', 'createdAt', 'anchor'
|
|
181
545
|
]
|
|
182
546
|
|
|
547
|
+
function checkHash32 (value, name, problems) {
|
|
548
|
+
if (value === undefined) return
|
|
549
|
+
var clean = typeof value === 'string' && value.slice(0, 2) === '0x' ? value.slice(2) : value
|
|
550
|
+
if (typeof clean !== 'string' || !HEX_RE.test(clean) || clean.length % 2 !== 0) {
|
|
551
|
+
problems.push(name + ' must be a hex string')
|
|
552
|
+
} else if (clean.length !== 64) {
|
|
553
|
+
problems.push(name + ' must be 32 bytes (64 hex chars)')
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isIntegerOrNull (v) { return v === null || Number.isInteger(v) }
|
|
558
|
+
|
|
183
559
|
/**
|
|
184
560
|
* Check a certificate's SHAPE — that the required fields are present and well-formed.
|
|
185
561
|
*
|
|
186
562
|
* This is NOT verification. It says nothing about whether the signature is valid, whether
|
|
187
563
|
* the proofHash matches, or whether the anchor exists. It exists so that those checks can
|
|
188
564
|
* 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.
|
|
565
|
+
* the caller can report which field is wrong. A legacy certificate is normalised first,
|
|
566
|
+
* so it is judged in the reference format like any other.
|
|
190
567
|
*
|
|
191
568
|
* @param {Object} certificate
|
|
192
569
|
* @returns {Array<String>} problems; empty means the shape is fine
|
|
193
570
|
*/
|
|
194
571
|
Certificate.validateShape = function (certificate) {
|
|
195
|
-
var problems = []
|
|
196
|
-
|
|
197
572
|
if (!certificate || typeof certificate !== 'object') {
|
|
198
573
|
return ['certificate must be an object']
|
|
199
574
|
}
|
|
575
|
+
var c = Certificate.normalize(certificate)
|
|
576
|
+
var problems = []
|
|
200
577
|
|
|
201
578
|
Certificate.REQUIRED_FIELDS.forEach(function (field) {
|
|
202
|
-
if (
|
|
579
|
+
if (c[field] === undefined) problems.push('missing required field: ' + field)
|
|
203
580
|
})
|
|
204
581
|
|
|
205
|
-
if (
|
|
582
|
+
if (c.protocol !== undefined && c.protocol !== Certificate.PROTOCOL) {
|
|
206
583
|
problems.push('protocol must be "' + Certificate.PROTOCOL + '"')
|
|
207
584
|
}
|
|
208
|
-
if (
|
|
209
|
-
problems.push('unsupported version: ' +
|
|
585
|
+
if (c.version !== undefined && c.version !== Certificate.REFERENCE_VERSION) {
|
|
586
|
+
problems.push('unsupported version: ' + JSON.stringify(c.version))
|
|
587
|
+
}
|
|
588
|
+
if (c.mode !== undefined && c.mode !== Certificate.MODE.FULL && c.mode !== Certificate.MODE.HYBRID) {
|
|
589
|
+
problems.push('mode must be "full" or "hybrid"')
|
|
210
590
|
}
|
|
211
|
-
;[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
591
|
+
;['algorithm', 'hashAlgorithm'].forEach(function (field) {
|
|
592
|
+
if (c[field] !== undefined && (typeof c[field] !== 'string' || c[field].length === 0)) {
|
|
593
|
+
problems.push(field + ' must be a non-empty string')
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
checkHash32(c.payloadHash, 'payloadHash', problems)
|
|
597
|
+
checkHash32(c.proofHash, 'proofHash', problems)
|
|
598
|
+
|
|
599
|
+
if (c.encoding !== undefined) {
|
|
600
|
+
if (c.encoding !== Certificate.ENCODING.HEX && c.encoding !== Certificate.ENCODING.BASE64) {
|
|
601
|
+
problems.push('encoding must be "hex" or "base64"')
|
|
602
|
+
} else {
|
|
603
|
+
;['publicKey', 'signature'].forEach(function (field) {
|
|
604
|
+
if (c[field] === undefined) return
|
|
605
|
+
try {
|
|
606
|
+
if (Certificate.decodeBytes(c[field], c.encoding, field).length === 0) {
|
|
607
|
+
problems.push(field + ' must not be empty')
|
|
608
|
+
}
|
|
609
|
+
} catch (e) {
|
|
610
|
+
problems.push(field + ' is not valid ' + c.encoding)
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (c.createdAt !== undefined && (typeof c.createdAt !== 'string' || isNaN(new Date(c.createdAt).getTime()))) {
|
|
617
|
+
problems.push('createdAt must be an ISO 8601 date')
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (c.anchor !== undefined) {
|
|
621
|
+
var a = c.anchor
|
|
622
|
+
if (!a || typeof a !== 'object') {
|
|
623
|
+
problems.push('anchor must be an object')
|
|
624
|
+
} else {
|
|
625
|
+
if (a.type !== Certificate.ANCHOR_TYPE.DIRECT && a.type !== Certificate.ANCHOR_TYPE.BATCH) {
|
|
626
|
+
problems.push('anchor.type must be "direct" or "batch"')
|
|
218
627
|
}
|
|
219
|
-
|
|
628
|
+
if (typeof a.txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(a.txid)) {
|
|
629
|
+
problems.push('anchor.txid must be a 32-byte hex string')
|
|
630
|
+
}
|
|
631
|
+
if (a.vout !== undefined && !(Number.isInteger(a.vout) && a.vout >= 0)) {
|
|
632
|
+
problems.push('anchor.vout must be a non-negative integer')
|
|
633
|
+
}
|
|
634
|
+
if (a.blockHeight !== undefined && !isIntegerOrNull(a.blockHeight)) {
|
|
635
|
+
problems.push('anchor.blockHeight must be an integer or null')
|
|
636
|
+
}
|
|
637
|
+
if (a.blockTime !== undefined && !isIntegerOrNull(a.blockTime)) {
|
|
638
|
+
problems.push('anchor.blockTime must be an integer or null')
|
|
639
|
+
}
|
|
640
|
+
if (a.type === Certificate.ANCHOR_TYPE.BATCH && c.merkle === undefined) {
|
|
641
|
+
problems.push('batch certificates require a merkle inclusion proof')
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
220
645
|
|
|
221
|
-
if (
|
|
222
|
-
|
|
646
|
+
if (c.merkle !== undefined) {
|
|
647
|
+
var m = c.merkle
|
|
648
|
+
if (!m || typeof m !== 'object') {
|
|
649
|
+
problems.push('merkle must be an object')
|
|
650
|
+
} else {
|
|
651
|
+
checkHash32(m.root, 'merkle.root', problems)
|
|
652
|
+
if (!(Number.isInteger(m.leafIndex) && m.leafIndex >= 0)) {
|
|
653
|
+
problems.push('merkle.leafIndex must be a non-negative integer')
|
|
654
|
+
}
|
|
655
|
+
if (!(Number.isInteger(m.leafCount) && m.leafCount >= 1)) {
|
|
656
|
+
problems.push('merkle.leafCount must be a positive integer')
|
|
657
|
+
} else if (Number.isInteger(m.leafIndex) && m.leafIndex >= m.leafCount) {
|
|
658
|
+
problems.push('merkle.leafIndex must be less than leafCount')
|
|
659
|
+
}
|
|
660
|
+
if (!Array.isArray(m.path)) {
|
|
661
|
+
problems.push('merkle.path must be an array')
|
|
662
|
+
} else {
|
|
663
|
+
m.path.forEach(function (n, i) {
|
|
664
|
+
if (!n || typeof n !== 'object' || (n.side !== 'left' && n.side !== 'right')) {
|
|
665
|
+
problems.push('merkle.path[' + i + '] must be { hash, side: "left" | "right" }')
|
|
666
|
+
} else {
|
|
667
|
+
checkHash32(n.hash, 'merkle.path[' + i + '].hash', problems)
|
|
668
|
+
}
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
}
|
|
223
672
|
}
|
|
224
673
|
|
|
225
674
|
return problems
|