hyper-multisig 1.0.2 → 1.0.4
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 +32 -26
- package/lib/core.js +96 -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,7 +43,7 @@ class HyperMultisig {
|
|
|
37
43
|
this.swarm = swarm
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
static 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 =
|
|
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
|
|
89
|
+
if (!force) await verifyCoreRequestable(srcCore, length, { peerUpdateTimeout })
|
|
84
90
|
|
|
85
|
-
const manifest =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
164
|
-
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)
|
|
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
|
|
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
|
|
193
|
+
await verifyCoreCommitted(core, { minPeers: minFullCopies })
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
const result = {
|
|
191
|
-
destCore: await
|
|
192
|
-
srcCore: await
|
|
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
|
|
229
|
+
await verifyCoreCommittable(srcDrive.db.core, core, length, {
|
|
224
230
|
skipTargetChecks,
|
|
225
231
|
peerUpdateTimeout,
|
|
226
232
|
coreId: 'db'
|
|
227
233
|
})
|
|
228
|
-
await
|
|
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.
|
|
238
|
-
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)
|
|
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
|
|
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
|
|
269
|
-
await
|
|
274
|
+
await verifyCoreCommitted(core)
|
|
275
|
+
await verifyCoreCommitted(blobsCore)
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
const result = {
|
|
273
279
|
db: {
|
|
274
|
-
destCore: await
|
|
275
|
-
srcCore: await
|
|
280
|
+
destCore: await getCoreInfo(core),
|
|
281
|
+
srcCore: await getCoreInfo(srcDrive.core),
|
|
276
282
|
batch
|
|
277
283
|
},
|
|
278
284
|
blobs: {
|
|
279
|
-
destCore: await
|
|
280
|
-
srcCore: await
|
|
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,96 @@
|
|
|
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
|
+
* @param {{ quorum?: number }} [opts]
|
|
27
|
+
* @return {string}
|
|
28
|
+
*/
|
|
29
|
+
function getCoreKey(publicKeys, namespace, { quorum } = {}) {
|
|
30
|
+
const manifest = getManifest(publicKeys, namespace, { quorum })
|
|
31
|
+
return Hypercore.key(manifest)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string[]} publicKeys
|
|
36
|
+
* @param {string} namespace
|
|
37
|
+
* @param {{ quorum?: number }} [opts]
|
|
38
|
+
* @return {Manifest}
|
|
39
|
+
*/
|
|
40
|
+
function getManifest(publicKeys, namespace, { quorum } = {}) {
|
|
41
|
+
if (!quorum) quorum = Math.floor(publicKeys.length / 2) + 1
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
hash: 'blake2b',
|
|
46
|
+
quorum,
|
|
47
|
+
signers: publicKeys.map((publicKey) => ({
|
|
48
|
+
signature: 'ed25519',
|
|
49
|
+
publicKey: idEnc.decode(publicKey),
|
|
50
|
+
namespace: idEnc.decode(getNamespace(namespace))
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} namespace
|
|
57
|
+
* @return {string}
|
|
58
|
+
*/
|
|
59
|
+
function getNamespace(namespace) {
|
|
60
|
+
return idEnc.normalize(crypto.hash(Buffer.from(namespace)))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Manifest} manifest
|
|
65
|
+
*/
|
|
66
|
+
function normalizeManifest(manifest) {
|
|
67
|
+
return {
|
|
68
|
+
...manifest,
|
|
69
|
+
signers: manifest.signers.map((signer) => ({
|
|
70
|
+
...signer,
|
|
71
|
+
publicKey: idEnc.normalize(signer.publicKey),
|
|
72
|
+
namespace: idEnc.normalize(signer.namespace)
|
|
73
|
+
}))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {Hypercore} core
|
|
79
|
+
* @return {Promise<{ key: string, length: number, treeHash: string }>}
|
|
80
|
+
*/
|
|
81
|
+
async function getCoreInfo(core) {
|
|
82
|
+
await core.ready()
|
|
83
|
+
return {
|
|
84
|
+
key: idEnc.normalize(core.key),
|
|
85
|
+
length: core.length,
|
|
86
|
+
treeHash: idEnc.normalize(await core.treeHash())
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
getCoreKey,
|
|
92
|
+
getManifest,
|
|
93
|
+
getNamespace,
|
|
94
|
+
normalizeManifest,
|
|
95
|
+
getCoreInfo
|
|
96
|
+
}
|
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.4",
|
|
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
|
-
}
|