hyper-multisig 1.0.2 → 1.0.3

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/index.js CHANGED
@@ -27,7 +27,13 @@ const SignRequest = require('hypercore-signing-request')
27
27
  const Hyperdrive = require('hyperdrive')
28
28
  const z32 = require('z32')
29
29
 
30
- const MultisigUtil = require('./lib/util')
30
+ const { getCoreKey, getManifest, getCoreInfo } = require('./lib/core')
31
+ const { signCore, signDrive } = require('./lib/sign')
32
+ const {
33
+ verifyCoreRequestable,
34
+ verifyCoreCommittable,
35
+ verifyCoreCommitted
36
+ } = require('./lib/verify')
31
37
 
32
38
  class HyperMultisig {
33
39
  constructor(store, swarm) {
@@ -37,7 +43,7 @@ class HyperMultisig {
37
43
  this.swarm = swarm
38
44
  }
39
45
 
40
- static getCoreKey = MultisigUtil.getCoreKey
46
+ static getCoreKey = getCoreKey
41
47
 
42
48
  /**
43
49
  * @param {string[]} publicKeys
@@ -45,7 +51,7 @@ class HyperMultisig {
45
51
  * @return {Promise<{ manifest: Manifest, key: Buffer, core: Hypercore }>}
46
52
  */
47
53
  async createCore(publicKeys, namespace, { quorum } = {}) {
48
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
54
+ const manifest = getManifest(publicKeys, namespace, { quorum })
49
55
  const core = this.store.get({ manifest })
50
56
  await core.ready()
51
57
  return { manifest, key: core.key, core }
@@ -80,9 +86,9 @@ class HyperMultisig {
80
86
  await srcCore.ready()
81
87
  this.swarm.join(srcCore.discoveryKey, { client: true, server: false })
82
88
 
83
- if (!force) await MultisigUtil.verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
89
+ if (!force) await verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
84
90
 
85
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
91
+ const manifest = getManifest(publicKeys, namespace, { quorum })
86
92
  const request = await SignRequest.generate(srcCore, { manifest, length })
87
93
  return { manifest, request }
88
94
  })
@@ -104,7 +110,7 @@ class HyperMultisig {
104
110
 
105
111
  if (!force) {
106
112
  runner.emit('verify-db-requestable-start')
107
- await MultisigUtil.verifyCoreRequestable(srcDrive.core, length, {
113
+ await verifyCoreRequestable(srcDrive.core, length, {
108
114
  peerUpdateTimeout,
109
115
  coreId: 'db'
110
116
  })
@@ -113,14 +119,14 @@ class HyperMultisig {
113
119
  const contentLength = await srcDrive.getBlobsLength(length)
114
120
 
115
121
  runner.emit('verify-blobs-requestable-start')
116
- await MultisigUtil.verifyCoreRequestable(srcDrive.blobs.core, contentLength, {
122
+ await verifyCoreRequestable(srcDrive.blobs.core, contentLength, {
117
123
  peerUpdateTimeout,
118
124
  coreId: 'blobs'
119
125
  })
120
126
  }
121
127
 
122
128
  runner.emit('creating-drive')
123
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
129
+ const manifest = getManifest(publicKeys, namespace, { quorum })
124
130
  const request = await SignRequest.generateDrive(srcDrive, { manifest, length })
125
131
  return { manifest, request }
126
132
  })
@@ -152,7 +158,7 @@ class HyperMultisig {
152
158
 
153
159
  if (!force) {
154
160
  runner.emit('verify-committable-start', srcCore.key, core.key)
155
- await MultisigUtil.verifyCoreCommittable(srcCore, core, length, {
161
+ await verifyCoreCommittable(srcCore, core, length, {
156
162
  skipTargetChecks,
157
163
  peerUpdateTimeout
158
164
  })
@@ -160,8 +166,8 @@ class HyperMultisig {
160
166
 
161
167
  const signResponses = []
162
168
  for (const response of responses) {
163
- const res = cenc.decode(CoreSign.messages.Response, z32.decode(response))
164
- await CoreSign.verify(response, request, z32.encode(res.publicKey))
169
+ const res = cenc.decode(CoreSign.Response, z32.decode(response))
170
+ await CoreSign.verify(z32.decode(response), z32.decode(request), res.publicKey)
165
171
  const publicKeyHex = res.publicKey.toString('hex')
166
172
  signResponses[publicKeyHex] = res
167
173
  }
@@ -177,19 +183,19 @@ class HyperMultisig {
177
183
  })
178
184
 
179
185
  runner.emit('commit-start')
180
- const batch = await MultisigUtil.signCore(core, srcCore, signatures, {
186
+ const batch = await signCore(core, srcCore, signatures, {
181
187
  end: length,
182
188
  commit: !dryRun
183
189
  })
184
190
 
185
191
  if (!force && !dryRun) {
186
192
  runner.emit('verify-committed-start', core.key)
187
- await MultisigUtil.verifyCoreCommitted(core, { minPeers: minFullCopies })
193
+ await verifyCoreCommitted(core, { minPeers: minFullCopies })
188
194
  }
189
195
 
190
196
  const result = {
191
- destCore: await MultisigUtil.getCoreInfo(core),
192
- srcCore: await MultisigUtil.getCoreInfo(srcCore),
197
+ destCore: await getCoreInfo(core),
198
+ srcCore: await getCoreInfo(srcCore),
193
199
  batch
194
200
  }
195
201
 
@@ -220,12 +226,12 @@ class HyperMultisig {
220
226
 
221
227
  if (!force) {
222
228
  runner.emit('verify-committable-start', srcDrive.db.core.key, core.key)
223
- await MultisigUtil.verifyCoreCommittable(srcDrive.db.core, core, length, {
229
+ await verifyCoreCommittable(srcDrive.db.core, core, length, {
224
230
  skipTargetChecks,
225
231
  peerUpdateTimeout,
226
232
  coreId: 'db'
227
233
  })
228
- await MultisigUtil.verifyCoreCommittable(srcDrive.blobs.core, blobsCore, blobsLength, {
234
+ await verifyCoreCommittable(srcDrive.blobs.core, blobsCore, blobsLength, {
229
235
  skipTargetChecks,
230
236
  peerUpdateTimeout,
231
237
  coreId: 'blobs'
@@ -234,8 +240,8 @@ class HyperMultisig {
234
240
 
235
241
  const signResponses = []
236
242
  for (const response of responses) {
237
- const res = cenc.decode(CoreSign.messages.Response, z32.decode(response))
238
- await CoreSign.verify(response, request, z32.encode(res.publicKey))
243
+ const res = cenc.decode(CoreSign.Response, z32.decode(response))
244
+ await CoreSign.verify(z32.decode(response), z32.decode(request), res.publicKey)
239
245
  const publicKeyHex = res.publicKey.toString('hex')
240
246
  signResponses[publicKeyHex] = res
241
247
  }
@@ -253,7 +259,7 @@ class HyperMultisig {
253
259
  const blobsSignatures = allSignatures.map((item) => item?.[1])
254
260
 
255
261
  runner.emit('commit-start')
256
- const { batch, blobsBatch } = await MultisigUtil.signDrive(
262
+ const { batch, blobsBatch } = await signDrive(
257
263
  core,
258
264
  srcDrive.core,
259
265
  signatures,
@@ -265,19 +271,19 @@ class HyperMultisig {
265
271
 
266
272
  if (!force && !dryRun) {
267
273
  runner.emit('verify-committed-start', core.key)
268
- await MultisigUtil.verifyCoreCommitted(core)
269
- await MultisigUtil.verifyCoreCommitted(blobsCore)
274
+ await verifyCoreCommitted(core)
275
+ await verifyCoreCommitted(blobsCore)
270
276
  }
271
277
 
272
278
  const result = {
273
279
  db: {
274
- destCore: await MultisigUtil.getCoreInfo(core),
275
- srcCore: await MultisigUtil.getCoreInfo(srcDrive.core),
280
+ destCore: await getCoreInfo(core),
281
+ srcCore: await getCoreInfo(srcDrive.core),
276
282
  batch
277
283
  },
278
284
  blobs: {
279
- destCore: await MultisigUtil.getCoreInfo(blobsCore),
280
- srcCore: await MultisigUtil.getCoreInfo(srcDrive.blobs.core),
285
+ destCore: await getCoreInfo(blobsCore),
286
+ srcCore: await getCoreInfo(srcDrive.blobs.core),
281
287
  batch: blobsBatch
282
288
  }
283
289
  }
package/lib/core.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @typedef {{
3
+ * version: number
4
+ * hash: string
5
+ * quorum: number
6
+ * signers: Array<{
7
+ * signature: string
8
+ * publicKey: Buffer
9
+ * namespace: Buffer
10
+ * }>
11
+ * }} Manifest
12
+ * @typedef {{
13
+ * key: string
14
+ * length: number
15
+ * treeHash: string
16
+ * }} CoreInfo
17
+ */
18
+
19
+ const Hypercore = require('hypercore')
20
+ const crypto = require('hypercore-crypto')
21
+ const idEnc = require('hypercore-id-encoding')
22
+
23
+ /**
24
+ * @param {string[]} publicKeys
25
+ * @param {string} namespace
26
+ * @return {string}
27
+ */
28
+ function getCoreKey(publicKeys, namespace) {
29
+ const manifest = getManifest(publicKeys, namespace)
30
+ return Hypercore.key(manifest)
31
+ }
32
+
33
+ /**
34
+ * @param {string[]} publicKeys
35
+ * @param {string} namespace
36
+ * @return {Manifest}
37
+ */
38
+ function getManifest(publicKeys, namespace, { quorum } = {}) {
39
+ if (!quorum) quorum = Math.floor(publicKeys.length / 2) + 1
40
+
41
+ return {
42
+ version: 1,
43
+ hash: 'blake2b',
44
+ quorum,
45
+ signers: publicKeys.map((publicKey) => ({
46
+ signature: 'ed25519',
47
+ publicKey: idEnc.decode(publicKey),
48
+ namespace: idEnc.decode(getNamespace(namespace))
49
+ }))
50
+ }
51
+ }
52
+
53
+ /**
54
+ * @param {string} namespace
55
+ * @return {string}
56
+ */
57
+ function getNamespace(namespace) {
58
+ return idEnc.normalize(crypto.hash(Buffer.from(namespace)))
59
+ }
60
+
61
+ /**
62
+ * @param {Manifest} manifest
63
+ */
64
+ function normalizeManifest(manifest) {
65
+ return {
66
+ ...manifest,
67
+ signers: manifest.signers.map((signer) => ({
68
+ ...signer,
69
+ publicKey: idEnc.normalize(signer.publicKey),
70
+ namespace: idEnc.normalize(signer.namespace)
71
+ }))
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {Hypercore} core
77
+ * @return {Promise<{ key: string, length: number, treeHash: string }>}
78
+ */
79
+ async function getCoreInfo(core) {
80
+ await core.ready()
81
+ return {
82
+ key: idEnc.normalize(core.key),
83
+ length: core.length,
84
+ treeHash: idEnc.normalize(await core.treeHash())
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ getCoreKey,
90
+ getManifest,
91
+ getNamespace,
92
+ normalizeManifest,
93
+ getCoreInfo
94
+ }
package/lib/sign.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @typedef {{
3
+ * key: string
4
+ * length: number
5
+ * treeHash: string
6
+ * }} CoreInfo
7
+ */
8
+
9
+ const { assemble, partialSignature } = require('hypercore/lib/multisig')
10
+
11
+ const { getCoreInfo } = require('./core')
12
+
13
+ /**
14
+ * @param {Hypercore} core
15
+ * @param {Hypercore} batch
16
+ * @param {Buffer[]} signatures
17
+ * @param {{
18
+ * length?: number
19
+ * start?: number
20
+ * end?: number
21
+ * commit?: boolean
22
+ * }} opts
23
+ * @return {Promise<CoreInfo>}
24
+ */
25
+ async function signCore(core, fromCore, signatures, { length, start, end, commit } = {}) {
26
+ /** @type {Hypercore | null} */
27
+ let batch = null
28
+ try {
29
+ batch = await createUpdateBatch(core, fromCore, { start, end })
30
+ if (commit) {
31
+ await commitUpdateBatch(core, batch, signatures, { length })
32
+ }
33
+ const batchInfo = await getCoreInfo(batch)
34
+ return batchInfo
35
+ } finally {
36
+ if (batch) await batch.close()
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @param {Hypercore} core
42
+ * @param {Hypercore} fromCore
43
+ * @param {Buffer[]} signatures
44
+ * @param {Hypercore} blobsCore
45
+ * @param {Hypercore} fromBlobsCore
46
+ * @param {Buffer[]} blobsSignatures
47
+ * @param {{
48
+ * start?: number
49
+ * end?: number
50
+ * length?: number
51
+ * blobsStart?: number
52
+ * blobsEnd?: number
53
+ * blobsLength?: number
54
+ * commit?: boolean
55
+ * }} opts
56
+ * @return {Promise<{ batch: CoreInfo, blobsBatch: CoreInfo }>}
57
+ */
58
+ async function signDrive(
59
+ core,
60
+ fromCore,
61
+ signatures,
62
+ blobsCore,
63
+ fromBlobsCore,
64
+ blobsSignatures,
65
+ { start, end, length, blobsStart, blobsEnd, blobsLength, commit } = {}
66
+ ) {
67
+ const blobsBatch = await signCore(blobsCore, fromBlobsCore, blobsSignatures, {
68
+ start: blobsStart,
69
+ end: blobsEnd,
70
+ length: blobsLength,
71
+ commit
72
+ })
73
+ const batch = await signCore(core, fromCore, signatures, {
74
+ start,
75
+ end,
76
+ length,
77
+ commit
78
+ })
79
+ return { batch, blobsBatch }
80
+ }
81
+
82
+ /**
83
+ * @param {Hypercore} core
84
+ * @param {Hypercore} fromCore
85
+ * @param {{ start?: number, end?: number }} opts
86
+ * @return {Promise<Hypercore>}
87
+ */
88
+ async function createUpdateBatch(core, fromCore, { start, end } = {}) {
89
+ start = start ?? core.length
90
+ end = end ?? fromCore.length
91
+
92
+ const download = fromCore.download({ start, end })
93
+ await download.done()
94
+
95
+ /** @type {Hypercore} */
96
+ const batch = core.session({ name: 'batch', overwrite: true })
97
+ for (let idx = start; idx < end; idx += 1) {
98
+ await batch.append(await fromCore.get(idx))
99
+ }
100
+ return batch
101
+ }
102
+
103
+ /**
104
+ * @param {Hypercore} core
105
+ * @param {Hypercore} batch
106
+ * @param {Buffer[]} signatures
107
+ * @param {{ length?: number }} opts
108
+ */
109
+ async function commitUpdateBatch(core, batch, signatures, { length } = {}) {
110
+ length = length || batch.length
111
+
112
+ const proofs = await Promise.all(
113
+ signatures.map(async (sig, idx) => {
114
+ if (!sig) return null
115
+ const proof = await partialSignature(batch, idx, length, length, sig) // idx is important here, must match with the signers index
116
+ return proof
117
+ })
118
+ )
119
+ const validProofs = proofs.filter(Boolean)
120
+ const multisig = assemble(validProofs)
121
+
122
+ await core.commit(batch, { signature: multisig, length })
123
+ }
124
+
125
+ module.exports = {
126
+ signCore,
127
+ signDrive,
128
+ createUpdateBatch,
129
+ commitUpdateBatch
130
+ }
package/lib/verify.js ADDED
@@ -0,0 +1,136 @@
1
+ const b4a = require('b4a')
2
+
3
+ const MultisigError = require('./error')
4
+
5
+ async function verifyCoreRequestable(
6
+ srcCore,
7
+ length,
8
+ { minPeers = 2, peerUpdateTimeout = 5000, coreId } = {}
9
+ ) {
10
+ await waitUntilCoreLength(srcCore, length, { timeout: peerUpdateTimeout })
11
+
12
+ if (length > srcCore.length) {
13
+ throw MultisigError.SOURCE_CORE_TOO_SMALL(length, { coreId })
14
+ }
15
+
16
+ await waitUntilSufficientPeers(srcCore, { minPeers, timeout: peerUpdateTimeout })
17
+
18
+ const nrSrcPeers = srcCore.peers.length
19
+ if (nrSrcPeers < minPeers) {
20
+ throw MultisigError.SOURCE_CORE_INSUFFICIENT_PEERS(nrSrcPeers, minPeers)
21
+ }
22
+
23
+ await waitUntilFullySeeded(srcCore, { minPeers, timeout: peerUpdateTimeout })
24
+
25
+ let srcFullCopies = 0
26
+ for (const p of srcCore.peers) {
27
+ if (p.remoteContiguousLength === srcCore.length) srcFullCopies++
28
+ }
29
+ if (srcFullCopies < minPeers) {
30
+ throw MultisigError.SOURCE_CORE_NOT_FULLY_SEEDED(srcFullCopies, minPeers, { coreId })
31
+ }
32
+ }
33
+
34
+ async function verifyCoreCommittable(
35
+ srcCore,
36
+ tgtCore,
37
+ length,
38
+ { minPeers = 2, skipTargetChecks = false, peerUpdateTimeout = 5000, coreId } = {}
39
+ ) {
40
+ await waitUntilCoreLength(srcCore, length, { timeout: peerUpdateTimeout })
41
+
42
+ if (length > srcCore.length) {
43
+ throw MultisigError.SOURCE_CORE_TOO_SMALL(length, { coreId })
44
+ }
45
+
46
+ // Either it corrupts the core, or it's a no-op (re-signing already signed data). There's no possible upside.
47
+ if (tgtCore.length > srcCore.length) {
48
+ throw MultisigError.TARGET_CORE_TOO_BIG()
49
+ }
50
+
51
+ await waitUntilSufficientPeers(srcCore, { minPeers, timeout: peerUpdateTimeout })
52
+
53
+ const nrSrcPeers = srcCore.peers.length
54
+ if (nrSrcPeers < minPeers) {
55
+ throw MultisigError.SOURCE_CORE_INSUFFICIENT_PEERS(nrSrcPeers, minPeers)
56
+ }
57
+
58
+ await waitUntilFullySeeded(srcCore, { minPeers, timeout: peerUpdateTimeout })
59
+
60
+ let srcFullCopies = 0
61
+ for (const p of srcCore.peers) {
62
+ if (p.remoteContiguousLength === srcCore.length) srcFullCopies++
63
+ }
64
+ if (srcFullCopies < minPeers) {
65
+ throw MultisigError.SOURCE_CORE_NOT_FULLY_SEEDED(srcFullCopies, minPeers, { coreId })
66
+ }
67
+
68
+ if (skipTargetChecks) {
69
+ // no checks on the target if it's the first commit
70
+ if (tgtCore.length > 0) throw MultisigError.TARGET_NOT_EMPTY()
71
+ return
72
+ }
73
+
74
+ await waitUntilSufficientPeers(tgtCore, { minPeers, timeout: peerUpdateTimeout })
75
+
76
+ const tgtPeers = tgtCore.peers.length
77
+ if (tgtPeers < minPeers) {
78
+ throw MultisigError.TARGET_CORE_INSUFFICIENT_PEERS(tgtPeers, minPeers, { coreId })
79
+ }
80
+
81
+ await waitUntilFullySeeded(tgtCore, { minPeers, timeout: peerUpdateTimeout })
82
+
83
+ let tgtFullCopies = 0
84
+ for (const p of tgtCore.peers) {
85
+ if (p.remoteContiguousLength === tgtCore.length) tgtFullCopies++
86
+ }
87
+ if (tgtFullCopies < minPeers) {
88
+ throw MultisigError.TARGET_CORE_NOT_FULLY_SEEDED(tgtFullCopies, minPeers, { coreId })
89
+ }
90
+
91
+ if (!b4a.equals(await tgtCore.treeHash(tgtCore.length), await srcCore.treeHash(tgtCore.length))) {
92
+ throw MultisigError.INCOMPATIBLE_SOURCE_AND_TARGET({ coreId })
93
+ }
94
+ }
95
+
96
+ async function verifyCoreCommitted(tgtCore, { minPeers = 2 } = {}) {
97
+ let tgtFullCopies = 0
98
+ for (const p of tgtCore.peers) {
99
+ if (p.remoteContiguousLength === tgtCore.length) tgtFullCopies++
100
+ }
101
+ if (tgtFullCopies < minPeers) {
102
+ await new Promise((resolve) => setTimeout(resolve, 100))
103
+ await verifyCoreCommitted(tgtCore, { minPeers })
104
+ }
105
+ }
106
+
107
+ async function waitUntilCoreLength(core, length, { timeout = 5000 } = {}) {
108
+ return await waitUntil(() => core.length >= length, { timeout })
109
+ }
110
+
111
+ async function waitUntilSufficientPeers(core, { minPeers = 2, timeout = 5000 } = {}) {
112
+ return await waitUntil(() => core.peers?.length >= minPeers, { timeout })
113
+ }
114
+
115
+ async function waitUntilFullySeeded(core, { minPeers = 2, timeout = 5000 } = {}) {
116
+ return await waitUntil(
117
+ () => core.peers?.filter((p) => p.remoteContiguousLength === core.length).length >= minPeers,
118
+ { timeout }
119
+ )
120
+ }
121
+
122
+ async function waitUntil(conditionFn, { timeout = 5000, interval = 100 } = {}) {
123
+ if (await conditionFn()) return true
124
+ if (timeout < 0) return false
125
+ await new Promise((resolve) => setTimeout(resolve, interval))
126
+ return waitUntil(conditionFn, { timeout: timeout - interval, interval })
127
+ }
128
+
129
+ module.exports = {
130
+ verifyCoreRequestable,
131
+ verifyCoreCommittable,
132
+ verifyCoreCommitted,
133
+ waitUntilCoreLength,
134
+ waitUntilSufficientPeers,
135
+ waitUntilFullySeeded
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-multisig",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Multisig hypercore and hyperdrive",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -20,7 +20,7 @@
20
20
  "hypercore": "^11.24.0",
21
21
  "hypercore-crypto": "^3.6.1",
22
22
  "hypercore-id-encoding": "^1.3.0",
23
- "hypercore-sign": "^3.0.0",
23
+ "hypercore-sign": "^4.0.1",
24
24
  "hypercore-signing-request": "^4.0.2",
25
25
  "hyperdrive": "^13.2.1",
26
26
  "z32": "^1.1.0"
package/lib/util.js DELETED
@@ -1,346 +0,0 @@
1
- /**
2
- * @typedef {import('corestore')} Corestore
3
- * @typedef {import('hypercore/lib/download')} Download
4
- * @typedef {{
5
- * version: number
6
- * hash: string
7
- * quorum: number
8
- * signers: Array<{
9
- * signature: string
10
- * publicKey: Buffer
11
- * namespace: Buffer
12
- * }>
13
- * }} Manifest
14
- * @typedef {{
15
- * key: string
16
- * length: number
17
- * treeHash: string
18
- * }} CoreInfo
19
- */
20
-
21
- const b4a = require('b4a')
22
- const Hypercore = require('hypercore')
23
- const { assemble, partialSignature } = require('hypercore/lib/multisig')
24
- const crypto = require('hypercore-crypto')
25
- const idEnc = require('hypercore-id-encoding')
26
-
27
- const MultisigError = require('./error')
28
-
29
- /**
30
- * @param {Hypercore} core
31
- * @param {Hypercore} batch
32
- * @param {Buffer[]} signatures
33
- * @param {{
34
- * length?: number
35
- * start?: number
36
- * end?: number
37
- * commit?: boolean
38
- * }} opts
39
- * @return {Promise<CoreInfo>}
40
- */
41
- async function signCore(core, fromCore, signatures, { length, start, end, commit } = {}) {
42
- /** @type {Hypercore | null} */
43
- let batch = null
44
- try {
45
- batch = await createUpdateBatch(core, fromCore, { start, end })
46
- if (commit) {
47
- await commitUpdateBatch(core, batch, signatures, { length })
48
- }
49
- const batchInfo = await getCoreInfo(batch)
50
- return batchInfo
51
- } finally {
52
- if (batch) await batch.close()
53
- }
54
- }
55
-
56
- /**
57
- * @param {Hypercore} core
58
- * @param {Hypercore} fromCore
59
- * @param {Buffer[]} signatures
60
- * @param {Hypercore} blobsCore
61
- * @param {Hypercore} fromBlobsCore
62
- * @param {Buffer[]} blobsSignatures
63
- * @param {{
64
- * start?: number
65
- * end?: number
66
- * length?: number
67
- * blobsStart?: number
68
- * blobsEnd?: number
69
- * blobsLength?: number
70
- * commit?: boolean
71
- * }} opts
72
- * @return {Promise<{ batch: CoreInfo, blobsBatch: CoreInfo }>}
73
- */
74
- async function signDrive(
75
- core,
76
- fromCore,
77
- signatures,
78
- blobsCore,
79
- fromBlobsCore,
80
- blobsSignatures,
81
- { start, end, length, blobsStart, blobsEnd, blobsLength, commit } = {}
82
- ) {
83
- const blobsBatch = await signCore(blobsCore, fromBlobsCore, blobsSignatures, {
84
- start: blobsStart,
85
- end: blobsEnd,
86
- length: blobsLength,
87
- commit
88
- })
89
- const batch = await signCore(core, fromCore, signatures, {
90
- start,
91
- end,
92
- length,
93
- commit
94
- })
95
- return { batch, blobsBatch }
96
- }
97
-
98
- /**
99
- * @param {Hypercore} core
100
- * @param {Hypercore} fromCore
101
- * @param {{ start?: number, end?: number }} opts
102
- * @return {Promise<Hypercore>}
103
- */
104
- async function createUpdateBatch(core, fromCore, { start, end } = {}) {
105
- start = start ?? core.length
106
- end = end ?? fromCore.length
107
-
108
- const download = fromCore.download({ start, end })
109
- await download.done()
110
-
111
- /** @type {Hypercore} */
112
- const batch = core.session({ name: 'batch', overwrite: true })
113
- for (let idx = start; idx < end; idx += 1) {
114
- await batch.append(await fromCore.get(idx))
115
- }
116
- return batch
117
- }
118
-
119
- /**
120
- * @param {Hypercore} core
121
- * @param {Hypercore} batch
122
- * @param {Buffer[]} signatures
123
- * @param {{ length?: number }} opts
124
- */
125
- async function commitUpdateBatch(core, batch, signatures, { length } = {}) {
126
- length = length || batch.length
127
-
128
- const proofs = await Promise.all(
129
- signatures.map(async (sig, idx) => {
130
- if (!sig) return null
131
- const proof = await partialSignature(batch, idx, length, length, sig) // idx is important here, must match with the signers index
132
- return proof
133
- })
134
- )
135
- const validProofs = proofs.filter(Boolean)
136
- const multisig = assemble(validProofs)
137
-
138
- await core.commit(batch, { signature: multisig, length })
139
- }
140
-
141
- /**
142
- * @param {string[]} publicKeys
143
- * @param {string} namespace
144
- * @return {string}
145
- */
146
- function getCoreKey(publicKeys, namespace) {
147
- const manifest = getManifest(publicKeys, namespace)
148
- return Hypercore.key(manifest)
149
- }
150
-
151
- /**
152
- * @param {string[]} publicKeys
153
- * @param {string} namespace
154
- * @return {Manifest}
155
- */
156
- function getManifest(publicKeys, namespace, { quorum } = {}) {
157
- if (!quorum) quorum = Math.floor(publicKeys.length / 2) + 1
158
-
159
- return {
160
- version: 1,
161
- hash: 'blake2b',
162
- quorum,
163
- signers: publicKeys.map((publicKey) => ({
164
- signature: 'ed25519',
165
- publicKey: idEnc.decode(publicKey),
166
- namespace: idEnc.decode(getNamespace(namespace))
167
- }))
168
- }
169
- }
170
-
171
- /**
172
- * @param {string} namespace
173
- * @return {string}
174
- */
175
- function getNamespace(namespace) {
176
- return idEnc.normalize(crypto.hash(Buffer.from(namespace)))
177
- }
178
-
179
- /**
180
- * @param {Manifest} manifest
181
- */
182
- function normalizeManifest(manifest) {
183
- return {
184
- ...manifest,
185
- signers: manifest.signers.map((signer) => ({
186
- ...signer,
187
- publicKey: idEnc.normalize(signer.publicKey),
188
- namespace: idEnc.normalize(signer.namespace)
189
- }))
190
- }
191
- }
192
-
193
- /**
194
- * @param {Hypercore} core
195
- * @return {Promise<{ key: string, length: number, treeHash: string }>}
196
- */
197
- async function getCoreInfo(core) {
198
- await core.ready()
199
- return {
200
- key: idEnc.normalize(core.key),
201
- length: core.length,
202
- treeHash: idEnc.normalize(await core.treeHash())
203
- }
204
- }
205
-
206
- async function verifyCoreRequestable(
207
- srcCore,
208
- length,
209
- { minPeers = 2, peerUpdateTimeout = 5000, coreId } = {}
210
- ) {
211
- await waitUntilCoreLength(srcCore, length, { timeout: peerUpdateTimeout })
212
-
213
- if (length > srcCore.length) {
214
- throw MultisigError.SOURCE_CORE_TOO_SMALL(length, { coreId })
215
- }
216
-
217
- await waitUntilSufficientPeers(srcCore, { minPeers, timeout: peerUpdateTimeout })
218
-
219
- const nrSrcPeers = srcCore.peers.length
220
- if (nrSrcPeers < minPeers) {
221
- throw MultisigError.SOURCE_CORE_INSUFFICIENT_PEERS(nrSrcPeers, minPeers)
222
- }
223
-
224
- await waitUntilFullySeeded(srcCore, { minPeers, timeout: peerUpdateTimeout })
225
-
226
- let srcFullCopies = 0
227
- for (const p of srcCore.peers) {
228
- if (p.remoteContiguousLength === srcCore.length) srcFullCopies++
229
- }
230
- if (srcFullCopies < minPeers) {
231
- throw MultisigError.SOURCE_CORE_NOT_FULLY_SEEDED(srcFullCopies, minPeers, { coreId })
232
- }
233
- }
234
-
235
- async function verifyCoreCommittable(
236
- srcCore,
237
- tgtCore,
238
- length,
239
- { minPeers = 2, skipTargetChecks = false, peerUpdateTimeout = 5000, coreId } = {}
240
- ) {
241
- await waitUntilCoreLength(srcCore, length, { timeout: peerUpdateTimeout })
242
-
243
- if (length > srcCore.length) {
244
- throw MultisigError.SOURCE_CORE_TOO_SMALL(length, { coreId })
245
- }
246
-
247
- // Either it corrupts the core, or it's a no-op (re-signing already signed data). There's no possible upside.
248
- if (tgtCore.length > srcCore.length) {
249
- throw MultisigError.TARGET_CORE_TOO_BIG()
250
- }
251
-
252
- await waitUntilSufficientPeers(srcCore, { minPeers, timeout: peerUpdateTimeout })
253
-
254
- const nrSrcPeers = srcCore.peers.length
255
- if (nrSrcPeers < minPeers) {
256
- throw MultisigError.SOURCE_CORE_INSUFFICIENT_PEERS(nrSrcPeers, minPeers)
257
- }
258
-
259
- await waitUntilFullySeeded(srcCore, { minPeers, timeout: peerUpdateTimeout })
260
-
261
- let srcFullCopies = 0
262
- for (const p of srcCore.peers) {
263
- if (p.remoteContiguousLength === srcCore.length) srcFullCopies++
264
- }
265
- if (srcFullCopies < minPeers) {
266
- throw MultisigError.SOURCE_CORE_NOT_FULLY_SEEDED(srcFullCopies, minPeers, { coreId })
267
- }
268
-
269
- if (skipTargetChecks) {
270
- // no checks on the target if it's the first commit
271
- if (tgtCore.length > 0) throw MultisigError.TARGET_NOT_EMPTY()
272
- return
273
- }
274
-
275
- await waitUntilSufficientPeers(tgtCore, { minPeers, timeout: peerUpdateTimeout })
276
-
277
- const tgtPeers = tgtCore.peers.length
278
- if (tgtPeers < minPeers) {
279
- throw MultisigError.TARGET_CORE_INSUFFICIENT_PEERS(tgtPeers, minPeers, { coreId })
280
- }
281
-
282
- await waitUntilFullySeeded(tgtCore, { minPeers, timeout: peerUpdateTimeout })
283
-
284
- let tgtFullCopies = 0
285
- for (const p of tgtCore.peers) {
286
- if (p.remoteContiguousLength === tgtCore.length) tgtFullCopies++
287
- }
288
- if (tgtFullCopies < minPeers) {
289
- throw MultisigError.TARGET_CORE_NOT_FULLY_SEEDED(tgtFullCopies, minPeers, { coreId })
290
- }
291
-
292
- if (!b4a.equals(await tgtCore.treeHash(tgtCore.length), await srcCore.treeHash(tgtCore.length))) {
293
- throw MultisigError.INCOMPATIBLE_SOURCE_AND_TARGET({ coreId })
294
- }
295
- }
296
-
297
- async function verifyCoreCommitted(tgtCore, { minPeers = 2 } = {}) {
298
- let tgtFullCopies = 0
299
- for (const p of tgtCore.peers) {
300
- if (p.remoteContiguousLength === tgtCore.length) tgtFullCopies++
301
- }
302
- if (tgtFullCopies < minPeers) {
303
- await new Promise((resolve) => setTimeout(resolve, 100))
304
- await verifyCoreCommitted(tgtCore, { minPeers })
305
- }
306
- }
307
-
308
- async function waitUntilCoreLength(core, length, { timeout = 5000 } = {}) {
309
- return await waitUntil(() => core.length >= length, { timeout })
310
- }
311
-
312
- async function waitUntilSufficientPeers(core, { minPeers = 2, timeout = 5000 } = {}) {
313
- return await waitUntil(() => core.peers?.length >= minPeers, { timeout })
314
- }
315
-
316
- async function waitUntilFullySeeded(core, { minPeers = 2, timeout = 5000 } = {}) {
317
- return await waitUntil(
318
- () => core.peers?.filter((p) => p.remoteContiguousLength === core.length).length >= minPeers,
319
- { timeout }
320
- )
321
- }
322
-
323
- async function waitUntil(conditionFn, { timeout = 5000, interval = 100 } = {}) {
324
- if (await conditionFn()) return true
325
- if (timeout < 0) return false
326
- await new Promise((resolve) => setTimeout(resolve, interval))
327
- return waitUntil(conditionFn, { timeout: timeout - interval, interval })
328
- }
329
-
330
- module.exports = {
331
- signCore,
332
- signDrive,
333
- createUpdateBatch,
334
- commitUpdateBatch,
335
- getCoreKey,
336
- getManifest,
337
- getNamespace,
338
- normalizeManifest,
339
- getCoreInfo,
340
- verifyCoreRequestable,
341
- verifyCoreCommittable,
342
- verifyCoreCommitted,
343
- waitUntilCoreLength,
344
- waitUntilSufficientPeers,
345
- waitUntilFullySeeded
346
- }