From 312381cd9d31500651a6c35043460ef3bce0fd67 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 25 May 2023 16:45:02 +0100 Subject: [PATCH] test: add tests for different key types, where possible (#52) Expands the tests to test using `secp256k1` and `RSA` keys as well as `Ed25519` where possible. Kubo doesn't support generating or importing `secp256k1` keys so those tests are skipped, as are `RSA` keys for pubsub resolution since the DHT needs to be enabled to resolve the public key component. --- packages/interop/test/dht.spec.ts | 271 +++++++++++--------- packages/interop/test/fixtures/key-types.ts | 7 + packages/interop/test/pubsub.spec.ts | 263 +++++++++---------- 3 files changed, 284 insertions(+), 257 deletions(-) create mode 100644 packages/interop/test/fixtures/key-types.ts diff --git a/packages/interop/test/dht.spec.ts b/packages/interop/test/dht.spec.ts index 638d15c..5dbff01 100644 --- a/packages/interop/test/dht.spec.ts +++ b/packages/interop/test/dht.spec.ts @@ -3,7 +3,7 @@ import { ipns } from '@helia/ipns' import { dht } from '@helia/ipns/routing' import { type KadDHT, kadDHT } from '@libp2p/kad-dht' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import { ipnsSelector } from 'ipns/selector' import { ipnsValidator } from 'ipns/validator' @@ -19,6 +19,7 @@ import { connect } from './fixtures/connect.js' import { createHeliaNode } from './fixtures/create-helia.js' import { createKuboNode } from './fixtures/create-kubo.js' import { sortClosestPeers } from './fixtures/create-peer-ids.js' +import { keyTypes } from './fixtures/key-types.js' import { waitFor } from './fixtures/wait-for.js' import type { Helia } from '@helia/interface' import type { IPNS } from '@helia/ipns' @@ -26,163 +27,177 @@ import type { Libp2p } from '@libp2p/interface-libp2p' import type { Controller } from 'ipfsd-ctl' import type { PeerId } from 'kubo-rpc-client/dist/src/types.js' -describe('dht routing', () => { - let helia: Helia> - let kubo: Controller - let name: IPNS - - // the CID we are going to publish - let value: CID - - // the public key we will use to publish the value - let key: PeerId - - /** - * Ensure that for the CID we are going to publish, the resolver has a peer ID that - * is KAD-closer to the routing key so we can predict the the resolver will receive - * the DHT record containing the IPNS record - */ - async function createNodes (resolver: 'kubo' | 'helia'): Promise { - const input = Uint8Array.from([0, 1, 2, 3, 4]) - const digest = await sha256.digest(input) - value = CID.createV1(raw.code, digest) - - helia = await createHeliaNode({ - services: { - identify: identifyService(), - dht: kadDHT({ - validators: { - ipns: ipnsValidator - }, - selectors: { - ipns: ipnsSelector - }, - // skips waiting for the initial self-query to find peers - allowQueryWithZeroPeers: true - }) - } - }) - kubo = await createKuboNode() - - // find a PeerId that is KAD-closer to the resolver than the publisher when used as an IPNS key - while (true) { - key = await createEd25519PeerId() - const routingKey = uint8ArrayConcat([ - uint8ArrayFromString('/ipns/'), - key.toBytes() - ]) +keyTypes.forEach(type => { + describe(`dht routing with ${type} keys`, () => { + let helia: Helia> + let kubo: Controller + let name: IPNS + + // the CID we are going to publish + let value: CID + + // the public key we will use to publish the value + let key: PeerId + + /** + * Ensure that for the CID we are going to publish, the resolver has a peer ID that + * is KAD-closer to the routing key so we can predict the the resolver will receive + * the DHT record containing the IPNS record + */ + async function createNodes (resolver: 'kubo' | 'helia'): Promise { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const digest = await sha256.digest(input) + value = CID.createV1(raw.code, digest) + + helia = await createHeliaNode({ + services: { + identify: identifyService(), + dht: kadDHT({ + validators: { + ipns: ipnsValidator + }, + selectors: { + ipns: ipnsSelector + }, + // skips waiting for the initial self-query to find peers + allowQueryWithZeroPeers: true + }) + } + }) + kubo = await createKuboNode() + + // find a PeerId that is KAD-closer to the resolver than the publisher when used as an IPNS key + while (true) { + if (type === 'Ed25519') { + key = await createEd25519PeerId() + } else if (type === 'secp256k1') { + key = await createSecp256k1PeerId() + } else { + key = await createRSAPeerId() + } - const [closest] = await sortClosestPeers(routingKey, [ - helia.libp2p.peerId, - kubo.peer.id - ]) + const routingKey = uint8ArrayConcat([ + uint8ArrayFromString('/ipns/'), + key.toBytes() + ]) - if (resolver === 'kubo' && closest.equals(kubo.peer.id)) { - break - } + const [closest] = await sortClosestPeers(routingKey, [ + helia.libp2p.peerId, + kubo.peer.id + ]) + + if (resolver === 'kubo' && closest.equals(kubo.peer.id)) { + break + } - if (resolver === 'helia' && closest.equals(helia.libp2p.peerId)) { - break + if (resolver === 'helia' && closest.equals(helia.libp2p.peerId)) { + break + } } - } - // connect the two nodes over the KAD-DHT protocol, this should ensure - // both nodes have each other in their KAD buckets - await connect(helia, kubo, '/ipfs/lan/kad/1.0.0') + // connect the two nodes over the KAD-DHT protocol, this should ensure + // both nodes have each other in their KAD buckets + await connect(helia, kubo, '/ipfs/lan/kad/1.0.0') - await waitFor(async () => { - let found = false + await waitFor(async () => { + let found = false - for await (const event of helia.libp2p.services.dht.findPeer(kubo.peer.id)) { - if (event.name === 'FINAL_PEER') { - found = true + for await (const event of helia.libp2p.services.dht.findPeer(kubo.peer.id)) { + if (event.name === 'FINAL_PEER') { + found = true + } } - } - return found - }, { - timeout: 30000, - delay: 1000, - message: 'Helia could not find Kubo on the DHT' - }) + return found + }, { + timeout: 30000, + delay: 1000, + message: 'Helia could not find Kubo on the DHT' + }) - await waitFor(async () => { - let found = false + await waitFor(async () => { + let found = false - for await (const event of kubo.api.dht.findPeer(helia.libp2p.peerId)) { - if (event.name === 'FINAL_PEER') { - found = true + for await (const event of kubo.api.dht.findPeer(helia.libp2p.peerId)) { + if (event.name === 'FINAL_PEER') { + found = true + } } + + return found + }, { + timeout: 30000, + delay: 1000, + message: 'Kubo could not find Helia on the DHT' + }) + + name = ipns(helia, [ + dht(helia) + ]) + } + + afterEach(async () => { + if (helia != null) { + await helia.stop() } - return found - }, { - timeout: 30000, - delay: 1000, - message: 'Kubo could not find Helia on the DHT' + if (kubo != null) { + await kubo.stop() + } }) - name = ipns(helia, [ - dht(helia) - ]) - } - - afterEach(async () => { - if (helia != null) { - await helia.stop() - } + it(`should publish on helia and resolve on kubo using a ${type} key`, async () => { + await createNodes('kubo') - if (kubo != null) { - await kubo.stop() - } - }) + const keyName = 'my-ipns-key' + await helia.libp2p.keychain.importPeer(keyName, key) - it('should publish on helia and resolve on kubo', async () => { - await createNodes('kubo') + await name.publish(key, value) - const keyName = 'my-ipns-key' - await helia.libp2p.keychain.importPeer(keyName, key) + const resolved = await last(kubo.api.name.resolve(key)) - await name.publish(key, value) + if (resolved == null) { + throw new Error('kubo failed to resolve name') + } - const resolved = await last(kubo.api.name.resolve(key)) + expect(resolved).to.equal(`/ipfs/${value.toString()}`) + }) - if (resolved == null) { - throw new Error('kubo failed to resolve name') - } + it('should publish on kubo and resolve on helia', async function () { + if (isElectronMain) { + // electron main does not have fetch, FormData or Blob APIs + // can revisit when kubo-rpc-client supports the key.import API + return this.skip() + } - expect(resolved).to.equal(`/ipfs/${value.toString()}`) - }) + if (type === 'secp256k1') { + // Kubo cannot import secp256k1 keys + return this.skip() + } - it('should publish on kubo and resolve on helia', async function () { - if (isElectronMain) { - // electron main does not have fetch, FormData or Blob APIs - // can revisit when kubo-rpc-client supports the key.import API - return this.skip() - } + await createNodes('helia') - await createNodes('helia') + const keyName = 'my-ipns-key' + const { cid } = await kubo.api.add(Uint8Array.from([0, 1, 2, 3, 4])) - const keyName = 'my-ipns-key' - const { cid } = await kubo.api.add(Uint8Array.from([0, 1, 2, 3, 4])) + // ensure the key is in the kubo keychain so we can use it to publish the IPNS record + const body = new FormData() + body.append('key', new Blob([key.privateKey ?? new Uint8Array(0)])) - // ensure the key is in the kubo keychain so we can use it to publish the IPNS record - const body = new FormData() - body.append('key', new Blob([key.privateKey ?? new Uint8Array(0)])) + // can't use the kubo-rpc-api for this call yet + const response = await fetch(`http://${kubo.api.apiHost}:${kubo.api.apiPort}/api/v0/key/import?arg=${keyName}`, { + method: 'POST', + body + }) - // can't use the kubo-rpc-api for this call yet - const response = await fetch(`http://${kubo.api.apiHost}:${kubo.api.apiPort}/api/v0/key/import?arg=${keyName}`, { - method: 'POST', - body - }) + expect(response).to.have.property('status', 200) - expect(response).to.have.property('status', 200) + await kubo.api.name.publish(cid, { + key: keyName + }) - await kubo.api.name.publish(cid, { - key: keyName + const resolvedCid = await name.resolve(key) + expect(resolvedCid.toString()).to.equal(cid.toString()) }) - - const resolvedCid = await name.resolve(key) - expect(resolvedCid.toString()).to.equal(cid.toString()) }) }) diff --git a/packages/interop/test/fixtures/key-types.ts b/packages/interop/test/fixtures/key-types.ts new file mode 100644 index 0000000..78b60e3 --- /dev/null +++ b/packages/interop/test/fixtures/key-types.ts @@ -0,0 +1,7 @@ +import type { PeerIdType } from '@libp2p/interface-peer-id' + +export const keyTypes: PeerIdType[] = [ + 'Ed25519', + 'secp256k1', + 'RSA' +] diff --git a/packages/interop/test/pubsub.spec.ts b/packages/interop/test/pubsub.spec.ts index 5e31573..9180d35 100644 --- a/packages/interop/test/pubsub.spec.ts +++ b/packages/interop/test/pubsub.spec.ts @@ -1,4 +1,5 @@ /* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 5] */ import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { ipns } from '@helia/ipns' @@ -18,6 +19,7 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { connect } from './fixtures/connect.js' import { createHeliaNode } from './fixtures/create-helia.js' import { createKuboNode } from './fixtures/create-kubo.js' +import { keyTypes } from './fixtures/key-types.js' import { waitFor } from './fixtures/wait-for.js' import type { Helia } from '@helia/interface' import type { IPNS } from '@helia/ipns' @@ -27,156 +29,159 @@ import type { Libp2p } from 'libp2p' const LIBP2P_KEY_CODEC = 0x72 -describe('pubsub routing', () => { - let helia: Helia> - let kubo: Controller - let name: IPNS - - beforeEach(async () => { - helia = await createHeliaNode({ - services: { - identify: identifyService(), - pubsub: gossipsub() - } - }) - kubo = await createKuboNode({ - ipfsOptions: { - config: { - Routing: { - Type: 'none' - } +// skip RSA tests because we need the DHT enabled to find the public key +// component of the keypair, but that means we can't test pubsub +// resolution because Kubo will use the DHT as well +keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => { + describe(`pubsub routing with ${keyType} keys`, () => { + let helia: Helia> + let kubo: Controller + let name: IPNS + + beforeEach(async () => { + helia = await createHeliaNode({ + services: { + identify: identifyService(), + pubsub: gossipsub() } - }, - args: ['--enable-pubsub-experiment', '--enable-namesys-pubsub'] - }) - - // connect the two nodes - await connect(helia, kubo, '/meshsub/1.1.0') - - name = ipns(helia, [ - pubsub(helia) - ]) - }) + }) + kubo = await createKuboNode({ + args: ['--enable-pubsub-experiment', '--enable-namesys-pubsub'] + }) - afterEach(async () => { - if (helia != null) { - await helia.stop() - } + // connect the two nodes + await connect(helia, kubo, '/meshsub/1.1.0') - if (kubo != null) { - await kubo.stop() - } - }) - - it('should publish on helia and resolve on kubo', async () => { - const input = Uint8Array.from([0, 1, 2, 3, 4]) - const digest = await sha256.digest(input) - const cid = CID.createV1(raw.code, digest) - - const keyName = 'my-ipns-key' - await helia.libp2p.keychain.createKey(keyName, 'Ed25519') - const peerId = await helia.libp2p.keychain.exportPeerId(keyName) - - if (peerId.publicKey == null) { - throw new Error('No public key present') - } + name = ipns(helia, [ + pubsub(helia) + ]) + }) - // first publish should fail because kubo isn't subscribed to key update channel - await expect(name.publish(peerId, cid)).to.eventually.be.rejected() - .with.property('message', 'PublishError.InsufficientPeers') + afterEach(async () => { + if (helia != null) { + await helia.stop() + } - // should fail to resolve the first time as kubo was not subscribed to the pubsub channel - await expect(last(kubo.api.name.resolve(peerId, { - timeout: 100 - }))).to.eventually.be.undefined() + if (kubo != null) { + await kubo.stop() + } + }) - // magic pubsub subscription name - const subscriptionName = `/ipns/${CID.createV1(LIBP2P_KEY_CODEC, identity.digest(peerId.publicKey)).toString(base36)}` + it('should publish on helia and resolve on kubo', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const digest = await sha256.digest(input) + const cid = CID.createV1(raw.code, digest) - // wait for kubo to be subscribed to updates - await waitFor(async () => { - const subs = await kubo.api.name.pubsub.subs() + const keyName = 'my-ipns-key' + await helia.libp2p.keychain.createKey(keyName, keyType) + const peerId = await helia.libp2p.keychain.exportPeerId(keyName) - return subs.includes(subscriptionName) - }, { - timeout: 30000 - }) + if (peerId.publicKey == null) { + throw new Error('No public key present') + } - // publish should now succeed - await name.publish(peerId, cid) + // first publish should fail because kubo isn't subscribed to key update channel + await expect(name.publish(peerId, cid)).to.eventually.be.rejected() + .with.property('message', 'PublishError.InsufficientPeers') - // kubo should now be able to resolve IPNS name - const resolved = await last(kubo.api.name.resolve(peerId, { - timeout: 100 - })) + // should fail to resolve the first time as kubo was not subscribed to the pubsub channel + await expect(last(kubo.api.name.resolve(peerId, { + timeout: 100 + }))).to.eventually.be.undefined() - expect(resolved).to.equal(`/ipfs/${cid.toString()}`) - }) + // magic pubsub subscription name + const subscriptionName = `/ipns/${CID.createV1(LIBP2P_KEY_CODEC, identity.digest(peerId.publicKey)).toString(base36)}` - it('should publish on kubo and resolve on helia', async () => { - const keyName = 'my-ipns-key' - const { cid } = await kubo.api.add(Uint8Array.from([0, 1, 2, 3, 4])) - const result = await kubo.api.key.gen(keyName, { - // @ts-expect-error kubo needs this in lower case - type: 'ed25519' - }) + // wait for kubo to be subscribed to updates + await waitFor(async () => { + const subs = await kubo.api.name.pubsub.subs() - // the generated id is libp2p-key CID with the public key as an identity multihash - const peerCid = CID.parse(result.id, base36) - const peerId = await peerIdFromKeys(peerCid.multihash.digest) - - // first call to pubsub resolver should fail but we should now be subscribed for updates - await expect(name.resolve(peerId)).to.eventually.be.rejected() - - // actual pubsub subscription name - const subscriptionName = `/record/${uint8ArrayToString(uint8ArrayConcat([ - uint8ArrayFromString('/ipns/'), - peerId.toBytes() - ]), 'base64url')}` - - // wait for helia to be subscribed to the topic for record updates - await waitFor(async () => { - return helia.libp2p.services.pubsub.getTopics().includes(subscriptionName) - }, { - timeout: 30000, - message: 'Helia did not register for record updates' - }) + return subs.includes(subscriptionName) + }, { + timeout: 30000 + }) - // wait for kubo to see that helia is subscribed to the topic for record updates - await waitFor(async () => { - const peers = await kubo.api.pubsub.peers(subscriptionName) + // publish should now succeed + await name.publish(peerId, cid) - return peers.map(p => p.toString()).includes(helia.libp2p.peerId.toString()) - }, { - timeout: 30000, - message: 'Kubo did not see that Helia was registered for record updates' - }) + // kubo should now be able to resolve IPNS name + const resolved = await last(kubo.api.name.resolve(peerId, { + timeout: 100 + })) - // now publish, this should cause a pubsub message on the topic for record updates - await kubo.api.name.publish(cid, { - key: keyName + expect(resolved).to.equal(`/ipfs/${cid.toString()}`) }) - let resolvedCid: CID | undefined + it('should publish on kubo and resolve on helia', async function () { + if (keyType === 'secp256k1') { + // Kubo cannot generate secp256k1 keys + return this.skip() + } - // we should get an update eventually - await waitFor(async () => { - try { - resolvedCid = await name.resolve(peerId) + const keyName = 'my-ipns-key' + const { cid } = await kubo.api.add(Uint8Array.from([0, 1, 2, 3, 4])) + const result = await kubo.api.key.gen(keyName, { + // @ts-expect-error kubo needs this in lower case + type: keyType.toLowerCase() + }) + + // the generated id is libp2p-key CID with the public key as an identity multihash + const peerCid = CID.parse(result.id, base36) + const peerId = await peerIdFromKeys(peerCid.multihash.digest) + + // first call to pubsub resolver should fail but we should now be subscribed for updates + await expect(name.resolve(peerId)).to.eventually.be.rejected() + + // actual pubsub subscription name + const subscriptionName = `/record/${uint8ArrayToString(uint8ArrayConcat([ + uint8ArrayFromString('/ipns/'), + peerId.toBytes() + ]), 'base64url')}` + + // wait for helia to be subscribed to the topic for record updates + await waitFor(async () => { + return helia.libp2p.services.pubsub.getTopics().includes(subscriptionName) + }, { + timeout: 30000, + message: 'Helia did not register for record updates' + }) + + // wait for kubo to see that helia is subscribed to the topic for record updates + await waitFor(async () => { + const peers = await kubo.api.pubsub.peers(subscriptionName) + + return peers.map(p => p.toString()).includes(helia.libp2p.peerId.toString()) + }, { + timeout: 30000, + message: 'Kubo did not see that Helia was registered for record updates' + }) + + // now publish, this should cause a pubsub message on the topic for record updates + await kubo.api.name.publish(cid, { + key: keyName + }) + + let resolvedCid: CID | undefined + + // we should get an update eventually + await waitFor(async () => { + try { + resolvedCid = await name.resolve(peerId) + + return true + } catch { + return false + } + }, { + timeout: 10000, + message: 'Helia could not resolve the IPNS record' + }) - return true - } catch { - return false + if (resolvedCid == null) { + throw new Error('Failed to resolve CID') } - }, { - timeout: 10000, - message: 'Helia could not resolve the IPNS record' - }) - - if (resolvedCid == null) { - throw new Error('Failed to resolve CID') - } - expect(resolvedCid.toString()).to.equal(cid.toString()) + expect(resolvedCid.toString()).to.equal(cid.toString()) + }) }) })