diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index c99418a589..400fc8d809 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -29,18 +29,27 @@ "test:firefox": "aegir test -t browser -- --browser firefox", "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", "test:node": "aegir test -t node --cov", + "test:interop": "aegir test -t node -f dist/test/interop.js", "dep-check": "aegir dep-check" }, "devDependencies": { + "@chainsafe/libp2p-gossipsub": "^11.0.0", "@chainsafe/libp2p-noise": "^14.0.0", "@chainsafe/libp2p-yamux": "^6.0.1", "@libp2p/circuit-relay-v2": "^1.0.0", + "@libp2p/crypto": "^2.0.8", + "@libp2p/daemon-client": "^8.0.1", + "@libp2p/daemon-server": "^7.0.1", "@libp2p/dcutr": "^1.0.0", "@libp2p/fetch": "^1.0.0", + "@libp2p/floodsub": "^9.0.0", "@libp2p/identify": "^1.0.0", "@libp2p/interface": "^1.0.0", "@libp2p/interface-compliance-tests": "^5.0.0", "@libp2p/interface-internal": "^1.0.0", + "@libp2p/interop": "^10.0.0", + "@libp2p/kad-dht": "^11.0.5", + "@libp2p/logger": "^4.0.1", "@libp2p/mplex": "^10.0.0", "@libp2p/peer-id": "^4.0.0", "@libp2p/peer-id-factory": "^3.0.8", @@ -53,6 +62,8 @@ "@multiformats/multiaddr": "^12.1.10", "aegir": "^41.0.2", "delay": "^6.0.0", + "execa": "^8.0.1", + "go-libp2p": "^1.1.1", "it-all": "^3.0.3", "it-pipe": "^3.0.1", "libp2p": "^1.0.0", diff --git a/packages/integration-tests/test/interop.ts b/packages/integration-tests/test/interop.ts new file mode 100644 index 0000000000..ce2802eee7 --- /dev/null +++ b/packages/integration-tests/test/interop.ts @@ -0,0 +1,229 @@ +import fs from 'fs' +import { gossipsub } from '@chainsafe/libp2p-gossipsub' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2' +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { createClient } from '@libp2p/daemon-client' +import { createServer } from '@libp2p/daemon-server' +import { floodsub } from '@libp2p/floodsub' +import { identify } from '@libp2p/identify' +import { contentRoutingSymbol, peerDiscoverySymbol, peerRoutingSymbol } from '@libp2p/interface' +import { interopTests } from '@libp2p/interop' +import { kadDHT } from '@libp2p/kad-dht' +import { logger } from '@libp2p/logger' +import { mplex } from '@libp2p/mplex' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { tcp } from '@libp2p/tcp' +import { multiaddr } from '@multiformats/multiaddr' +import { execa } from 'execa' +import { path as p2pd } from 'go-libp2p' +import { createLibp2p, type Libp2pOptions, type ServiceFactoryMap } from 'libp2p' +import pDefer from 'p-defer' +import type { ServiceMap, PeerId } from '@libp2p/interface' +import type { SpawnOptions, Daemon, DaemonFactory } from '@libp2p/interop' + +/** + * @packageDocumentation + * + * To enable debug logging, run the tests with the following env vars: + * + * ```console + * DEBUG=libp2p*,go-libp2p:* npm run test:interop + * ``` + */ + +async function createGoPeer (options: SpawnOptions): Promise { + const controlPort = Math.floor(Math.random() * (50000 - 10000 + 1)) + 10000 + const apiAddr = multiaddr(`/ip4/127.0.0.1/tcp/${controlPort}`) + + const log = logger(`go-libp2p:${controlPort}`) + + const opts = [ + `-listen=${apiAddr.toString()}` + ] + + if (options.noListen === true) { + opts.push('-noListenAddrs') + } else { + opts.push('-hostAddrs=/ip4/127.0.0.1/tcp/0') + } + + if (options.noise === true) { + opts.push('-noise=true') + } + + if (options.dht === true) { + opts.push('-dhtServer') + } + + if (options.relay === true) { + opts.push('-relay') + } + + if (options.pubsub === true) { + opts.push('-pubsub') + } + + if (options.pubsubRouter != null) { + opts.push(`-pubsubRouter=${options.pubsubRouter}`) + } + + if (options.key != null) { + opts.push(`-id=${options.key}`) + } + + if (options.muxer === 'mplex') { + opts.push('-muxer=mplex') + } else { + opts.push('-muxer=yamux') + } + + const deferred = pDefer() + const proc = execa(p2pd(), opts, { + env: { + GOLOG_LOG_LEVEL: 'debug' + } + }) + + proc.stdout?.on('data', (buf: Buffer) => { + const str = buf.toString() + log(str) + + // daemon has started + if (str.includes('Control socket:')) { + deferred.resolve() + } + }) + + proc.stderr?.on('data', (buf) => { + log.error(buf.toString()) + }) + + await deferred.promise + + return { + client: createClient(apiAddr), + stop: async () => { + proc.kill() + } + } +} + +async function createJsPeer (options: SpawnOptions): Promise { + let peerId: PeerId | undefined + + if (options.key != null) { + const keyFile = fs.readFileSync(options.key) + const privateKey = await unmarshalPrivateKey(keyFile) + peerId = await peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) + } + + const opts: Libp2pOptions = { + peerId, + addresses: { + listen: options.noListen === true ? [] : ['/ip4/127.0.0.1/tcp/0'] + }, + transports: [tcp(), circuitRelayTransport()], + streamMuxers: [], + connectionEncryption: [noise()], + connectionManager: { + minConnections: 0 + } + } + + const services: ServiceFactoryMap = { + identify: identify() + } + + if (options.muxer === 'mplex') { + opts.streamMuxers?.push(mplex()) + } else { + opts.streamMuxers?.push(yamux()) + } + + if (options.pubsub === true) { + if (options.pubsubRouter === 'floodsub') { + services.pubsub = floodsub() + } else { + services.pubsub = gossipsub() + } + } + + if (options.relay === true) { + services.relay = circuitRelayServer() + } + + if (options.dht === true) { + services.dht = (components: any) => { + const dht: any = kadDHT({ + clientMode: false + })(components) + + // go-libp2p-daemon only has the older single-table DHT instead of the dual + // lan/wan version found in recent go-ipfs versions. unfortunately it's been + // abandoned so here we simulate the older config with the js implementation + const lan: any = dht.lan + + const protocol = '/ipfs/kad/1.0.0' + lan.protocol = protocol + lan.network.protocol = protocol + lan.topologyListener.protocol = protocol + + Object.defineProperties(lan, { + [contentRoutingSymbol]: { + get () { + return dht[contentRoutingSymbol] + } + }, + [peerRoutingSymbol]: { + get () { + return dht[peerRoutingSymbol] + } + }, + [peerDiscoverySymbol]: { + get () { + return dht[peerDiscoverySymbol] + } + } + }) + + return lan + } + } + + const node: any = await createLibp2p({ + ...opts, + services + }) + + const server = createServer(multiaddr('/ip4/0.0.0.0/tcp/0'), node) + await server.start() + + return { + client: createClient(server.getMultiaddr()), + stop: async () => { + await server.stop() + await node.stop() + } + } +} + +async function main (): Promise { + const factory: DaemonFactory = { + async spawn (options: SpawnOptions) { + if (options.type === 'go') { + return createGoPeer(options) + } + + return createJsPeer(options) + } + } + + await interopTests(factory) +} + +main().catch(err => { + console.error(err) // eslint-disable-line no-console + process.exit(1) +})