@smartledger/bsv 8.2.0 → 8.3.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 +84 -0
- package/README.md +38 -38
- package/bsv-ecies.min.js +1 -1
- package/bsv-gdaf.min.js +62 -62
- package/bsv-ltp.min.js +48 -48
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +62 -62
- package/bsv.min.js +62 -62
- package/docs/AUDIT_SCOPE.md +8 -8
- package/docs/BRC220_ENCODING_AMENDMENT.md +100 -0
- package/docs/BRC220_PLAN.md +224 -0
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/index.js +5 -0
- package/index.mjs +1 -0
- package/lib/gdaf/attestation-signer.js +2 -38
- package/lib/notaryhash/certificate.js +282 -0
- package/lib/notaryhash/encoding.js +150 -0
- package/lib/notaryhash/index.js +334 -0
- package/lib/notaryhash/merkle.js +209 -0
- package/lib/notaryhash/script.js +261 -0
- package/lib/notaryhash/suites.js +156 -0
- package/lib/util/jcs.js +75 -0
- package/package.json +7 -5
- package/test/notaryhash/certificate.js +249 -0
- package/test/notaryhash/encoding.js +186 -0
- package/test/notaryhash/merkle.js +181 -0
- package/test/notaryhash/script.js +270 -0
- package/test/notaryhash/verify.js +339 -0
- package/version.js +1 -1
|
@@ -159,17 +159,17 @@ const recovered = bsv.reconstructSecret([shares[0], shares[2], shares[4]]);
|
|
|
159
159
|
### **New Modular Options**
|
|
160
160
|
```html
|
|
161
161
|
<!-- Core compatibility (same size as bsv@1.5.6) -->
|
|
162
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
162
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv.min.js"></script>
|
|
163
163
|
|
|
164
164
|
<!-- Add smart contracts when ready -->
|
|
165
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
165
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-smartcontract.min.js"></script>
|
|
166
166
|
|
|
167
167
|
<!-- Add advanced features as needed -->
|
|
168
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
169
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
168
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-ltp.min.js"></script>
|
|
169
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-gdaf.min.js"></script>
|
|
170
170
|
|
|
171
171
|
<!-- Everything in one file -->
|
|
172
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
172
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv.bundle.js"></script>
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
## 🔍 **Testing Your Migration**
|
package/index.js
CHANGED
|
@@ -115,6 +115,11 @@ bsv.Block = require('./lib/block')
|
|
|
115
115
|
bsv.MerkleBlock = require('./lib/block/merkleblock')
|
|
116
116
|
bsv.BlockHeader = require('./lib/block/blockheader')
|
|
117
117
|
bsv.SPV = require('./lib/spv')
|
|
118
|
+
|
|
119
|
+
// BRC-220 NotaryHash. Exposed only now that verification exists: build/parse alone would
|
|
120
|
+
// let a caller construct a record whose proofHash means nothing, with nothing to stop
|
|
121
|
+
// them. See docs/BRC220_PLAN.md.
|
|
122
|
+
bsv.NotaryHash = require('./lib/notaryhash')
|
|
118
123
|
bsv.HDPrivateKey = require('./lib/hdprivatekey.js')
|
|
119
124
|
bsv.HDPublicKey = require('./lib/hdpublickey.js')
|
|
120
125
|
bsv.Networks = require('./lib/networks')
|
package/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ var Hash = require('../crypto/hash')
|
|
|
7
7
|
var ECDSA = require('../crypto/ecdsa')
|
|
8
8
|
var Signature = require('../crypto/signature')
|
|
9
9
|
var $ = require('../util/preconditions')
|
|
10
|
+
var JCS = require('../util/jcs')
|
|
10
11
|
var _ = require('../util/_')
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -124,44 +125,7 @@ AttestationSigner._canonicalizeJSON = function(obj) {
|
|
|
124
125
|
* @returns {String} RFC 8785 canonical JSON
|
|
125
126
|
*/
|
|
126
127
|
AttestationSigner._canonicalizeJCS = function (value) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
var type = typeof value
|
|
130
|
-
|
|
131
|
-
if (type === 'boolean') return value ? 'true' : 'false'
|
|
132
|
-
|
|
133
|
-
if (type === 'number') {
|
|
134
|
-
// JCS has no representation for these; refuse rather than emit `null`, which is what
|
|
135
|
-
// JSON.stringify does and which would silently sign a different document.
|
|
136
|
-
if (!isFinite(value)) {
|
|
137
|
-
throw new Error('Cannot canonicalize non-finite number: ' + value)
|
|
138
|
-
}
|
|
139
|
-
return JSON.stringify(value)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (type === 'string') return JSON.stringify(value)
|
|
143
|
-
|
|
144
|
-
if (Array.isArray(value)) {
|
|
145
|
-
// Arrays are order-significant. `undefined` has no JSON form and becomes null in an
|
|
146
|
-
// array, matching JSON.stringify.
|
|
147
|
-
return '[' + value.map(function (item) {
|
|
148
|
-
return item === undefined ? 'null' : AttestationSigner._canonicalizeJCS(item)
|
|
149
|
-
}).join(',') + ']'
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (type === 'object') {
|
|
153
|
-
var keys = Object.keys(value).filter(function (key) {
|
|
154
|
-
// Absent and explicitly-undefined are indistinguishable in JSON, so both are
|
|
155
|
-
// dropped — as JSON.stringify does.
|
|
156
|
-
return value[key] !== undefined && typeof value[key] !== 'function'
|
|
157
|
-
}).sort()
|
|
158
|
-
|
|
159
|
-
return '{' + keys.map(function (key) {
|
|
160
|
-
return JSON.stringify(key) + ':' + AttestationSigner._canonicalizeJCS(value[key])
|
|
161
|
-
}).join(',') + '}'
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
throw new Error('Cannot canonicalize value of type: ' + type)
|
|
128
|
+
return JCS.stringify(value)
|
|
165
129
|
}
|
|
166
130
|
|
|
167
131
|
/**
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
var JCS = require('../util/jcs')
|
|
4
|
+
var Encoding = require('./encoding')
|
|
5
|
+
var NotaryScript = require('./script')
|
|
6
|
+
var $ = require('../util/preconditions')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* BRC-220 certificate — the self-contained object a verifier is handed.
|
|
10
|
+
*
|
|
11
|
+
* Required fields, per the spec: protocol, version, mode, algorithm, hashAlgorithm,
|
|
12
|
+
* payloadHash, publicKey, signature, encoding, proofHash, createdAt, anchor. A batched
|
|
13
|
+
* certificate additionally carries `merkle`.
|
|
14
|
+
*
|
|
15
|
+
* Two things about it are easy to get wrong, and both are load-bearing:
|
|
16
|
+
*
|
|
17
|
+
* - The certificate carries the FULL publicKey and signature in every mode, including
|
|
18
|
+
* hybrid, where only their SHA-256 digests go on chain. That asymmetry is the point of
|
|
19
|
+
* hybrid mode: the chain stays small, the certificate stays complete.
|
|
20
|
+
* - The SPV envelope is ADDITIVE. The spec is explicit that attaching it never changes
|
|
21
|
+
* proofHash and never invalidates a previously issued certificate — which follows from
|
|
22
|
+
* proofHash being computed over the canonical proof bytes, not over the certificate
|
|
23
|
+
* JSON. A test asserts it rather than trusting the reasoning.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var Certificate = {}
|
|
27
|
+
|
|
28
|
+
Certificate.PROTOCOL = 'NotaryHash'
|
|
29
|
+
Certificate.VERSION = 1
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The byte representation of publicKey and signature.
|
|
33
|
+
*
|
|
34
|
+
* The spec requires the `encoding` field but never enumerates its values; this library
|
|
35
|
+
* defines them and proposes the definition upstream — see
|
|
36
|
+
* docs/BRC220_ENCODING_AMENDMENT.md.
|
|
37
|
+
*
|
|
38
|
+
* RAW is what everything should emit. For ECDSA-secp256k1 that is 64 bytes of r || s,
|
|
39
|
+
* each a 32-byte big-endian integer, with a 33-byte compressed public key. DER is
|
|
40
|
+
* accepted for Bitcoin-native signers that already hold it, and discouraged: DER is not
|
|
41
|
+
* canonical, so the same signature can encode to 69, 70 or 71 bytes, and those bytes are
|
|
42
|
+
* inside proofHash.
|
|
43
|
+
*/
|
|
44
|
+
Certificate.ENCODING = {
|
|
45
|
+
RAW: 'raw',
|
|
46
|
+
DER: 'der'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hex (buf, name) {
|
|
50
|
+
if (!Buffer.isBuffer(buf)) {
|
|
51
|
+
throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof buf))
|
|
52
|
+
}
|
|
53
|
+
return buf.toString('hex')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a certificate.
|
|
58
|
+
*
|
|
59
|
+
* `proofHash` is computed here rather than accepted, so a caller cannot supply one that
|
|
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
|
|
76
|
+
*/
|
|
77
|
+
Certificate.build = function (params) {
|
|
78
|
+
$.checkArgument(params && typeof params === 'object', 'params is required')
|
|
79
|
+
$.checkArgument(params.anchor && typeof params.anchor === 'object', 'anchor is required')
|
|
80
|
+
$.checkArgument(typeof params.anchor.txid === 'string', 'anchor.txid must be a string')
|
|
81
|
+
|
|
82
|
+
var encoding = params.encoding || Certificate.ENCODING.RAW
|
|
83
|
+
$.checkArgument(encoding === Certificate.ENCODING.RAW || encoding === Certificate.ENCODING.DER,
|
|
84
|
+
'encoding must be "raw" or "der"')
|
|
85
|
+
|
|
86
|
+
var createdAt = params.createdAt
|
|
87
|
+
? (params.createdAt instanceof Date ? params.createdAt.toISOString() : params.createdAt)
|
|
88
|
+
: new Date().toISOString()
|
|
89
|
+
|
|
90
|
+
var createdAtUnix = Encoding.toUnixSeconds(createdAt)
|
|
91
|
+
|
|
92
|
+
var proofHash = Encoding.proofHash({
|
|
93
|
+
algorithm: params.algorithm,
|
|
94
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
95
|
+
payloadHash: params.payloadHash,
|
|
96
|
+
publicKey: params.publicKey,
|
|
97
|
+
signature: params.signature,
|
|
98
|
+
createdAtUnix: createdAtUnix
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
var certificate = {
|
|
102
|
+
protocol: Certificate.PROTOCOL,
|
|
103
|
+
version: Certificate.VERSION,
|
|
104
|
+
mode: params.mode,
|
|
105
|
+
algorithm: params.algorithm,
|
|
106
|
+
hashAlgorithm: params.hashAlgorithm,
|
|
107
|
+
payloadHash: hex(params.payloadHash, 'payloadHash'),
|
|
108
|
+
publicKey: hex(params.publicKey, 'publicKey'),
|
|
109
|
+
signature: hex(params.signature, 'signature'),
|
|
110
|
+
encoding: encoding,
|
|
111
|
+
proofHash: proofHash.toString('hex'),
|
|
112
|
+
createdAt: createdAt,
|
|
113
|
+
anchor: {
|
|
114
|
+
txid: params.anchor.txid,
|
|
115
|
+
blockHeight: params.anchor.blockHeight
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (params.mode === NotaryScript.MODE.BATCH) {
|
|
120
|
+
$.checkArgument(params.merkle && typeof params.merkle === 'object',
|
|
121
|
+
'batch certificates require a merkle inclusion proof')
|
|
122
|
+
certificate.merkle = {
|
|
123
|
+
root: params.merkle.root,
|
|
124
|
+
leafIndex: params.merkle.leafIndex,
|
|
125
|
+
leafCount: params.merkle.leafCount,
|
|
126
|
+
path: params.merkle.path
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return certificate
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Recompute the proofHash a certificate's own fields imply.
|
|
135
|
+
*
|
|
136
|
+
* This is validity check 2 of the three the spec requires, and it needs no network. It
|
|
137
|
+
* decodes the hex fields back to bytes first: the canonical proof bytes are over the raw
|
|
138
|
+
* bytes, and hex is twice as long, so hashing the strings would produce a value that is
|
|
139
|
+
* wrong in a way that still looks like a hash.
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} certificate
|
|
142
|
+
* @returns {Buffer} 32 bytes
|
|
143
|
+
*/
|
|
144
|
+
Certificate.recomputeProofHash = function (certificate) {
|
|
145
|
+
$.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
|
|
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
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Does the stated proofHash match the fields beside it?
|
|
158
|
+
*
|
|
159
|
+
* Strict boolean. Returns false rather than throwing on a malformed certificate, because
|
|
160
|
+
* "this certificate is not valid" is the honest answer to a certificate that cannot be
|
|
161
|
+
* parsed, and a caller writing `if (proofHashMatches(c))` must not get a truthy object.
|
|
162
|
+
*
|
|
163
|
+
* @param {Object} certificate
|
|
164
|
+
* @returns {Boolean}
|
|
165
|
+
*/
|
|
166
|
+
Certificate.proofHashMatches = function (certificate) {
|
|
167
|
+
try {
|
|
168
|
+
if (!certificate || typeof certificate.proofHash !== 'string') return false
|
|
169
|
+
var stated = Buffer.from(certificate.proofHash, 'hex')
|
|
170
|
+
if (stated.length !== 32) return false
|
|
171
|
+
return Certificate.recomputeProofHash(certificate).equals(stated)
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Every field the spec requires of a certificate, in the order it lists them. */
|
|
178
|
+
Certificate.REQUIRED_FIELDS = [
|
|
179
|
+
'protocol', 'version', 'mode', 'algorithm', 'hashAlgorithm', 'payloadHash',
|
|
180
|
+
'publicKey', 'signature', 'encoding', 'proofHash', 'createdAt', 'anchor'
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check a certificate's SHAPE — that the required fields are present and well-formed.
|
|
185
|
+
*
|
|
186
|
+
* This is NOT verification. It says nothing about whether the signature is valid, whether
|
|
187
|
+
* the proofHash matches, or whether the anchor exists. It exists so that those checks can
|
|
188
|
+
* 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.
|
|
190
|
+
*
|
|
191
|
+
* @param {Object} certificate
|
|
192
|
+
* @returns {Array<String>} problems; empty means the shape is fine
|
|
193
|
+
*/
|
|
194
|
+
Certificate.validateShape = function (certificate) {
|
|
195
|
+
var problems = []
|
|
196
|
+
|
|
197
|
+
if (!certificate || typeof certificate !== 'object') {
|
|
198
|
+
return ['certificate must be an object']
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Certificate.REQUIRED_FIELDS.forEach(function (field) {
|
|
202
|
+
if (certificate[field] === undefined) problems.push('missing required field: ' + field)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
if (certificate.protocol !== undefined && certificate.protocol !== Certificate.PROTOCOL) {
|
|
206
|
+
problems.push('protocol must be "' + Certificate.PROTOCOL + '"')
|
|
207
|
+
}
|
|
208
|
+
if (certificate.version !== undefined && certificate.version !== Certificate.VERSION) {
|
|
209
|
+
problems.push('unsupported version: ' + certificate.version)
|
|
210
|
+
}
|
|
211
|
+
;[[certificate.payloadHash, 'payloadHash', 32], [certificate.proofHash, 'proofHash', 32]]
|
|
212
|
+
.forEach(function (pair) {
|
|
213
|
+
if (pair[0] === undefined) return
|
|
214
|
+
if (typeof pair[0] !== 'string' || !/^[0-9a-fA-F]*$/.test(pair[0])) {
|
|
215
|
+
problems.push(pair[1] + ' must be a hex string')
|
|
216
|
+
} else if (pair[0].length !== pair[2] * 2) {
|
|
217
|
+
problems.push(pair[1] + ' must be ' + pair[2] + ' bytes (' + pair[2] * 2 + ' hex chars)')
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
if (certificate.mode === NotaryScript.MODE.BATCH && certificate.merkle === undefined) {
|
|
222
|
+
problems.push('batch certificates require a merkle inclusion proof')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return problems
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Attach the SPV envelope to a finished certificate.
|
|
230
|
+
*
|
|
231
|
+
* Returns a NEW object rather than mutating: a certificate that has been handed to
|
|
232
|
+
* someone should not change under them, and a caller comparing before and after needs
|
|
233
|
+
* both.
|
|
234
|
+
*
|
|
235
|
+
* The spec guarantees this never changes proofHash, because proofHash is over the
|
|
236
|
+
* canonical proof bytes and the envelope is not among them. `attachSPV` asserts that
|
|
237
|
+
* rather than assuming it — if the guarantee ever broke, every previously issued
|
|
238
|
+
* certificate would become unverifiable, and it should break loudly here rather than
|
|
239
|
+
* quietly at a verifier.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object} certificate
|
|
242
|
+
* @param {Object} spv - { rawTx, blockHash, blockHeight, merkleProof, format }
|
|
243
|
+
* @returns {Object} a new certificate with `spv` attached
|
|
244
|
+
*/
|
|
245
|
+
Certificate.attachSPV = function (certificate, spv) {
|
|
246
|
+
$.checkArgument(certificate && typeof certificate === 'object', 'certificate is required')
|
|
247
|
+
$.checkArgument(spv && typeof spv === 'object', 'spv envelope is required')
|
|
248
|
+
|
|
249
|
+
var before = certificate.proofHash
|
|
250
|
+
|
|
251
|
+
var withSPV = Object.assign({}, certificate, {
|
|
252
|
+
spv: {
|
|
253
|
+
rawTx: spv.rawTx,
|
|
254
|
+
blockHash: spv.blockHash,
|
|
255
|
+
blockHeight: spv.blockHeight,
|
|
256
|
+
merkleProof: spv.merkleProof,
|
|
257
|
+
format: spv.format || 'TSC'
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (withSPV.proofHash !== before) {
|
|
262
|
+
throw new Error('attaching the SPV envelope changed proofHash; this must never happen')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return withSPV
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Canonical JSON for the certificate, per RFC 8785.
|
|
270
|
+
*
|
|
271
|
+
* Used for transport and for hashing the certificate itself. Note this is NOT what
|
|
272
|
+
* proofHash is computed over — that is the length-prefixed binary of the proof fields.
|
|
273
|
+
* Confusing the two produces a value that looks like a proofHash and is not one.
|
|
274
|
+
*
|
|
275
|
+
* @param {Object} certificate
|
|
276
|
+
* @returns {String}
|
|
277
|
+
*/
|
|
278
|
+
Certificate.canonicalize = function (certificate) {
|
|
279
|
+
return JCS.stringify(certificate)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = Certificate
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
var Hash = require('../crypto/hash')
|
|
4
|
+
var $ = require('../util/preconditions')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BRC-220 (NotaryHash) canonical binary encoding.
|
|
8
|
+
*
|
|
9
|
+
* The whole point of this file is that two independent implementations produce the same
|
|
10
|
+
* bytes. Everything here is fixed by the spec; nothing is a local convention.
|
|
11
|
+
*
|
|
12
|
+
* See docs/BRC220_PLAN.md for how this fits together.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var Encoding = {}
|
|
16
|
+
|
|
17
|
+
/** The protocol prefix that opens the canonical bytes. Verbatim from the spec. */
|
|
18
|
+
Encoding.PROTOCOL_PREFIX = 'NotaryHash/1.0'
|
|
19
|
+
|
|
20
|
+
/** Protocol version carried as a single byte inside the canonical bytes. */
|
|
21
|
+
Encoding.VERSION = 1
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Length-prefix a value: `lp(x) = u32be(len(x)) || x`.
|
|
25
|
+
*
|
|
26
|
+
* The length is the BYTE length, which is why strings are UTF-8 encoded first rather than
|
|
27
|
+
* measured with `.length` — those differ the moment a non-ASCII character appears, and
|
|
28
|
+
* the resulting proof would be unverifiable by anyone else.
|
|
29
|
+
*
|
|
30
|
+
* @param {Buffer|String} value - bytes, or text to encode as UTF-8
|
|
31
|
+
* @returns {Buffer}
|
|
32
|
+
*/
|
|
33
|
+
Encoding.lp = function (value) {
|
|
34
|
+
var buf = Buffer.isBuffer(value) ? value : Buffer.from(String(value), 'utf8')
|
|
35
|
+
var prefix = Buffer.alloc(4)
|
|
36
|
+
prefix.writeUInt32BE(buf.length, 0)
|
|
37
|
+
return Buffer.concat([prefix, buf])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Require a byte field to actually be bytes.
|
|
42
|
+
*
|
|
43
|
+
* Certificates carry `payloadHash`, `publicKey` and `signature` as HEX STRINGS, and the
|
|
44
|
+
* canonical bytes are over the RAW BYTES those strings decode to. Passing the hex string
|
|
45
|
+
* straight through would length-prefix 64 bytes where the spec wants 32 — a proofHash
|
|
46
|
+
* that is self-consistent, verifies against itself, and matches no other implementation.
|
|
47
|
+
* That is the failure this guard exists to make impossible, so hex must be decoded by the
|
|
48
|
+
* caller rather than guessed at here.
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
function requireBytes (value, name) {
|
|
53
|
+
if (!Buffer.isBuffer(value)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
name + ' must be a Buffer of raw bytes, not ' + (typeof value) +
|
|
56
|
+
'. Certificates store this field as hex; decode it with Buffer.from(hex, \'hex\') ' +
|
|
57
|
+
'before hashing, or the length prefix covers the wrong number of bytes.'
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encode a Unix timestamp as `u64be`.
|
|
65
|
+
*
|
|
66
|
+
* Seconds, not milliseconds. `Date.now()` is milliseconds, so a caller who forgets to
|
|
67
|
+
* divide produces a timestamp ~1000x in the future that still encodes cleanly — a test
|
|
68
|
+
* pins the boundary rather than trusting the caller.
|
|
69
|
+
*
|
|
70
|
+
* @param {Number} seconds - whole seconds since the Unix epoch
|
|
71
|
+
* @returns {Buffer} 8 bytes, big-endian
|
|
72
|
+
*/
|
|
73
|
+
Encoding.u64be = function (seconds) {
|
|
74
|
+
$.checkArgument(typeof seconds === 'number' && isFinite(seconds), 'createdAtUnix must be a finite number')
|
|
75
|
+
$.checkArgument(Number.isInteger(seconds), 'createdAtUnix must be whole seconds, not milliseconds or a fraction')
|
|
76
|
+
$.checkArgument(seconds >= 0, 'createdAtUnix must not be negative')
|
|
77
|
+
var buf = Buffer.alloc(8)
|
|
78
|
+
buf.writeBigUInt64BE(BigInt(seconds), 0)
|
|
79
|
+
return buf
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert an ISO 8601 timestamp to the whole seconds the canonical bytes carry.
|
|
84
|
+
*
|
|
85
|
+
* Truncates rather than rounds: a certificate's `createdAt` has millisecond precision and
|
|
86
|
+
* `createdAtUnix` does not, so two implementations must agree on which way the fractional
|
|
87
|
+
* part goes. Truncation is the only choice that never moves a timestamp forward.
|
|
88
|
+
*
|
|
89
|
+
* @param {String|Date} createdAt
|
|
90
|
+
* @returns {Number} whole seconds
|
|
91
|
+
*/
|
|
92
|
+
Encoding.toUnixSeconds = function (createdAt) {
|
|
93
|
+
var date = createdAt instanceof Date ? createdAt : new Date(createdAt)
|
|
94
|
+
$.checkArgument(!isNaN(date.getTime()), 'createdAt is not a valid date: ' + createdAt)
|
|
95
|
+
return Math.floor(date.getTime() / 1000)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the canonical proof bytes.
|
|
100
|
+
*
|
|
101
|
+
* lp("NotaryHash/1.0") || u8(version=1) ||
|
|
102
|
+
* lp(algorithm) || lp(hashAlgorithm) ||
|
|
103
|
+
* lp(payloadHash) || lp(publicKey) || lp(signature) ||
|
|
104
|
+
* u64be(createdAtUnix)
|
|
105
|
+
*
|
|
106
|
+
* The protocol prefix, the version byte and the timestamp are part of this and are NEVER
|
|
107
|
+
* part of the on-chain record. So the OP_RETURN output alone cannot reconstruct the
|
|
108
|
+
* proofHash — a verifier needs the certificate too. That looks like an omission when
|
|
109
|
+
* reading the on-chain format in isolation; it is deliberate, and it is what lets an SPV
|
|
110
|
+
* envelope be added afterwards without disturbing the hash.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} fields
|
|
113
|
+
* @param {String} fields.algorithm - e.g. 'ECDSA-secp256k1', 'ML-DSA-65'
|
|
114
|
+
* @param {String} fields.hashAlgorithm - e.g. 'SHA-256'
|
|
115
|
+
* @param {Buffer} fields.payloadHash - raw bytes
|
|
116
|
+
* @param {Buffer} fields.publicKey - raw bytes
|
|
117
|
+
* @param {Buffer} fields.signature - raw bytes
|
|
118
|
+
* @param {Number} fields.createdAtUnix - whole seconds
|
|
119
|
+
* @returns {Buffer}
|
|
120
|
+
*/
|
|
121
|
+
Encoding.canonicalBytes = function (fields) {
|
|
122
|
+
$.checkArgument(fields && typeof fields === 'object', 'fields object is required')
|
|
123
|
+
$.checkArgument(typeof fields.algorithm === 'string' && fields.algorithm.length > 0,
|
|
124
|
+
'algorithm must be a non-empty string')
|
|
125
|
+
$.checkArgument(typeof fields.hashAlgorithm === 'string' && fields.hashAlgorithm.length > 0,
|
|
126
|
+
'hashAlgorithm must be a non-empty string')
|
|
127
|
+
|
|
128
|
+
return Buffer.concat([
|
|
129
|
+
Encoding.lp(Encoding.PROTOCOL_PREFIX),
|
|
130
|
+
Buffer.from([Encoding.VERSION]),
|
|
131
|
+
Encoding.lp(fields.algorithm),
|
|
132
|
+
Encoding.lp(fields.hashAlgorithm),
|
|
133
|
+
Encoding.lp(requireBytes(fields.payloadHash, 'payloadHash')),
|
|
134
|
+
Encoding.lp(requireBytes(fields.publicKey, 'publicKey')),
|
|
135
|
+
Encoding.lp(requireBytes(fields.signature, 'signature')),
|
|
136
|
+
Encoding.u64be(fields.createdAtUnix)
|
|
137
|
+
])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The integrity root: `proofHash = SHA-256(canonicalBytes)`.
|
|
142
|
+
*
|
|
143
|
+
* @param {Object} fields - as for canonicalBytes()
|
|
144
|
+
* @returns {Buffer} 32 bytes
|
|
145
|
+
*/
|
|
146
|
+
Encoding.proofHash = function (fields) {
|
|
147
|
+
return Hash.sha256(Encoding.canonicalBytes(fields))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = Encoding
|