hyper-multisig 1.0.1 → 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,13 +43,15 @@ class HyperMultisig {
37
43
  this.swarm = swarm
38
44
  }
39
45
 
46
+ static getCoreKey = getCoreKey
47
+
40
48
  /**
41
49
  * @param {string[]} publicKeys
42
50
  * @param {string} namespace
43
51
  * @return {Promise<{ manifest: Manifest, key: Buffer, core: Hypercore }>}
44
52
  */
45
53
  async createCore(publicKeys, namespace, { quorum } = {}) {
46
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
54
+ const manifest = getManifest(publicKeys, namespace, { quorum })
47
55
  const core = this.store.get({ manifest })
48
56
  await core.ready()
49
57
  return { manifest, key: core.key, core }
@@ -78,9 +86,9 @@ class HyperMultisig {
78
86
  await srcCore.ready()
79
87
  this.swarm.join(srcCore.discoveryKey, { client: true, server: false })
80
88
 
81
- if (!force) await MultisigUtil.verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
89
+ if (!force) await verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
82
90
 
83
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
91
+ const manifest = getManifest(publicKeys, namespace, { quorum })
84
92
  const request = await SignRequest.generate(srcCore, { manifest, length })
85
93
  return { manifest, request }
86
94
  })
@@ -102,7 +110,7 @@ class HyperMultisig {
102
110
 
103
111
  if (!force) {
104
112
  runner.emit('verify-db-requestable-start')
105
- await MultisigUtil.verifyCoreRequestable(srcDrive.core, length, {
113
+ await verifyCoreRequestable(srcDrive.core, length, {
106
114
  peerUpdateTimeout,
107
115
  coreId: 'db'
108
116
  })
@@ -111,14 +119,14 @@ class HyperMultisig {
111
119
  const contentLength = await srcDrive.getBlobsLength(length)
112
120
 
113
121
  runner.emit('verify-blobs-requestable-start')
114
- await MultisigUtil.verifyCoreRequestable(srcDrive.blobs.core, contentLength, {
122
+ await verifyCoreRequestable(srcDrive.blobs.core, contentLength, {
115
123
  peerUpdateTimeout,
116
124
  coreId: 'blobs'
117
125
  })
118
126
  }
119
127
 
120
128
  runner.emit('creating-drive')
121
- const manifest = MultisigUtil.getManifest(publicKeys, namespace, { quorum })
129
+ const manifest = getManifest(publicKeys, namespace, { quorum })
122
130
  const request = await SignRequest.generateDrive(srcDrive, { manifest, length })
123
131
  return { manifest, request }
124
132
  })
@@ -150,7 +158,7 @@ class HyperMultisig {
150
158
 
151
159
  if (!force) {
152
160
  runner.emit('verify-committable-start', srcCore.key, core.key)
153
- await MultisigUtil.verifyCoreCommittable(srcCore, core, length, {
161
+ await verifyCoreCommittable(srcCore, core, length, {
154
162
  skipTargetChecks,
155
163
  peerUpdateTimeout
156
164
  })
@@ -158,8 +166,8 @@ class HyperMultisig {
158
166
 
159
167
  const signResponses = []
160
168
  for (const response of responses) {
161
- const res = cenc.decode(CoreSign.messages.Response, z32.decode(response))
162
- 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)
163
171
  const publicKeyHex = res.publicKey.toString('hex')
164
172
  signResponses[publicKeyHex] = res
165
173
  }
@@ -175,19 +183,19 @@ class HyperMultisig {
175
183
  })
176
184
 
177
185
  runner.emit('commit-start')
178
- const batch = await MultisigUtil.signCore(core, srcCore, signatures, {
186
+ const batch = await signCore(core, srcCore, signatures, {
179
187
  end: length,
180
188
  commit: !dryRun
181
189
  })
182
190
 
183
191
  if (!force && !dryRun) {
184
192
  runner.emit('verify-committed-start', core.key)
185
- await MultisigUtil.verifyCoreCommitted(core, { minPeers: minFullCopies })
193
+ await verifyCoreCommitted(core, { minPeers: minFullCopies })
186
194
  }
187
195
 
188
196
  const result = {
189
- destCore: await MultisigUtil.getCoreInfo(core),
190
- srcCore: await MultisigUtil.getCoreInfo(srcCore),
197
+ destCore: await getCoreInfo(core),
198
+ srcCore: await getCoreInfo(srcCore),
191
199
  batch
192
200
  }
193
201
 
@@ -218,12 +226,12 @@ class HyperMultisig {
218
226
 
219
227
  if (!force) {
220
228
  runner.emit('verify-committable-start', srcDrive.db.core.key, core.key)
221
- await MultisigUtil.verifyCoreCommittable(srcDrive.db.core, core, length, {
229
+ await verifyCoreCommittable(srcDrive.db.core, core, length, {
222
230
  skipTargetChecks,
223
231
  peerUpdateTimeout,
224
232
  coreId: 'db'
225
233
  })
226
- await MultisigUtil.verifyCoreCommittable(srcDrive.blobs.core, blobsCore, blobsLength, {
234
+ await verifyCoreCommittable(srcDrive.blobs.core, blobsCore, blobsLength, {
227
235
  skipTargetChecks,
228
236
  peerUpdateTimeout,
229
237
  coreId: 'blobs'
@@ -232,8 +240,8 @@ class HyperMultisig {
232
240
 
233
241
  const signResponses = []
234
242
  for (const response of responses) {
235
- const res = cenc.decode(CoreSign.messages.Response, z32.decode(response))
236
- 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)
237
245
  const publicKeyHex = res.publicKey.toString('hex')
238
246
  signResponses[publicKeyHex] = res
239
247
  }
@@ -251,7 +259,7 @@ class HyperMultisig {
251
259
  const blobsSignatures = allSignatures.map((item) => item?.[1])
252
260
 
253
261
  runner.emit('commit-start')
254
- const { batch, blobsBatch } = await MultisigUtil.signDrive(
262
+ const { batch, blobsBatch } = await signDrive(
255
263
  core,
256
264
  srcDrive.core,
257
265
  signatures,
@@ -263,19 +271,19 @@ class HyperMultisig {
263
271
 
264
272
  if (!force && !dryRun) {
265
273
  runner.emit('verify-committed-start', core.key)
266
- await MultisigUtil.verifyCoreCommitted(core)
267
- await MultisigUtil.verifyCoreCommitted(blobsCore)
274
+ await verifyCoreCommitted(core)
275
+ await verifyCoreCommitted(blobsCore)
268
276
  }
269
277
 
270
278
  const result = {
271
279
  db: {
272
- destCore: await MultisigUtil.getCoreInfo(core),
273
- srcCore: await MultisigUtil.getCoreInfo(srcDrive.core),
280
+ destCore: await getCoreInfo(core),
281
+ srcCore: await getCoreInfo(srcDrive.core),
274
282
  batch
275
283
  },
276
284
  blobs: {
277
- destCore: await MultisigUtil.getCoreInfo(blobsCore),
278
- srcCore: await MultisigUtil.getCoreInfo(srcDrive.blobs.core),
285
+ destCore: await getCoreInfo(blobsCore),
286
+ srcCore: await getCoreInfo(srcDrive.blobs.core),
279
287
  batch: blobsBatch
280
288
  }
281
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.1",
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
- }