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 +33 -25
- package/lib/core.js +94 -0
- package/lib/sign.js +130 -0
- package/lib/verify.js +136 -0
- package/package.json +2 -2
- package/lib/util.js +0 -346
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
|
|
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 =
|
|
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
|
|
89
|
+
if (!force) await verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
|
|
82
90
|
|
|
83
|
-
const manifest =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
162
|
-
await CoreSign.verify(response, request,
|
|
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
|
|
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
|
|
193
|
+
await verifyCoreCommitted(core, { minPeers: minFullCopies })
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
const result = {
|
|
189
|
-
destCore: await
|
|
190
|
-
srcCore: await
|
|
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
|
|
229
|
+
await verifyCoreCommittable(srcDrive.db.core, core, length, {
|
|
222
230
|
skipTargetChecks,
|
|
223
231
|
peerUpdateTimeout,
|
|
224
232
|
coreId: 'db'
|
|
225
233
|
})
|
|
226
|
-
await
|
|
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.
|
|
236
|
-
await CoreSign.verify(response, request,
|
|
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
|
|
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
|
|
267
|
-
await
|
|
274
|
+
await verifyCoreCommitted(core)
|
|
275
|
+
await verifyCoreCommitted(blobsCore)
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
const result = {
|
|
271
279
|
db: {
|
|
272
|
-
destCore: await
|
|
273
|
-
srcCore: await
|
|
280
|
+
destCore: await getCoreInfo(core),
|
|
281
|
+
srcCore: await getCoreInfo(srcDrive.core),
|
|
274
282
|
batch
|
|
275
283
|
},
|
|
276
284
|
blobs: {
|
|
277
|
-
destCore: await
|
|
278
|
-
srcCore: await
|
|
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.
|
|
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": "^
|
|
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
|
-
}
|