From 52201f2b9a663d04af7eb3c2584c4ef4f531380d Mon Sep 17 00:00:00 2001 From: Mathias Buus Date: Thu, 7 Sep 2023 12:57:41 +0200 Subject: [PATCH] Manifest (#430 + #431) * Initial manifest support (#426) * add auth section to upgrade * exchange auth definitions through a manifest * fix a ton of stuff * skip custom keypair tests for now * allow explicit compat mode * no need to special case keypair * actually fix preload test * make manifest explicit * also set compat mode when replicating manifest * very explicit manifest exchange * verifyBatch -> verifyBatchUpgraded * tweak namespacing and add explicit default ns * add preliminary functions for parsing manifest * typo on imports * add encodings and entropy/namespacing * add compat signer and more tests * lib/core uses manifest createAuth function * default to compat mode * make signers classes and update tests * correctly set compat on sessions/clones/upgrade * fix bad merge * need to specify compat for clone * replace auth with manifest * update clone and ensure that key is different * update tests to manifest api * load new auth if secret key is provided * remove custom auth test * support passing a sign function * remove auth tests * manifest renamed to multipleSigners * properly set compat and keyPair * pass batch to patched verify * port tests from hypercore-crypto-multisig * include file for multisig helpers * rename to verifiers and add explicit compat verifier * split out signing and verification * remove sign option and just pass keyPair or signature * only set keyPair after openCapabilities * clarify code around creating a manifest * conflict test now works * fix core copyFrom and core clone tests * fix clone and clone tests * standardise opts across append and truncate * tidy up * add multisig lib file * expose helper * fix batch flush api * fix noManifest logic * fix some typos/missing apis * set signature on tree only after verification * rename static helpers * rename defaultAuth to verifier * rename defaultAuth to verifier * remove static createVerifier method * review by @mafintosh * move manifestHash to lib/manifest --------- Co-authored-by: Mathias Buus * last part of the constructor should be openSession * simplify key handler * fix keypair handler * define writable condition in one place * fix typo in manifestHash * pass manifest directly to createVerifier * no need to dbl copy manifest now * tweak manifest construction * consistent errors * add manifest getter * tweak manifest checks on load * review by @chm-diederichs * set keypair in constructor if possible * simplify core append signature * tweak batch flush * simplify compat option * update batch test * move compat default to lib/core * move compat check to a single func * no manifesthash in caps --------- Co-authored-by: Christophe Diederichs <45171645+chm-diederichs@users.noreply.github.com> Co-authored-by: Christophe Diederichs Manifest tweaks (#431) * more tests for now truncate/append options * move more manifest functinality into manifest.js * move isCompat to manifest and simplify clone options * enable all tests * pass batches to verifiers and move namespace support to batch * fix storage test --- index.js | 96 +++-- lib/batch.js | 15 +- lib/caps.js | 22 +- lib/core.js | 110 +++--- lib/manifest.js | 243 ++++++++++++ lib/merkle-tree.js | 12 +- lib/messages.js | 88 +++++ lib/multisig.js | 146 +++++++ test/all.js | 2 +- test/auth.js | 253 ------------ test/batch.js | 23 +- test/clone.js | 110 ++---- test/core.js | 8 +- test/manifest.js | 932 +++++++++++++++++++++++++++++++++++++++++++++ test/preload.js | 5 +- test/replicate.js | 80 +++- test/sessions.js | 38 +- 17 files changed, 1648 insertions(+), 535 deletions(-) create mode 100644 lib/manifest.js create mode 100644 lib/multisig.js delete mode 100644 test/auth.js create mode 100644 test/manifest.js diff --git a/index.js b/index.js index bd1028e2..0fcf7c44 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ const BlockEncryption = require('./lib/block-encryption') const Info = require('./lib/info') const Download = require('./lib/download') const Batch = require('./lib/batch') +const { manifestHash, defaultSignerManifest, createVerifier, createManifest, isCompat } = require('./lib/manifest') const { ReadStream, WriteStream, ByteStream } = require('./lib/streams') const { BAD_ARGUMENT, @@ -69,7 +70,7 @@ module.exports = class Hypercore extends EventEmitter { this.id = null this.key = key || null - this.keyPair = null + this.keyPair = opts.keyPair || null this.readable = true this.writable = false this.opened = false @@ -77,22 +78,23 @@ module.exports = class Hypercore extends EventEmitter { this.snapshotted = !!opts.snapshot this.sparse = opts.sparse !== false this.sessions = opts._sessions || [this] - this.auth = opts.auth || null this.autoClose = !!opts.autoClose this.onwait = opts.onwait || null this.wait = opts.wait !== false this.timeout = opts.timeout || 0 - this._clone = opts.clone || null - this._readonly = opts.writable === false - this.closing = null - this.opening = this._openSession(key, storage, opts) - this.opening.catch(noop) + this.opening = null + this._manifest = opts.manifest || null + this._clone = opts.clone || null + this._readonly = opts.writable === false this._preappend = preappend.bind(this) this._snapshot = null this._batch = opts._batch || null this._findingPeers = 0 + + this.opening = this._openSession(key, storage, opts) + this.opening.catch(noop) } [inspect] (depth, opts) { @@ -139,6 +141,14 @@ module.exports = class Hypercore extends EventEmitter { indent + ')' } + static key (manifest, { compat } = {}) { + return compat ? manifest.signer.publicKey : manifestHash(manifest) + } + + static discoveryKey (key) { + return hypercoreCrypto.discoveryKey(key) + } + static getProtocolMuxer (stream) { return stream.noiseStream.userData } @@ -245,20 +255,19 @@ module.exports = class Hypercore extends EventEmitter { } setKeyPair (keyPair) { - this.auth = Core.createAuth(this.crypto, { keyPair }) - this.writable = !this._readonly && !!this.auth && !!this.auth.sign + this.keyPair = keyPair + this.writable = this._isWritable() } _passCapabilities (o) { - if (!this.auth) this.auth = o.auth - + if (!this.keyPair) this.keyPair = o.keyPair this.crypto = o.crypto this.id = o.id this.key = o.key this.core = o.core this.replicator = o.replicator this.encryption = o.encryption - this.writable = !this._readonly && !!this.auth && !!this.auth.sign + this.writable = this._isWritable() this.autoClose = o.autoClose if (this.snapshotted && this.core && !this._snapshot) this._updateSnapshot() @@ -288,14 +297,6 @@ module.exports = class Hypercore extends EventEmitter { if (!isFirst) await opts._opening if (opts.preload) opts = { ...opts, ...(await this._retryPreload(opts.preload)) } - const keyPair = opts.keyPair - - if (opts.auth) { - this.auth = opts.auth - } else if (keyPair && keyPair.secretKey) { - this.setKeyPair(keyPair) - } - if (isFirst) { await this._openCapabilities(key, storage, opts) // Only the root session should pass capabilities to other sessions. @@ -306,15 +307,14 @@ module.exports = class Hypercore extends EventEmitter { // copy state over if (this._clone) { - const { from, upgrade } = this._clone + const { from, signature } = this._clone await from.opening - await this.core.copyFrom(from.core, upgrade) + await this.core.copyFrom(from.core, signature) this._clone = null } } - if (!this.auth) this.auth = this.core.defaultAuth - this.writable = !this._readonly && !!this.auth && !!this.auth.sign + this.writable = this._isWritable() if (opts.valueEncoding) { this.valueEncoding = c.from(opts.valueEncoding) @@ -353,7 +353,7 @@ module.exports = class Hypercore extends EventEmitter { this.storage = Hypercore.defaultStorage(opts.storage || storage, { unlocked, writable: !unlocked }) this.core = await Core.open(this.storage, { - compat: opts.compat !== false, // default to true for now + compat: opts.compat, force: opts.force, createIfMissing: opts.createIfMissing, readonly: unlocked, @@ -362,7 +362,7 @@ module.exports = class Hypercore extends EventEmitter { keyPair: opts.keyPair, crypto: this.crypto, legacy: opts.legacy, - auth: opts.auth, + manifest: opts.manifest, onupdate: this._oncoreupdate.bind(this), onconflict: this._oncoreconflict.bind(this) }) @@ -417,6 +417,10 @@ module.exports = class Hypercore extends EventEmitter { return prev.length !== next.length || prev.fork !== next.fork } + _isWritable () { + return !this._readonly && !!(this.keyPair && this.keyPair.secretKey) + } + close (err) { if (this.closing) return this.closing this.closing = this._close(err || null) @@ -465,23 +469,22 @@ module.exports = class Hypercore extends EventEmitter { this.emit('close', true) } - clone (storage, opts = {}) { + clone (keyPair, storage, opts = {}) { // TODO: current limitation is no forking if ((opts.fork && opts.fork !== 0) || this.fork !== 0) { throw BAD_ARGUMENT('Cannot clone a fork') } - const key = opts.key === undefined ? opts.keyPair ? null : this.key : opts.key - const keyPair = (opts.auth || opts.keyPair === undefined) ? null : opts.keyPair + const manifest = opts.manifest || defaultSignerManifest(keyPair.publicKey) + const key = opts.key || (opts.compat !== false ? manifest.signer.publicKey : manifestHash(manifest)) - let auth = this.core.defaultAuth - if (opts.auth) { - auth = opts.auth - } else if (keyPair && keyPair.secretKey) { - auth = Core.createAuth(this.crypto, { keyPair, manifest: { signer: { publicKey: keyPair.publicKey } } }) + if (b4a.equals(key, this.key)) { + throw BAD_ARGUMENT('Clone cannot share verification information') } - const upgrade = opts.upgrade === undefined ? null : opts.upgrade + const signature = opts.signature === undefined + ? createVerifier(createManifest(manifest), { compat: isCompat(key, manifest) }).sign(this.core.tree.batch(), keyPair) + : opts.signature const sparse = opts.sparse === false ? false : this.sparse const wait = opts.wait === false ? false : this.wait @@ -497,11 +500,11 @@ module.exports = class Hypercore extends EventEmitter { wait, onwait, timeout, - auth, + manifest, overwrite: true, clone: { from: this, - upgrade + signature } }) } @@ -542,6 +545,10 @@ module.exports = class Hypercore extends EventEmitter { return this.replicator === null ? null : this.replicator.discoveryKey } + get manifest () { + return this._manifest || (this.core === null ? null : this.core.header.manifest) + } + get length () { if (this._snapshot) return this._snapshot.length if (this.core === null) return 0 @@ -934,16 +941,22 @@ module.exports = class Hypercore extends EventEmitter { if (this.opened === false) await this.opening if (this.writable === false) throw SESSION_NOT_WRITABLE() - const { fork = this.core.tree.fork + 1, force = false } = typeof opts === 'number' ? { fork: opts } : opts + const { + fork = this.core.tree.fork + 1, + force = false, + keyPair = this.keyPair, + signature = null + } = typeof opts === 'number' ? { fork: opts } : opts + if (this._batch && !force) throw BATCH_UNFLUSHED() - await this.core.truncate(newLength, fork, this.auth) + await this.core.truncate(newLength, fork, { keyPair, signature }) // TODO: Should propagate from an event triggered by the oplog this.replicator.updateAll() } - async append (blocks, opts) { + async append (blocks, opts = {}) { if (this._batch && this !== this._batch.session) throw BATCH_UNFLUSHED() if (this.opened === false) await this.opening @@ -961,7 +974,8 @@ module.exports = class Hypercore extends EventEmitter { } } - return this.core.append(buffers, (opts && opts.auth) || this.auth, { preappend }) + const { keyPair = this.keyPair, signature = null } = opts + return this.core.append(buffers, { keyPair, signature, preappend }) } async treeHash (length) { diff --git a/lib/batch.js b/lib/batch.js index 5db888c7..f7e6c69b 100644 --- a/lib/batch.js +++ b/lib/batch.js @@ -59,8 +59,8 @@ module.exports = class HypercoreBatch extends EventEmitter { return this.session.core } - get auth () { - return this.session.auth + get manifest () { + return this.session.manifest } async ready () { @@ -230,12 +230,14 @@ module.exports = class HypercoreBatch extends EventEmitter { return info } - async flush (length = this._appends.length, auth) { + async flush (opts = {}) { if (this.opened === false) await this.opening if (this.closing) throw SESSION_CLOSED() + const { length = this._appends.length, keyPair = this.session.keyPair, signature = null } = opts + while (this._flushing) await this._flushing - this._flushing = this._flush(length, auth) + this._flushing = this._flush(length, keyPair, signature) try { await this._flushing @@ -246,11 +248,12 @@ module.exports = class HypercoreBatch extends EventEmitter { if (this.autoClose) await this.close() } - async _flush (length, auth) { // TODO: make this safe to interact with a parallel truncate... + async _flush (length, keyPair, signature) { // TODO: make this safe to interact with a parallel truncate... if (this._appends.length === 0) return const flushingLength = Math.min(length, this._appends.length) - const info = await this.session.append(flushingLength < this._appends.length ? this._appends.slice(0, flushingLength) : this._appends, { auth }) + const blocks = flushingLength < this._appends.length ? this._appends.slice(0, flushingLength) : this._appends + const info = await this.session.append(blocks, { keyPair, signature }) const delta = info.byteLength - this._sessionByteLength this._sessionLength = info.length diff --git a/lib/caps.js b/lib/caps.js index 892121a0..db81edc5 100644 --- a/lib/caps.js +++ b/lib/caps.js @@ -8,6 +8,7 @@ const c = require('compact-encoding') const [TREE, REPLICATE_INITIATOR, REPLICATE_RESPONDER, MANIFEST, DEFAULT_NAMESPACE] = crypto.namespace('hypercore', 5) +exports.MANIFEST = MANIFEST exports.DEFAULT_NAMESPACE = DEFAULT_NAMESPACE exports.replicate = function (isInitiator, key, handshakeHash) { @@ -16,27 +17,20 @@ exports.replicate = function (isInitiator, key, handshakeHash) { return out } -exports.manifestHash = function (manifest) { - const state = { start: 0, end: 32 + manifest.byteLength, buffer: null } - state.buffer = b4a.allocUnsafe(state.end) - c.raw.encode(state, MANIFEST) - c.raw.encode(state, manifest) - const out = b4a.allocUnsafe(32) - sodium.crypto_generichash(out, state.buffer) - return out -} - -exports.treeSignable = function (hash, length, fork) { - const state = { start: 0, end: 80, buffer: b4a.allocUnsafe(80) } +exports.treeSignable = function (namespace, hash, length, fork) { + const state = { start: 0, end: 112, buffer: b4a.allocUnsafe(112) } c.raw.encode(state, TREE) + c.raw.encode(state, namespace) c.raw.encode(state, hash) c.uint64.encode(state, length) c.uint64.encode(state, fork) return state.buffer } -exports.treeSignableLegacy = function (hash, length, fork) { - const state = { start: 0, end: 48, buffer: b4a.allocUnsafe(48) } +exports.treeSignableCompat = function (hash, length, fork, noHeader) { + const end = noHeader ? 48 : 80 + const state = { start: 0, end, buffer: b4a.allocUnsafe(end) } + if (!noHeader) c.raw.encode(state, TREE) // ultra legacy mode, kill in future major c.raw.encode(state, hash) c.uint64.encode(state, length) c.uint64.encode(state, fork) diff --git a/lib/core.js b/lib/core.js index d77cff81..03f9f3a7 100644 --- a/lib/core.js +++ b/lib/core.js @@ -8,24 +8,23 @@ const BlockStore = require('./block-store') const Bitfield = require('./bitfield') const Info = require('./info') const { BAD_ARGUMENT, STORAGE_EMPTY, STORAGE_CONFLICT, INVALID_SIGNATURE } = require('hypercore-errors') -const c = require('compact-encoding') const m = require('./messages') -const caps = require('./caps') +const { manifestHash, createVerifier, createManifest, defaultSignerManifest, isCompat } = require('./manifest') module.exports = class Core { - constructor (header, crypto, oplog, bigHeader, tree, blocks, bitfield, auth, legacy, onupdate, onconflict) { + constructor (header, crypto, oplog, bigHeader, tree, blocks, bitfield, verifier, legacy, onupdate, onconflict) { this.onupdate = onupdate this.onconflict = onconflict this.preupdate = null this.header = header - this.compat = !!(header.manifest && header.manifest.signer && b4a.equals(header.key, header.manifest.signer.publicKey)) + this.compat = isCompat(header.key, header.manifest) this.crypto = crypto this.oplog = oplog this.bigHeader = bigHeader this.tree = tree this.blocks = blocks this.bitfield = bitfield - this.defaultAuth = auth + this.verifier = verifier this.truncating = 0 this.updating = false this.closed = false @@ -54,26 +53,16 @@ module.exports = class Core { } } - static createAuth (crypto, header) { - const manifest = header.manifest || defaultSignerManifest(header.keyPair.publicKey) - const secretKey = header.keyPair && header.keyPair.secretKey - const publicKey = manifest.signer.publicKey - const sign = signable => crypto.sign(signable, secretKey) - - return { - sign: secretKey ? sign : null, - verify (signable, signature) { - return crypto.verify(signable, signature, publicKey) - } - } - } - static async resume (oplogFile, treeFile, bitfieldFile, dataFile, headerFile, opts) { let overwrite = opts.overwrite === true const force = opts.force === true const createIfMissing = opts.createIfMissing !== false const crypto = opts.crypto || hypercoreCrypto + // default to true for now if no manifest is provided + const compat = opts.compat === true || (opts.compat !== false && !opts.manifest) + // kill this flag soon + const legacy = !!opts.legacy const oplog = new Oplog(oplogFile, { headerEncoding: m.oplog.header, @@ -94,18 +83,19 @@ module.exports = class Core { throw STORAGE_EMPTY('No Hypercore is stored here') } - if (opts.compat) { + if (compat) { if (opts.key && opts.keyPair && !b4a.equals(opts.key, opts.keyPair.publicKey)) { - throw BAD_ARGUMENT('Key must match publicKey when in compat mode.') + throw BAD_ARGUMENT('Key must match publicKey when in compat mode') } } const keyPair = opts.keyPair || (opts.key ? null : crypto.keyPair()) - const manifest = opts.manifest || (opts.key && !opts.compat) ? null : defaultSignerManifest(opts.key || keyPair.publicKey) + const noManifest = !opts.manifest && (compat || !!keyPair) + const manifest = noManifest ? defaultSignerManifest(opts.key || keyPair.publicKey) : createManifest(opts.manifest) header = { external: null, - key: opts.key || (opts.compat ? manifest.signer.publicKey : hashManifest(manifest)), + key: opts.key || (compat ? manifest.signer.publicKey : manifestHash(manifest)), manifest, keyPair, userData: [], @@ -126,6 +116,17 @@ module.exports = class Core { header = await bigHeader.load(header.external) } + if (opts.manifest) { + if (compat && !b4a.equals(header.key, opts.manifest.signer.publicKey)) { + throw BAD_ARGUMENT('Key must match publicKey when in compat mode') + } + + // if we provide a manifest and no key, verify that the stored key is the same + if (!opts.key && !b4a.equals(header.key, manifestHash(createManifest(opts.manifest)))) { + throw STORAGE_CONFLICT('Manifest does not hash to provided key') + } + } + if (opts.key && !b4a.equals(header.key, opts.key)) { throw STORAGE_CONFLICT('Another Hypercore is stored here') } @@ -149,7 +150,7 @@ module.exports = class Core { while (bitfield.get(header.hints.contiguousLength)) header.hints.contiguousLength++ } - const auth = opts.auth || (header.manifest ? this.createAuth(crypto, header) : null) + const verifier = header.manifest ? createVerifier(header.manifest, { compat: isCompat(header.key, header.manifest), crypto, legacy }) : null for (const e of entries) { if (e.userData) { @@ -181,7 +182,7 @@ module.exports = class Core { } } - return new this(header, crypto, oplog, bigHeader, tree, blocks, bitfield, auth, !!opts.legacy, opts.onupdate || noop, opts.onconflict || noop) + return new this(header, crypto, oplog, bigHeader, tree, blocks, bitfield, verifier, legacy, opts.onupdate || noop, opts.onconflict || noop) } _shouldFlush () { @@ -199,7 +200,7 @@ module.exports = class Core { return false } - async copyFrom (src, signature, auth = this.defaultAuth) { + async copyFrom (src, signature) { this._mutex.lock() try { @@ -242,14 +243,16 @@ module.exports = class Core { this.tree.roots = [...src.tree.roots] this.tree.length = src.tree.length this.tree.byteLength = src.tree.byteLength - this.tree.signature = null // must provide signature - this.tree.signature = signature || auth.sign(this.tree.signable()) - - if (signature && !this._verifyBatchUpgrade(this.tree, null)) { - // TODO: how to handle signature failure? + try { + const batch = this.tree.batch() + batch.signature = signature + this._verifyBatchUpgrade(batch, this.header.manifest) + this.tree.signature = signature + } catch (err) { this.tree.signature = null - throw INVALID_SIGNATURE('Clone was provided with an invalid signature') + // TODO: how to handle signature failure? + throw err } this.header.tree.length = this.tree.length @@ -316,13 +319,13 @@ module.exports = class Core { } } - async truncate (length, fork, auth = this.defaultAuth) { + async truncate (length, fork, { signature, keyPair = this.header.keyPair } = {}) { this.truncating++ await this._mutex.lock() try { const batch = await this.tree.truncate(length, fork) - batch.signature = await auth.sign(batch.signable(), batch) + batch.signature = signature || this.verifier.sign(batch, keyPair) await this._truncate(batch, null) } finally { this.truncating-- @@ -398,11 +401,11 @@ module.exports = class Core { }) } - async append (values, auth = this.defaultAuth, hooks = {}) { + async append (values, { signature, keyPair = this.header.keyPair, preappend } = {}) { await this._mutex.lock() try { - if (hooks.preappend) await hooks.preappend(values) + if (preappend) await preappend(values) if (!values.length) { return { length: this.tree.length, byteLength: this.tree.byteLength } @@ -411,8 +414,7 @@ module.exports = class Core { const batch = this.tree.batch() for (const val of values) batch.append(val) - const hash = batch.hash() - batch.signature = await auth.sign(this._legacy ? batch.signableLegacy(hash) : batch.signable(hash), batch) + batch.signature = signature || this.verifier.sign(batch, keyPair) const entry = { userData: null, @@ -433,7 +435,7 @@ module.exports = class Core { batch.commit() this.header.tree.length = batch.length - this.header.tree.rootHash = hash + this.header.tree.rootHash = batch.hash() this.header.tree.signature = batch.signature const status = 0b0001 | updateContig(this.header, entry.bitfield, this.bitfield) @@ -448,27 +450,24 @@ module.exports = class Core { } _verifyBatchUpgrade (batch, manifest) { - const hash = batch.hash() - const signable = this._legacy ? batch.signableLegacy(hash) : batch.signable(hash) - if (!this.header.manifest) { if (!manifest) { // compat mode, remove in future version manifest = defaultSignerManifest(this.header.key) - } else if (!manifest || !b4a.equals(this.header.key, hashManifest(manifest))) { + } else if (!manifest || !b4a.equals(this.header.key, manifestHash(manifest))) { throw INVALID_SIGNATURE('Proof contains an invalid manifest') // TODO: proper error type } } - const auth = this.defaultAuth || Core.createAuth(this.crypto, { ...this.header, manifest }) + const verifier = this.verifier || createVerifier(manifest, { compat: isCompat(this.header.key, manifest), crypto: this.crypto, legacy: this._legacy }) - if (!batch.signature || !auth.verify(signable, batch.signature, batch)) { + if (!batch.signature || !verifier.verify(batch, batch.signature)) { throw INVALID_SIGNATURE('Proof contains an invalid signature') } - this.defaultAuth = auth if (!this.header.manifest) { - this.compat = !!manifest.signer && b4a.equals(this.header.key, manifest.signer.publicKey) + this.compat = isCompat(this.header.key, manifest) this.header.manifest = manifest + this.verifier = verifier } } @@ -788,21 +787,4 @@ async function flushHeader (oplog, bigHeader, header) { } } -function defaultSignerManifest (publicKey) { - return { - hash: 'blake2b', - static: null, - signer: { - signature: 'ed25519', - namespace: caps.DEFAULT_NAMESPACE, - publicKey - }, - multipleSigners: null - } -} - -function hashManifest (manifest) { - return caps.manifestHash(c.encode(m.manifest, manifest)) -} - function noop () {} diff --git a/lib/manifest.js b/lib/manifest.js new file mode 100644 index 00000000..fd98ce6f --- /dev/null +++ b/lib/manifest.js @@ -0,0 +1,243 @@ +const defaultCrypto = require('hypercore-crypto') +const b4a = require('b4a') +const c = require('compact-encoding') +const { BAD_ARGUMENT } = require('hypercore-errors') + +const m = require('./messages') +const multisig = require('./multisig') +const caps = require('./caps') + +const signatureArray = c.array(c.fixed64) + +module.exports = { + manifestHash, + isCompat, + defaultSignerManifest, + createManifest, + createVerifier +} + +class StaticVerifier { + constructor (treeHash) { + this.treeHash = treeHash + } + + sign () { + return null + } + + verify (batch, signature) { + return b4a.equals(batch.hash(), this.treeHash) + } +} + +class CompatVerifier { + constructor (crypto, signer, legacy) { + validateSigner(signer) + + this.legacy = legacy + this.crypto = crypto + this.publicKey = signer.publicKey + } + + sign (batch, keyPair) { + if (!keyPair || !keyPair.secretKey) throw BAD_ARGUMENT('No signer was passed') + return this.crypto.sign(batch.signableCompat(this.legacy), keyPair.secretKey) + } + + verify (batch, signature) { + return this.crypto.verify(batch.signableCompat(this.legacy), signature, this.publicKey) + } +} + +class SingleVerifier { + constructor (crypto, signer) { + validateSigner(signer) + + this.crypto = crypto + this.publicKey = signer.publicKey + this.namespace = signer.namespace + } + + sign (batch, keyPair) { + if (!keyPair || !keyPair.secretKey) throw BAD_ARGUMENT('No signer was passed') + return this.crypto.sign(batch.signable(this.namespace), keyPair.secretKey) + } + + verify (batch, signature) { + return this.crypto.verify(batch.signable(this.namespace), signature, this.publicKey) + } +} + +class MultiVerifier { + constructor (crypto, multipleSigners) { + this.signers = multipleSigners.signers + this.quorum = multipleSigners.quorum + this.allowPatched = multipleSigners.allowPatched + this.verifiers = this.signers.map(s => new SingleVerifier(crypto, s)) + + if (this.verifiers.length < this.quorum || (this.quorum === 0)) throw BAD_ARGUMENT('Invalid quorum') + } + + sign () { + throw BAD_ARGUMENT('Multi signature must be provided') + } + + verify (batch, signature) { + if (!this.allowPatched) return this._verify(batch, signature) + return this._verifyPatched(batch, signature) + } + + _verify (batch, signature) { + const signatures = c.decode(signatureArray, signature) + + if (signatures.length < this.quorum) return false + + let valid = 0 + const idx = this.verifiers.slice(0) + + for (const sig of signatures) { + if (signed(batch, sig, idx)) valid++ + } + + return valid >= this.quorum + } + + _verifyPatched (batch) { + const { proofs } = multisig.decode(batch.signature) + + if (proofs.length < this.quorum) return false + + let valid = 0 + const idx = this.verifiers.slice(0) + + for (const proof of proofs) { + const ref = batch.clone() + + if (proof.patch) { + try { + if (!ref.verifyUpgrade(proof.patch)) continue + } catch { + continue + } + } + + if (signed(ref, proof.signature, idx)) valid++ + } + + return valid >= this.quorum + } +} + +function createVerifier (manifest, { compat = false, crypto = defaultCrypto, legacy = false } = {}) { + if (compat && manifest.signer) { + return new CompatVerifier(crypto, manifest.signer, legacy) + } + + if (manifest.static) { + return new StaticVerifier(manifest.static) + } + + if (manifest.signer) { + return new SingleVerifier(crypto, manifest.signer) + } + + if (manifest.multipleSigners) { + return new MultiVerifier(crypto, manifest.multipleSigners) + } + + throw BAD_ARGUMENT('No signer was provided') +} + +function createManifest (inp) { + const manifest = { + hash: 'blake2b', + static: null, + signer: null, + multipleSigners: null + } + + if (inp.hash && inp.hash !== 'blake2b') throw BAD_ARGUMENT('Only Blake2b hashes are supported') + + if (inp.static) { + if (!(b4a.isBuffer(inp.static) && inp.static.byteLength === 32)) throw BAD_ARGUMENT('Invalid static manifest') + manifest.static = inp.static + return manifest + } + + if (inp.signer) { + manifest.signer = parseSigner(inp.signer) + return manifest + } + + if (inp.multipleSigners) { + manifest.multipleSigners = parseMultipleSigners(inp.multipleSigners) + return manifest + } + + throw BAD_ARGUMENT('No signer was provided') +} + +function parseMultipleSigners (m) { + if (m.signers.length < m.quorum || !(m.quorum > 0)) throw BAD_ARGUMENT('Invalid quorum') + + return { + allowPatched: !!m.allowPatched, + quorum: m.quorum, + signers: m.signers.map(parseSigner) + } +} + +function parseSigner (signer) { + validateSigner(signer) + return { + signature: 'ed25519', + namespace: signer.namespace || caps.DEFAULT_NAMESPACE, + publicKey: signer.publicKey + } +} + +function validateSigner (signer) { + if (!signer || !signer.publicKey) throw BAD_ARGUMENT('Signer missing public key') + if (signer.signature !== 'ed25519') throw BAD_ARGUMENT('Only Ed25519 signatures are supported') +} + +function defaultSignerManifest (publicKey) { + return { + hash: 'blake2b', + static: null, + signer: { + signature: 'ed25519', + namespace: caps.DEFAULT_NAMESPACE, + publicKey + }, + multipleSigners: null + } +} + +function manifestHash (manifest) { + const state = { start: 0, end: 32, buffer: null } + m.manifest.preencode(state, manifest) + state.buffer = b4a.allocUnsafe(state.end) + c.raw.encode(state, caps.MANIFEST) + m.manifest.encode(state, manifest) + return defaultCrypto.hash(state.buffer) +} + +function isCompat (key, manifest) { + return !!(manifest && manifest.signer && b4a.equals(key, manifest.signer.publicKey)) +} + +function signed (batch, signature, idx) { + for (let i = 0; i < idx.length; i++) { + const indexer = idx[i] + if (!indexer.verify(batch, signature)) continue + + const swap = idx.pop() + if (indexer !== swap) idx[i--] = swap + + return true + } + + return false +} diff --git a/lib/merkle-tree.js b/lib/merkle-tree.js index 44f69c60..b9f6851f 100644 --- a/lib/merkle-tree.js +++ b/lib/merkle-tree.js @@ -79,12 +79,12 @@ class MerkleTreeBatch { return this.hashCached } - signable (hash = this.hash()) { - return caps.treeSignable(hash, this.length, this.fork) + signable (namespace) { + return caps.treeSignable(namespace, this.hash(), this.length, this.fork) } - signableLegacy (hash = this.hash()) { - return caps.treeSignableLegacy(hash, this.length, this.fork) + signableCompat (noHeader) { + return caps.treeSignableCompat(this.hash(), this.length, this.fork, noHeader) } get (index) { @@ -425,8 +425,8 @@ module.exports = class MerkleTree { return this.crypto.tree(this.roots) } - signable (hash = this.hash()) { - return caps.treeSignable(hash, this.length, this.fork) + signable (namespace) { + return caps.treeSignable(namespace, this.hash(), this.length, this.fork) } getRoots (length) { diff --git a/lib/messages.js b/lib/messages.js index 2abead22..30ac5182 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -868,3 +868,91 @@ oplog.header = { } } } + +const uintArray = c.array(c.uint) + +const patchEncoding = { + preencode (state, n) { + c.uint.preencode(state, n.start) + c.uint.preencode(state, n.length) + uintArray.preencode(state, n.nodes) + }, + encode (state, n) { + c.uint.encode(state, n.start) + c.uint.encode(state, n.length) + uintArray.encode(state, n.nodes) + }, + decode (state) { + return { + start: c.uint.decode(state), + length: c.uint.decode(state), + nodes: uintArray.decode(state) + } + } +} + +const proofsEncoding = c.array({ + preencode (state, n) { + c.uint.preencode(state, n.length) + c.fixed64.preencode(state, n.signature) + + // flag + state.end++ + + if (n.patch) patchEncoding.preencode(state, n.patch) + }, + encode (state, n) { + c.uint.encode(state, n.length) + c.fixed64.encode(state, n.signature) + c.uint.encode(state, n.patch ? 1 : 0) + if (n.patch) patchEncoding.encode(state, n.patch) + }, + decode (state) { + return { + length: c.uint.decode(state), + signature: c.fixed64.decode(state), + patch: c.uint.decode(state) ? patchEncoding.decode(state) : null + } + } +}) + +const nodesEncoding = c.array({ + preencode (state, n) { + c.uint.preencode(state, n.index) + c.uint.preencode(state, n.size) + c.fixed32.preencode(state, n.hash) + }, + encode (state, n) { + c.uint.encode(state, n.index) + c.uint.encode(state, n.size) + c.fixed32.encode(state, n.hash) + }, + decode (state) { + return { + index: c.uint.decode(state), + size: c.uint.decode(state), + hash: c.fixed32.decode(state) + } + } +}) + +exports.multisignature = { + preencode (state, s) { + c.uint.preencode(state, s.length) + proofsEncoding.preencode(state, s.proofs) + nodesEncoding.preencode(state, s.nodes) + }, + encode (state, s) { + c.uint.encode(state, s.length) + proofsEncoding.encode(state, s.proofs) + nodesEncoding.encode(state, s.nodes) + }, + decode (state) { + return { + length: c.uint.decode(state), + proofs: proofsEncoding.decode(state), + nodes: nodesEncoding.decode(state) + + } + } +} diff --git a/lib/multisig.js b/lib/multisig.js new file mode 100644 index 00000000..2e9be957 --- /dev/null +++ b/lib/multisig.js @@ -0,0 +1,146 @@ +const c = require('compact-encoding') +const b4a = require('b4a') + +const encoding = require('./messages').multisignature + +module.exports = { + encode, + decode, + assemble, + partialSignature +} + +function encode (signature) { + return c.encode(encoding, signature) +} + +function decode (data) { + const signature = c.decode(encoding, data) + for (const proof of signature.proofs) { + if (proof.patch) { + proof.patch = inflateUpgrade(proof.patch, signature.nodes) + } + } + + return signature +} + +function assemble (partials) { + return encode(aggregate(partials, partials.length)) +} + +async function partialSignature (core, length) { + if (length >= core.core.tree.length) length = null + + return { + length: core.core.tree.length, + signature: b4a.from(core.core.tree.signature), + patch: await upgrade(core, length) + } +} + +async function upgrade (core, from) { + if (!from && from !== 0) return null + + const tree = core.core.tree + const p = await tree.proof({ upgrade: { start: from, length: tree.length - from } }) + return p.upgrade +} + +function aggregate (inputs, thres) { + let min = -1 + const selected = [] + + for (let i = 0; i < inputs.length; i++) { + const length = inputs[i].length + const lowest = min < 0 ? null : selected[min] + + if (selected.length < thres) { + const j = selected.push(inputs[i]) - 1 + if (!lowest || length < lowest.length) min = j + continue + } + + if (length <= lowest.length) continue + selected[min] = inputs[i] + } + + const length = selected[min].length + + const proofs = [] + const nodes = [] + + for (const u of selected) { + proofs.push(compressProof(u, nodes)) + } + + return { + length, + proofs, + nodes + } +} + +function compareNode (a, b) { + if (a.index !== b.index) return false + if (a.size !== b.size) return false + return b4a.equals(a.hash, b.hash) +} + +function compressProof (proof, nodes) { + const c = {} + + c.length = proof.length + c.signature = proof.signature + if (proof.patch) c.patch = compressUpgrade(proof.patch, nodes) + + return c +} + +function compressUpgrade (p, nodes) { + const u = {} + + u.length = p.length + u.start = p.start + u.signature = p.signature + u.publicKey = null + u.nodes = [] + + for (const node of p.nodes) { + let present = false + for (let i = 0; i < nodes.length; i++) { + if (!compareNode(nodes[i], node)) continue + + u.nodes.push(i) + present = true + break + } + + if (present) continue + u.nodes.push(nodes.push(node) - 1) + } + + return u +} + +function inflateUpgrade (s, nodes) { + const upgrade = { + start: s.start, + length: s.length, + signature: s.signature, + nodes: [], + additionalNodes: [] + } + + for (const i of s.nodes) { + upgrade.nodes.push(nodes[i]) + } + + return { + fork: 0, + block: null, + seek: null, + hash: null, + upgrade + } +} diff --git a/test/all.js b/test/all.js index 8882e376..b56e65aa 100644 --- a/test/all.js +++ b/test/all.js @@ -7,7 +7,6 @@ async function runTests () { test.pause() - await import('./auth.js') await import('./basic.js') await import('./batch.js') await import('./bitfield.js') @@ -20,6 +19,7 @@ async function runTests () { await import('./encodings.js') await import('./encryption.js') await import('./extension.js') + await import('./manifest.js') await import('./merkle-tree.js') await import('./mutex.js') await import('./oplog.js') diff --git a/test/auth.js b/test/auth.js deleted file mode 100644 index 46f8e995..00000000 --- a/test/auth.js +++ /dev/null @@ -1,253 +0,0 @@ -const test = require('brittle') -const RAM = require('random-access-memory') -const crypto = require('hypercore-crypto') -const sodium = require('sodium-universal') -const b4a = require('b4a') -const { eventFlush, replicate } = require('./helpers') - -const Hypercore = require('../') - -test.skip('multisig hypercore', async function (t) { - t.plan(2) - - const k1 = crypto.keyPair() - const k2 = crypto.keyPair() - - const auth = { - sign: (signable) => { - const sig1 = crypto.sign(signable, k1.secretKey) - const sig2 = crypto.sign(signable, k2.secretKey) - - return b4a.concat([sig1, sig2]) - }, - verify: (signable, signature) => { - const sig1 = signature.subarray(0, 64) - const sig2 = signature.subarray(64) - - return crypto.verify(signable, sig1, k1.publicKey) && - crypto.verify(signable, sig2, k2.publicKey) - } - } - - const a = new Hypercore(RAM, null, { - valueEncoding: 'utf-8', - auth - }) - - await a.ready() - - const b = new Hypercore(RAM, a.key, { - valueEncoding: 'utf-8', - auth - }) - - await b.ready() - - await a.append(['a', 'b', 'c', 'd', 'e']) - - t.is(a.length, 5) - - replicate(a, b, t) - - const r = b.download({ start: 0, end: a.length }) - await r.done() - - t.is(b.length, 5) -}) - -test.skip('multisig hypercore with instance and extension', async function (t) { - t.plan(3) - - class MultiSigAuth { - constructor (local, remote, opts = {}) { - this.local = local - this.remote = remote - - this._sign = opts.sign - ? opts.sign - : s => crypto.sign(s, opts.keyPair.secretKey) - - this.sigs = [] - - this.localFirst = b4a.compare(this.local, this.remote) < 0 - } - - sign (signable) { - const sig = this.sigs.find(({ signature }) => { - const s = b4a.from(signature, 'base64') - return crypto.verify(signable, s, this.remote) - }) - - if (!sig) throw new Error('No remote signature.') - - const local = this._sign(signable) - const remote = b4a.from(sig.signature, 'base64') - - const sigs = [] - sigs.push(this.localFirst ? local : remote) - sigs.push(this.localFirst ? remote : local) - - return b4a.concat(sigs) - } - - verify (signable, signature) { - const sig1 = signature.subarray(0, 64) - const sig2 = signature.subarray(64) - - const key1 = this.localFirst ? this.local : this.remote - const key2 = this.localFirst ? this.remote : this.local - - return crypto.verify(signable, sig1, key1) && - crypto.verify(signable, sig2, key2) - } - - addSignature (m) { - this.sigs.push(m) - } - } - - const aKey = crypto.keyPair() - const bKey = crypto.keyPair() - - const a = new Hypercore(RAM, null, { - valueEncoding: 'utf-8', - auth: new MultiSigAuth(aKey.publicKey, bKey.publicKey, { keyPair: aKey }) - }) - - await a.ready() - - const b = new Hypercore(RAM, a.key, { - valueEncoding: 'utf-8', - auth: new MultiSigAuth(bKey.publicKey, aKey.publicKey, { keyPair: bKey }) - }) - - await b.ready() - - replicate(a, b, t) - - a.registerExtension('multisig-extension', { - encoding: 'json', - onmessage: a.auth.addSignature.bind(a.auth) - }) - - const ext = b.registerExtension('multisig-extension', { - encoding: 'json', - onmessage: b.auth.addSignature.bind(b.auth) - }) - - await eventFlush() - t.is(b.peers.length, 1) - - const data = 'hello' - - const batch = b.core.tree.batch() - batch.append(b._encode(b.valueEncoding, data)) - - const signable = batch.signable() - const signature = crypto.sign(signable, bKey.secretKey).toString('base64') - - ext.send({ data, signature, length: a.length }, b.peers[0]) - - await eventFlush() - - await a.append('hello') - - t.is(a.length, 1) - - const r = b.download({ start: 0, end: a.length }) - await r.done() - - t.is(a.length, 1) -}) - -test.skip('proof-of-work hypercore', async function (t) { - t.plan(2) - - const ZEROES = 8 - - const auth = { - sign: (signable) => { - const sig = new Uint8Array(32) - const view = new DataView(sig.buffer) - - for (let i = 0; ;) { - view.setUint32(0, i++, true) - const buf = hash(signable, sig) - - let test = 0 - for (let j = 0; j < ZEROES / 8; j++) test |= buf[j] - - if (test) continue - return sig - } - }, - verify: (signable, signature) => { - const buf = hash(signable, signature) - - let test = 0 - for (let j = 0; j < ZEROES / 8; j++) test |= buf[j] - return test === 0 - } - } - - const a = new Hypercore(RAM, null, { - valueEncoding: 'utf-8', - auth - }) - - await a.ready() - - const b = new Hypercore(RAM, a.key, { - valueEncoding: 'utf-8', - auth - }) - - await b.ready() - - await a.append(['a', 'b', 'c', 'd', 'e']) - - t.is(a.length, 5) - - replicate(a, b, t) - - const r = b.download({ start: 0, end: a.length }) - await r.done() - - t.is(b.length, 5) -}) - -test.skip('core using custom sign fn', async function (t) { - t.plan(2) - - const keyPair = crypto.keyPair() - - const a = new Hypercore(RAM, null, { - valueEncoding: 'utf-8', - sign: (signable) => crypto.sign(signable, keyPair.secretKey), - keyPair: { - publicKey: keyPair.publicKey - } - }) - - await a.ready() - - const b = new Hypercore(RAM, a.key, { valueEncoding: 'utf-8' }) - await b.ready() - - await a.append(['a', 'b', 'c', 'd', 'e']) - - t.is(a.length, 5) - - replicate(a, b, t) - - const r = b.download({ start: 0, end: a.length }) - await r.done() - - t.is(b.length, 5) -}) - -function hash (...data) { - const out = b4a.alloc(32) - sodium.crypto_generichash(out, b4a.concat(data)) - return out -} diff --git a/test/batch.js b/test/batch.js index 6f9f1e5e..2da58e53 100644 --- a/test/batch.js +++ b/test/batch.js @@ -1,6 +1,7 @@ const test = require('brittle') const b4a = require('b4a') +const NS = b4a.alloc(32) const { create } = require('./helpers') test('batch append', async function (t) { @@ -172,25 +173,25 @@ test('partial flush', async function (t) { await b.append(['a', 'b', 'c', 'd']) - await b.flush(2) + await b.flush({ length: 2 }) t.is(core.length, 2) t.is(b.length, 4) t.is(b.byteLength, 4) - await b.flush(1) + await b.flush({ length: 1 }) t.is(core.length, 3) t.is(b.length, 4) t.is(b.byteLength, 4) - await b.flush(1) + await b.flush({ length: 1 }) t.is(core.length, 4) t.is(b.length, 4) t.is(b.byteLength, 4) - await b.flush(1) + await b.flush({ length: 1 }) t.is(core.length, 4) t.is(b.length, 4) @@ -287,15 +288,15 @@ test('create tree batches', async function (t) { t2.append(b4a.from('c')) - t.alike(t3.signable(), t2.signable()) + t.alike(t3.signable(NS), t2.signable(NS)) - const t4s = t4.signable() + const t4s = t4.signable(NS) await b.append('d') - t.alike(b.createTreeBatch().signable(), t4s) + t.alike(b.createTreeBatch().signable(NS), t4s) await b.append('e') - t.alike(b.createTreeBatch().signable(), t5.signable()) + t.alike(b.createTreeBatch().signable(NS), t5.signable(NS)) // remove appended values blocks.shift() @@ -312,7 +313,7 @@ test('create tree batches', async function (t) { await b2.ready() t.absent(b2.createTreeBatch(3)) - t.alike(t4.signable(), t4s) + t.alike(t4.signable(NS), t4s) const t6 = b2.createTreeBatch(6, blocks) const t7 = b2.createTreeBatch(7, blocks) @@ -321,8 +322,8 @@ test('create tree batches', async function (t) { t.is(t7.length, 7) await b2.append('f') - t.alike(b2.createTreeBatch().signable(), t6.signable()) + t.alike(b2.createTreeBatch().signable(NS), t6.signable(NS)) await b2.append('g') - t.alike(b2.createTreeBatch().signable(), t7.signable()) + t.alike(b2.createTreeBatch().signable(NS), t7.signable(NS)) }) diff --git a/test/clone.js b/test/clone.js index a739c2a7..93a655ed 100644 --- a/test/clone.js +++ b/test/clone.js @@ -12,7 +12,7 @@ test('clone', async function (t) { await core.append('hello') await core.append('world') - const clone = await core.clone(RAM) + const clone = core.clone(crypto.keyPair(), RAM) await clone.ready() const info = await clone.info() @@ -29,7 +29,7 @@ test('clone - append after clone', async function (t) { await core.append('hello') await core.append('world') - const clone = await core.clone(RAM) + const clone = core.clone(crypto.keyPair(), RAM) await clone.ready() const hash = clone.core.tree.hash() @@ -49,7 +49,7 @@ test('clone - src appends after clone', async function (t) { await core.append('hello') await core.append('world') - const clone = await core.clone(RAM) + const clone = core.clone(crypto.keyPair(), RAM) await clone.ready() const hash = clone.core.tree.hash() @@ -71,7 +71,7 @@ test('clone - truncate src after', async function (t) { await core.append('goodbye') await core.append('home') - const clone = await core.clone(RAM) + const clone = core.clone(crypto.keyPair(), RAM) await clone.ready() const hash = clone.core.tree.hash() @@ -95,10 +95,10 @@ test('clone - pass new keypair', async function (t) { const keyPair = crypto.keyPair() - const batch = await core.core.tree.batch() - const signature = crypto.sign(batch.signable(), keyPair.secretKey) + const batch = core.core.tree.batch() + const signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const clone = await core.clone(RAM, { keyPair, signature }) + const clone = core.clone(keyPair, RAM, { signature }) await clone.ready() t.is(clone.length, 4) @@ -109,21 +109,21 @@ test('clone - pass new keypair', async function (t) { test('clone - sparse', async function (t) { const core = await create() - const replica = await create(core.key, { auth: core.core.defaultAuth, sparse: true }) + const replica = await create(core.key, { manifest: core.manifest, sparse: true }) await core.append('hello') await core.append('world') await core.append('goodbye') await core.append('home') - replicate(core, replica) + replicate(core, replica, t) await replica.get(0) await replica.get(3) t.is(replica.length, 4) - const clone = await replica.clone(RAM) + const clone = replica.clone(crypto.keyPair(), RAM) await clone.ready() t.is(clone.length, 4) @@ -142,32 +142,12 @@ test('clone - sparse', async function (t) { await t.execution(clone.append('final')) }) -test('clone - replicate clones', async function (t) { - const core = await create() - - const clone = await core.clone(RAM) - await clone.ready() - - await core.append('hello') - await core.append('world') - await core.append('goodbye') - await core.append('home') - - const full = await core.clone(RAM) - await full.ready() - - replicate(clone, full) - - t.alike(await clone.get(3), await core.get(3)) - t.is(clone.length, 4) -}) - test('clone - replicate clones new key', async function (t) { const core = await create() const keyPair = crypto.keyPair() - const clone = await core.clone(RAM, { keyPair }) + const clone = core.clone(keyPair, RAM) await clone.ready() await core.append('hello') @@ -175,10 +155,10 @@ test('clone - replicate clones new key', async function (t) { await core.append('goodbye') await core.append('home') - const batch = await core.core.tree.batch() - const signature = crypto.sign(batch.signable(), keyPair.secretKey) + const batch = core.core.tree.batch() + const signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const full = await core.clone(RAM, { keyPair, signature }) + const full = core.clone(keyPair, RAM, { signature }) await full.ready() replicate(clone, full) @@ -205,7 +185,7 @@ test('clone - replicate clones new key', async function (t) { test('clone - replicate sparse clone with new key', async function (t) { const core = await create() - const replica = await create(core.key, { auth: core.core.defaultAuth, sparse: true }) + const replica = await create(core.key, { manifest: core.core.header.manifest, sparse: true }) const keyPair = crypto.keyPair() @@ -214,10 +194,10 @@ test('clone - replicate sparse clone with new key', async function (t) { await core.append('goodbye') await core.append('home') - const batch = await core.core.tree.batch() - const signature = crypto.sign(batch.signable(), keyPair.secretKey) + const batch = core.core.tree.batch() + const signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const full = await core.clone(RAM, { keyPair, signature }) + const full = core.clone(keyPair, RAM, { signature }) await full.ready() replicate(core, replica) @@ -227,7 +207,7 @@ test('clone - replicate sparse clone with new key', async function (t) { t.is(replica.length, 4) - const clone = await replica.clone(RAM, { keyPair, signature }) + const clone = replica.clone(keyPair, RAM, { signature }) await clone.ready() t.is(clone.length, 4) @@ -249,10 +229,10 @@ test('clone - persist clone to disk', async function (t) { await core.append('goodbye') await core.append('home') - const batch = await core.core.tree.batch() - const signature = crypto.sign(batch.signable(), keyPair.secretKey) + const batch = core.core.tree.batch() + const signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const clone = await core.clone(storage, { keyPair, signature }) + const clone = core.clone(keyPair, storage, { signature }) await clone.ready() t.is(clone.length, 4) @@ -270,40 +250,6 @@ test('clone - persist clone to disk', async function (t) { await reopened.close() }) -test('clone - persisted clone can replicate', async function (t) { - const core = await create() - const storage = await tmpDir(t) - - await core.append('hello') - await core.append('world') - - const clone = await core.clone(storage) - await clone.ready() - - await core.append('goodbye') - await core.append('home') - - t.is(clone.length, 2) - - await clone.close() - - const reopened = new Hypercore(storage) - await reopened.ready() - - t.is(reopened.length, 2) - - replicate(reopened, core) - - t.alike(await reopened.get(0), await core.get(0)) - t.alike(await reopened.get(1), await core.get(1)) - t.alike(await reopened.get(2), await core.get(2)) - t.alike(await reopened.get(3), await core.get(3)) - - t.is(reopened.length, 4) - - await reopened.close() -}) - test('clone - persisted clone with new key can replicate', async function (t) { const core = await create() const storage = await tmpDir(t) @@ -313,19 +259,19 @@ test('clone - persisted clone with new key can replicate', async function (t) { await core.append('hello') await core.append('world') - let batch = await core.core.tree.batch() - let signature = crypto.sign(batch.signable(), keyPair.secretKey) + let batch = core.core.tree.batch() + let signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const clone = await core.clone(storage, { keyPair, signature }) + const clone = core.clone(keyPair, storage, { signature }) await clone.ready() await core.append('goodbye') await core.append('home') - batch = await core.core.tree.batch() - signature = crypto.sign(batch.signable(), keyPair.secretKey) + batch = core.core.tree.batch() + signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) - const fullClone = await core.clone(RAM, { keyPair, signature }) + const fullClone = core.clone(keyPair, RAM, { signature, compat: true }) await fullClone.ready() t.is(clone.length, 2) diff --git a/test/core.js b/test/core.js index 539b7335..352dddd9 100644 --- a/test/core.js +++ b/test/core.js @@ -275,7 +275,7 @@ test('core - update hook is triggered', async function (t) { test('core - clone', async function (t) { const { core } = await create() - const { core: copy } = (await create()) + const { core: copy } = (await create({ keyPair: { publicKey: core.header.keyPair.publicKey } })) await core.userData('hello', Buffer.from('world')) @@ -284,7 +284,7 @@ test('core - clone', async function (t) { Buffer.from('world') ]) - await copy.copyFrom(core) + await copy.copyFrom(core, core.tree.signature) t.alike(copy.header.userData, [{ key: 'hello', value: Buffer.from('world') }]) @@ -319,7 +319,7 @@ test('core - clone verify', async function (t) { const { core: clone } = await create({ keyPair: { publicKey: core.header.keyPair.publicKey } }) await core.append([Buffer.from('a'), Buffer.from('b')]) - await copy.copyFrom(core) + await copy.copyFrom(core, core.tree.signature) t.is(copy.header.keyPair.publicKey, core.header.keyPair.publicKey) t.is(copy.header.keyPair.publicKey, clone.header.keyPair.publicKey) @@ -342,7 +342,7 @@ test('core - clone verify', async function (t) { } }) -test('clone - truncate original', async function (t) { +test.skip('clone - truncate original', async function (t) { const { core } = await create() const { core: copy } = await create({ keyPair: core.header.keyPair }) diff --git a/test/manifest.js b/test/manifest.js new file mode 100644 index 00000000..92099d6d --- /dev/null +++ b/test/manifest.js @@ -0,0 +1,932 @@ +const test = require('brittle') +const c = require('compact-encoding') +const crypto = require('hypercore-crypto') +const b4a = require('b4a') +const tmpDir = require('test-tmp') +const ram = require('random-access-memory') + +const Hypercore = require('../') +const { assemble, partialSignature } = require('../lib/multisig') +const { createVerifier, createManifest } = require('../lib/manifest') +const caps = require('../lib/caps') + +// TODO: move this to be actual tree batches instead - less future surprises +// for now this is just to get the tests to work as they test important things +class AssertionTreeBatch { + constructor (hash, signable) { + this._hash = hash + this._signable = signable + } + + hash () { + return this._hash + } + + signable (ns) { + return b4a.concat([ns, this._signable]) + } + + signableCompat () { + return this._signable + } +} + +test('create verifier - static signer', async function (t) { + const treeHash = b4a.alloc(32, 1) + + const manifest = { + static: treeHash + } + + const verifier = createVerifier(manifest) + + const batch = new AssertionTreeBatch(b4a.alloc(32, 1), null) + + t.ok(verifier.verify(batch)) + + batch._hash[0] ^= 0xff + + t.absent(verifier.verify(batch)) +}) + +test('create verifier - single signer no sign', async function (t) { + const keyPair = crypto.keyPair() + + const namespace = b4a.alloc(32, 2) + + const manifest = { + signer: { + signature: 'ed25519', + namespace, + publicKey: keyPair.publicKey + } + } + + const verifier = createVerifier(manifest) + + const batch = new AssertionTreeBatch(null, b4a.alloc(32, 1)) + + const signature = crypto.sign(batch.signable(namespace), keyPair.secretKey) + + t.ok(verifier.verify(batch, signature)) + + signature[0] ^= 0xff + + t.absent(verifier.verify(batch, signature)) +}) + +test('create verifier - single signer', async function (t) { + const keyPair = crypto.keyPair() + + const namespace = b4a.alloc(32, 2) + + const manifest = { + signer: { + signature: 'ed25519', + namespace, + publicKey: keyPair.publicKey + } + } + + const verifier = createVerifier(manifest) + + const batch = new AssertionTreeBatch(null, b4a.alloc(32, 1)) + const signature = verifier.sign(batch, keyPair) + + t.ok(verifier.verify(batch, signature)) + + signature[0] ^= 0xff + + t.absent(verifier.verify(batch, signature)) +}) + +test('create verifier - multi signer', async function (t) { + const a = crypto.keyPair() + const b = crypto.keyPair() + + const signable = b4a.alloc(32, 1) + const aEntropy = b4a.alloc(32, 2) + const bEntropy = b4a.alloc(32, 3) + + const manifest = { + multipleSigners: { + allowPatched: false, + quorum: 2, + signers: [{ + publicKey: a.publicKey, + namespace: aEntropy, + signature: 'ed25519' + }, { + publicKey: b.publicKey, + namespace: bEntropy, + signature: 'ed25519' + }] + } + } + + const batch = new AssertionTreeBatch(null, signable) + + const asig = crypto.sign(batch.signable(aEntropy), a.secretKey) + const bsig = crypto.sign(batch.signable(bEntropy), b.secretKey) + + const enc = c.array(c.fixed64) + + const signature = c.encode(enc, [asig, bsig]) + const badSignature = c.encode(enc, [asig, asig]) + + const verifier = createVerifier(manifest) + + t.ok(verifier.verify(batch, signature)) + t.absent(verifier.verify(batch, badSignature)) +}) + +test('create verifier - defaults', async function (t) { + const keyPair = crypto.keyPair() + + const manifest = createManifest({ + signer: { + signature: 'ed25519', + publicKey: keyPair.publicKey + } + }) + + const verifier = createVerifier(manifest) + + const batch = new AssertionTreeBatch(null, b4a.alloc(32, 1)) + const signature = verifier.sign(batch, keyPair) + + t.ok(verifier.verify(batch, signature)) + + signature[0] ^= 0xff + + t.absent(verifier.verify(batch, signature)) +}) + +test('create verifier - unsupported curve', async function (t) { + t.plan(2) + + const keyPair = crypto.keyPair() + + const manifest = { + signer: { + signature: 'SECP_256K1', + publicKey: keyPair.publicKey + } + } + + try { + createManifest(manifest) + } catch { + t.pass('threw') + } + + try { + createVerifier(manifest) + } catch { + t.pass('also threw') + } +}) + +test('create verifier - compat signer', async function (t) { + const keyPair = crypto.keyPair() + + const namespace = b4a.alloc(32, 2) + + const manifest = { + signer: { + signature: 'ed25519', + namespace, + publicKey: keyPair.publicKey + } + } + + const verifier = createVerifier(manifest, { compat: true }) + + const batch = new AssertionTreeBatch(null, b4a.alloc(32, 1)) + + const signature = crypto.sign(batch.signableCompat(), keyPair.secretKey) + + t.alike(verifier.sign(batch, keyPair), signature) + t.ok(verifier.verify(batch, signature)) +}) + +test('multisig - append', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[1].append(b4a.from('0')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append(b4a.from('0'), { signature: multisig })) + + t.is(core.length, 1) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, core.length) + + await core2.download({ start: 0, end: core.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) +}) + +test('multisig - batch failed', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + + await Promise.all(signers.map(s => s.ready())) + + let multisig = null + + const manifest = createMultiManifest(signers) + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[1].append(b4a.from('0')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append(b4a.from('hello'), { signature: multisig })) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s2.on('error', reject) + + setImmediate(resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.exception(p) + + t.is(core2.length, 0) +}) + +test('multisig - patches', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + await signers[0].append(b4a.from('4')) + + await signers[1].append(b4a.from('0')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append(b4a.from('0'), { signature: multisig })) + + t.is(core.length, 1) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, core.length) + + await core2.download({ start: 0, end: core.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) +}) + +test('multisig - batch append', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + + await signers[1].append(b4a.from('0')) + await signers[1].append(b4a.from('1')) + await signers[1].append(b4a.from('2')) + await signers[1].append(b4a.from('3')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append([ + b4a.from('0'), + b4a.from('1'), + b4a.from('2'), + b4a.from('3') + ], { + signature: multisig + })) + + t.is(core.length, 4) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, core.length) + + await core2.download({ start: 0, end: core.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) + t.alike(await core2.get(1), b4a.from('1')) + t.alike(await core2.get(2), b4a.from('2')) + t.alike(await core2.get(3), b4a.from('3')) +}) + +test('multisig - batch append with patches', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + await signers[0].append(b4a.from('4')) + await signers[0].append(b4a.from('5')) + + await signers[1].append(b4a.from('0')) + await signers[1].append(b4a.from('1')) + await signers[1].append(b4a.from('2')) + await signers[1].append(b4a.from('3')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append([ + b4a.from('0'), + b4a.from('1'), + b4a.from('2'), + b4a.from('3') + ], { + signature: multisig + })) + + t.is(core.length, 4) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, core.length) + + await core2.download({ start: 0, end: core.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) + t.alike(await core2.get(1), b4a.from('1')) + t.alike(await core2.get(2), b4a.from('2')) + t.alike(await core2.get(3), b4a.from('3')) +}) + +test('multisig - cannot divide batch', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + + await signers[1].append(b4a.from('0')) + await signers[1].append(b4a.from('1')) + await signers[1].append(b4a.from('2')) + await signers[1].append(b4a.from('3')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append([ + b4a.from('0'), + b4a.from('1') + ], { + signature: multisig + })) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.exception(p) + + t.is(core2.length, 0) +}) + +test('multisig - multiple appends', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig1 = null + let multisig2 = null + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + await signers[0].append(b4a.from('4')) + await signers[0].append(b4a.from('5')) + + await signers[1].append(b4a.from('0')) + await signers[1].append(b4a.from('1')) + + multisig1 = assemble([ + await partialSignature(signers[0], signers[1].length), + await partialSignature(signers[1]) + ]) + + await signers[1].append(b4a.from('2')) + await signers[1].append(b4a.from('3')) + + multisig2 = assemble([ + await partialSignature(signers[0], signers[1].length), + await partialSignature(signers[1]) + ]) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + s1.pipe(s2).pipe(s1) + + const p = new Promise((resolve, reject) => { + s2.on('error', reject) + core2.on('append', resolve) + }) + + core.append([ + b4a.from('0'), + b4a.from('1') + ], { + signature: multisig1 + }) + + await t.execution(p) + + t.is(core.length, 2) + t.is(core2.length, 2) + + const p2 = new Promise((resolve, reject) => { + s1.on('error', reject) + core.on('append', resolve) + }) + + core2.append([ + b4a.from('2'), + b4a.from('3') + ], { + signature: multisig2 + }) + + await t.execution(p2) + + t.is(core.length, 4) + t.is(core2.length, 4) +}) + +test('multisig - persist to disk', async t => { + const storage = await tmpDir(t) + + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + + const core = new Hypercore(storage, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[1].append(b4a.from('0')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append(b4a.from('0'), { signature: multisig })) + + t.is(core.length, 1) + + await core.close() + + const clone = new Hypercore(storage, { manifest }) + await t.execution(clone.ready()) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = clone.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, clone.length) + + await core2.download({ start: 0, end: clone.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) + + await clone.close() +}) + +test('multisig - overlapping appends', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig1 = null + let multisig2 = null + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + const core2 = new Hypercore(ram, { manifest }) + await core.ready() + + await signers[0].append(b4a.from('0')) + await signers[0].append(b4a.from('1')) + await signers[0].append(b4a.from('2')) + await signers[0].append(b4a.from('3')) + await signers[0].append(b4a.from('4')) + await signers[0].append(b4a.from('5')) + + await signers[1].append(b4a.from('0')) + await signers[1].append(b4a.from('1')) + + await signers[2].append(b4a.from('0')) + await signers[2].append(b4a.from('1')) + await signers[2].append(b4a.from('2')) + + multisig1 = assemble([ + await partialSignature(signers[1]), + await partialSignature(signers[0], signers[1].length) + ]) + + multisig2 = assemble([ + await partialSignature(signers[2]), + await partialSignature(signers[0], signers[2].length) + ]) + + await core.append([ + b4a.from('0'), + b4a.from('1') + ], { + signature: multisig1 + }) + + await core2.append([ + b4a.from('0'), + b4a.from('1'), + b4a.from('2') + ], { + signature: multisig2 + }) + + t.is(core.length, 2) + t.is(core2.length, 3) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + s1.pipe(s2).pipe(s1) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + core.on('append', resolve) + }) + + await t.execution(p) + + t.is(core.length, 3) + t.is(core2.length, 3) +}) + +test('multisig - normal operating mode', async t => { + const inputs = [] + + for (let i = 0; i < 0xff; i++) inputs.push(b4a.from([i])) + + const signers = [] + signers.push(new Hypercore(ram, { compat: false })) + signers.push(new Hypercore(ram, { compat: false })) + signers.push(new Hypercore(ram, { compat: false })) + + const [a, b, d] = signers + + await Promise.all(signers.map(s => s.ready())) + const manifest = createMultiManifest(signers) + + const signer1 = signer(a, b) + const signer2 = signer(b, d) + + const core = new Hypercore(ram, { manifest, sign: signer1.sign }) + await core.ready() + + const core2 = new Hypercore(ram, { manifest, sign: signer2.sign }) + await core.ready() + + let ai = 0 + let bi = 0 + let ci = 0 + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + s1.pipe(s2).pipe(s1) + + t.teardown(() => { + s1.destroy() + s2.destroy() + }) + + s1.on('error', t.fail) + s2.on('error', t.fail) + + while (true) { + if (core.length === inputs.length && core2.length === inputs.length) break + + const as = Math.min(inputs.length, ai + 1 + Math.floor(Math.random() * 4)) + const bs = Math.min(inputs.length, bi + 1 + Math.floor(Math.random() * 4)) + const cs = Math.min(inputs.length, ci + 1 + Math.floor(Math.random() * 4)) + + while (ai < as) await a.append(inputs[ai++]) + while (bi < bs) await b.append(inputs[bi++]) + while (ci < cs) await d.append(inputs[ci++]) + + if (Math.random() < 0.5) { + const m1s = Math.min(ai, bi) + if (m1s <= core2.length) continue + + const p = new Promise(resolve => core2.once('append', resolve)) + + core.append(inputs.slice(core.length, m1s), { signature: await signer1() }) + + await p + } else { + const m2s = Math.min(bi, ci) + if (m2s <= core.length) continue + + const p = new Promise(resolve => core.once('append', resolve)) + + core2.append(inputs.slice(core2.length, m2s), { signature: await signer2() }) + + await p + } + } + + t.is(core.length, inputs.length) + t.is(core.length, core2.length) + + for (let i = 0; i < inputs.length; i++) { + const l = await core.get(i) + const r = await core2.get(i) + + if (!b4a.equals(l, r)) t.fail() + if (l[0] !== i) t.fail() + } + + t.pass() + + function signer (w1, w2) { + return async (batch) => { + return assemble([ + await partialSignature(w1, w2.length), + await partialSignature(w2, w1.length) + ]) + } + } +}) + +test('multisig - large patches', async t => { + const signers = [] + for (let i = 0; i < 3; i++) signers.push(new Hypercore(ram, { compat: false })) + await Promise.all(signers.map(s => s.ready())) + + const manifest = createMultiManifest(signers) + + let multisig = null + + const core = new Hypercore(ram, { manifest }) + await core.ready() + + for (let i = 0; i < 10000; i++) { + await signers[0].append(b4a.from(i.toString(10))) + } + + await signers[1].append(b4a.from('0')) + + const proof = await partialSignature(signers[0], signers[1].length) + const proof2 = await partialSignature(signers[1]) + + multisig = assemble([proof, proof2]) + + await t.execution(core.append(b4a.from('0'), { signature: multisig })) + + t.is(core.length, 1) + + const core2 = new Hypercore(ram, { manifest }) + + const s1 = core.replicate(true) + const s2 = core2.replicate(false) + + const p = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + s1.pipe(s2).pipe(s1) + + await t.execution(p) + + t.is(core2.length, core.length) + + await core2.download({ start: 0, end: core.length }).downloaded() + + t.alike(await core2.get(0), b4a.from('0')) + + const batch = [] + for (let i = 1; i < 1000; i++) { + batch.push(b4a.from(i.toString(10))) + await signers[1].append(b4a.from(i.toString(10))) + } + + for (let i = 0; i < 10000; i++) { + await signers[0].append(b4a.from(i.toString(10))) + } + + const proof3 = await partialSignature(signers[0], signers[1].length) + const proof4 = await partialSignature(signers[1]) + + multisig = assemble([proof3, proof4]) + + const p2 = new Promise((resolve, reject) => { + s1.on('error', reject) + s2.on('error', reject) + + core2.on('append', resolve) + }) + + await t.execution(core.append(batch, { signature: multisig })) + + t.is(core.length, 1000) + + await t.execution(p2) + + t.is(core2.length, core.length) +}) + +function createMultiManifest (signers) { + return { + hash: 'blake2b', + multipleSigners: { + quorum: (signers.length >> 1) + 1, + allowPatched: true, + signers: signers.map(s => ({ + signature: 'ed25519', + namespace: caps.DEFAULT_NAMESPACE, + publicKey: s.manifest.signer.publicKey + })) + } + } +} diff --git a/test/preload.js b/test/preload.js index d976d935..4eea452c 100644 --- a/test/preload.js +++ b/test/preload.js @@ -53,10 +53,7 @@ test('preload - sign/storage', async function (t) { preload: () => { return { storage: RAM, - auth: { - sign: signable => crypto.sign(signable, keyPair.secretKey), - verify: (signable, signature) => crypto.verify(signable, signature, keyPair.publicKey) - } + keyPair } } }) diff --git a/test/replicate.js b/test/replicate.js index 8bd738c8..d97ee4e3 100644 --- a/test/replicate.js +++ b/test/replicate.js @@ -153,19 +153,18 @@ test('high latency reorg', async function (t) { test('invalid signature fails', async function (t) { t.plan(2) - const a = await create(null, { - auth: { - sign () { - return Buffer.alloc(64) - }, - verify (s, sig) { - return false - } - } - }) - + const a = await create(null) const b = await create(a.key) + a.core.verifier = { + sign () { + return Buffer.alloc(64) + }, + verify (s, sig) { + return false + } + } + await a.append(['a', 'b', 'c', 'd', 'e']) const [s1, s2] = replicate(a, b, t) @@ -190,6 +189,63 @@ test('invalid signature fails', async function (t) { }) }) +test('more invalid signatures fails', async function (t) { + const a = await create(null) + + await a.append(['a', 'b'], { signature: b4a.alloc(64) }) + + await t.test('replication fails after bad append', async function (sub) { + sub.plan(2) + + const b = await create(a.key) + const [s1, s2] = replicate(a, b, sub) + + s1.on('error', (err) => { + sub.ok(err, 'stream closed') + }) + + s2.on('error', (err) => { + sub.is(err.code, 'INVALID_SIGNATURE') + }) + + b.get(0).then(() => sub.fail('should not get block'), () => {}) + sub.teardown(() => b.close()) + }) + + await a.truncate(1, { signature: b4a.alloc(64) }) + + await t.test('replication fails after bad truncate', async function (sub) { + sub.plan(2) + + const b = await create(a.key) + const [s1, s2] = replicate(a, b, sub) + + s1.on('error', (err) => { + sub.ok(err, 'stream closed') + }) + + s2.on('error', (err) => { + sub.is(err.code, 'INVALID_SIGNATURE') + }) + + b.get(0).then(() => sub.fail('should not get block'), () => {}) + sub.teardown(() => b.close()) + }) + + await a.append('good') + + await t.test('replication works again', async function (sub) { + const b = await create(a.key) + replicate(a, b, sub) + + await new Promise(resolve => setImmediate(resolve)) + + sub.alike(await b.get(0), b4a.from('a'), 'got block') + + sub.teardown(() => b.close()) + }) +}) + test('invalid capability fails', async function (t) { t.plan(2) @@ -932,7 +988,7 @@ test('sparse replication without gossiping', async function (t) { test('force update writable cores', async function (t) { const a = await create() - const b = await create(a.key, { auth: a.auth }) + const b = await create(a.key, { header: a.core.header.manifest }) await a.append(['a', 'b', 'c', 'd', 'e']) diff --git a/test/sessions.js b/test/sessions.js index e8cf79fb..dabfae67 100644 --- a/test/sessions.js +++ b/test/sessions.js @@ -16,7 +16,7 @@ test('sessions - can create writable sessions from a read-only core', async func await core.ready() t.absent(core.writable) - const session = core.session({ keyPair: { secretKey: keyPair.secretKey } }) + const session = core.session({ keyPair }) await session.ready() t.ok(session.writable) @@ -37,42 +37,6 @@ test('sessions - can create writable sessions from a read-only core', async func t.is(core.length, 1) }) -test('sessions - writable session with custom sign function', async function (t) { - t.plan(5) - - const keyPair = crypto.keyPair() - const core = new Hypercore(RAM, keyPair.publicKey, { - valueEncoding: 'utf-8' - }) - await core.ready() - t.absent(core.writable) - - const session = core.session({ - auth: { - sign: signable => crypto.sign(signable, keyPair.secretKey), - verify: (signable, signature) => crypto.verify(signable, signature, keyPair.publicKey) - } - }) - - t.ok(session.writable) - - try { - await core.append('hello') - t.fail('should not have appended to the read-only core') - } catch { - t.pass('read-only core append threw correctly') - } - - try { - await session.append('world') - t.pass('session append did not throw') - } catch { - t.fail('session append should not have thrown') - } - - t.is(core.length, 1) -}) - test('sessions - auto close', async function (t) { const core = new Hypercore(RAM, { autoClose: true })