From 171e7472dc830b56b436b2e397025538aaed7520 Mon Sep 17 00:00:00 2001 From: haad Date: Fri, 19 Jan 2024 14:48:40 +0100 Subject: [PATCH 1/4] Add test utils --- test/.mocharc.json | 2 +- test/utils/connect-nodes.js | 31 ++++++++++++++++++++ test/utils/relay.js | 56 +++++++++++++++++++++++++++++++++++++ test/utils/wait-for.js | 12 ++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 test/utils/connect-nodes.js create mode 100644 test/utils/relay.js create mode 100644 test/utils/wait-for.js diff --git a/test/.mocharc.json b/test/.mocharc.json index e003c41..6609ab9 100644 --- a/test/.mocharc.json +++ b/test/.mocharc.json @@ -5,6 +5,6 @@ "exit": true, "bail": false, "slow": 1000, - "exclude": ["test/browser/**/*.js"], + "exclude": ["test/browser/**/*.js", "test/utils/**/*.js"], "timeout": 30000 } \ No newline at end of file diff --git a/test/utils/connect-nodes.js b/test/utils/connect-nodes.js new file mode 100644 index 0000000..82eff47 --- /dev/null +++ b/test/utils/connect-nodes.js @@ -0,0 +1,31 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { WebRTC } from '@multiformats/multiaddr-matcher' +import waitFor from './wait-for.js' + +const defaultFilter = () => true + +const isBrowser = () => typeof window !== 'undefined' + +const connectIpfsNodes = async (ipfs1, ipfs2, options = { + filter: defaultFilter +}) => { + if (isBrowser()) { + const relayId = '12D3KooWAJjbRkp8FPF5MKgMU53aUTxWkqvDrs4zc1VMbwRwfsbE' + + await ipfs1.libp2p.dial(multiaddr(`/ip4/127.0.0.1/tcp/12345/ws/p2p/${relayId}`)) + + let address1 + + await waitFor(() => { + address1 = ipfs1.libp2p.getMultiaddrs().filter(ma => WebRTC.matches(ma)).pop() + return address1 != null + }, () => true) + + await ipfs2.libp2p.dial(address1) + } else { + await ipfs2.libp2p.peerStore.save(ipfs1.libp2p.peerId, { multiaddrs: ipfs1.libp2p.getMultiaddrs().filter(options.filter) }) + await ipfs2.libp2p.dial(ipfs1.libp2p.peerId) + } +} + +export default connectIpfsNodes diff --git a/test/utils/relay.js b/test/utils/relay.js new file mode 100644 index 0000000..5ae1213 --- /dev/null +++ b/test/utils/relay.js @@ -0,0 +1,56 @@ +import { yamux } from '@chainsafe/libp2p-yamux' +import { createLibp2p } from 'libp2p' +import { noise } from '@chainsafe/libp2p-noise' +import { circuitRelayServer } from '@libp2p/circuit-relay-v2' +import { webSockets } from '@libp2p/websockets' +import * as filters from '@libp2p/websockets/filters' +import { identify } from '@libp2p/identify' +import { createFromPrivKey } from '@libp2p/peer-id-factory' +import { unmarshalPrivateKey } from '@libp2p/crypto/keys' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' + +// output of: console.log(server.peerId.privateKey.toString('hex')) +const relayPrivKey = '08011240821cb6bc3d4547fcccb513e82e4d718089f8a166b23ffcd4a436754b6b0774cf07447d1693cd10ce11ef950d7517bad6e9472b41a927cd17fc3fb23f8c70cd99' +// the peer id of the above key +// const relayId = '12D3KooWAJjbRkp8FPF5MKgMU53aUTxWkqvDrs4zc1VMbwRwfsbE' + +const encoded = uint8ArrayFromString(relayPrivKey, 'hex') +const privateKey = await unmarshalPrivateKey(encoded) +const peerId = await createFromPrivKey(privateKey) + +const server = await createLibp2p({ + peerId, + addresses: { + listen: ['/ip4/0.0.0.0/tcp/12345/ws'] + }, + transports: [ + webSockets({ + filter: filters.all + }) + ], + connectionEncryption: [noise()], + streamMuxers: [yamux()], + services: { + identify: identify(), + relay: circuitRelayServer({ + reservations: { + maxReservations: 5000, + reservationTtl: 1000, + defaultDataLimit: BigInt(1024 * 1024 * 1024) + } + }) + } +}) + +server.addEventListener('peer:connect', async event => { + console.log('peer:connect', event.detail) +}) + +server.addEventListener('peer:disconnect', async event => { + console.log('peer:disconnect', event.detail) + server.peerStore.delete(event.detail) +}) + +console.log(server.peerId.toString()) +console.log('p2p addr: ', server.getMultiaddrs().map((ma) => ma.toString())) +// generates a deterministic address: /ip4/127.0.0.1/tcp/33519/ws/p2p/12D3KooWAJjbRkp8FPF5MKgMU53aUTxWkqvDrs4zc1VMbwRwfsbE diff --git a/test/utils/wait-for.js b/test/utils/wait-for.js new file mode 100644 index 0000000..15d4beb --- /dev/null +++ b/test/utils/wait-for.js @@ -0,0 +1,12 @@ +const waitFor = async (valueA, toBeValueB, pollInterval = 100) => { + return new Promise((resolve) => { + const interval = setInterval(async () => { + if (await valueA() === await toBeValueB()) { + clearInterval(interval) + resolve() + } + }, pollInterval) + }) +} + +export default waitFor From c0561fe4545afd9e69448a3b5cba842d85b88063 Mon Sep 17 00:00:00 2001 From: haad Date: Fri, 19 Jan 2024 14:49:00 +0100 Subject: [PATCH 2/4] Add webrtc relay script --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 477d11d..a9453f4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "build:tests": "rm -f test/browser/bundle.js* && webpack --config ./conf/webpack.tests.config.js", "prepublishOnly": "npm run build", "lint": "standard --env=mocha", - "lint:fix": "standard --fix" + "lint:fix": "standard --fix", + "webrtc": "node ./test/utils/relay.js", + "webrtc:background": "node ./test/utils/relay.js &" }, "repository": { "type": "git", @@ -39,7 +41,7 @@ "ignore": [ "test/browser/**" ] - }, + }, "devDependencies": { "cross-env": "^7.0.3", "mocha": "^10.2.0", From 0ec9cf65f939914c966c8b4ba33e0172db4b1301 Mon Sep 17 00:00:00 2001 From: haad Date: Fri, 19 Jan 2024 14:49:43 +0100 Subject: [PATCH 3/4] Renaming, close blockstore explicitly, expose isBrowser function --- src/index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index 5ed63ae..197be6d 100644 --- a/src/index.js +++ b/src/index.js @@ -24,22 +24,25 @@ const startOrbitDB = async ({ id, identity, identities, directory } = {}) => { directory = directory || '.' const blockstore = new LevelBlockstore(`${directory}/ipfs/blocks`) const ipfs = await createHelia({ libp2p, blockstore, blockBrokers: [bitswap()] }) - return createOrbitDB({ ipfs, id, identity, identities, directory }) + const orbitdb = await createOrbitDB({ ipfs, id, identity, identities, directory }) + return orbitdb } /** * Stops the OrbitDB peer and associated services. * @function stopOrbitDB - * @param {Object} instance The instance of OrbitDB to stop. + * @param {Object} orbitdb The OrbitDB instance to stop. */ -const stopOrbitDB = async (instance) => { - await instance.stop() - await instance.ipfs.stop() +const stopOrbitDB = async (orbitdb) => { + await orbitdb.stop() + await orbitdb.ipfs.stop() + await orbitdb.ipfs.blockstore.unwrap().unwrap().close() } export { startOrbitDB, stopOrbitDB, DefaultLibp2pOptions, - DefaultLibp2pBrowserOptions + DefaultLibp2pBrowserOptions, + isBrowser } From 69a8149e201b2daafd6af241c28756e63378e1a2 Mon Sep 17 00:00:00 2001 From: haad Date: Fri, 19 Jan 2024 14:50:01 +0100 Subject: [PATCH 4/4] Add multiple nodes and replication tests --- test/quickstart.test.js | 117 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/test/quickstart.test.js b/test/quickstart.test.js index ae324d0..86f6f76 100644 --- a/test/quickstart.test.js +++ b/test/quickstart.test.js @@ -1,6 +1,8 @@ -import { startOrbitDB, stopOrbitDB } from '../src/index.js' -import { deepStrictEqual } from 'assert' +import { startOrbitDB, stopOrbitDB, isBrowser } from '../src/index.js' +import { strictEqual, notStrictEqual, deepStrictEqual } from 'assert' import { rimraf } from 'rimraf' +import connectIpfsNodes from './utils/connect-nodes.js' +import waitFor from './utils/wait-for.js' describe('Starting OrbitDB', function () { it('starts OrbitDB with a preconfigured Helia instance', async () => { @@ -15,3 +17,114 @@ describe('Starting OrbitDB', function () { await rimraf('./ipfs') }) }) + +describe('Multiple nodes', function () { + it('starts and connects two OrbitDB instances', async () => { + const orbitdb1 = await startOrbitDB({ directory: './orbitdb1' }) + const orbitdb2 = await startOrbitDB({ directory: './orbitdb2' }) + await connectIpfsNodes(orbitdb1.ipfs, orbitdb2.ipfs) + await stopOrbitDB(orbitdb1) + await stopOrbitDB(orbitdb2) + await rimraf('./orbitdb1') + await rimraf('./orbitdb2') + }) + + it('throws an error if trying to start two OrbitDB instances with same directory', async () => { + // Skip this test in browser tests + if (isBrowser()) { + return + } + + let err + + const orbitdb1 = await startOrbitDB({ directory: './orbitdb' }) + + try { + await startOrbitDB({ directory: './orbitdb' }) + } catch (e) { + err = e + } + + notStrictEqual(err, undefined) + strictEqual(err.message, 'Database is not open') + + await stopOrbitDB(orbitdb1) + await rimraf('./orbitdb') + }) + + it('replicates a database between two OrbitDB instances', async () => { + let replicated = false + + const orbitdb1 = await startOrbitDB({ directory: './orbitdb1' }) + const orbitdb2 = await startOrbitDB({ directory: './orbitdb2' }) + + await connectIpfsNodes(orbitdb1.ipfs, orbitdb2.ipfs) + + const db1 = await orbitdb1.open('db2') + + const onJoin = () => (replicated = true) + const db2 = await orbitdb2.open(db1.address) + db2.events.on('join', onJoin) + + await db1.add('A') + await db1.add('B') + await db1.add('C') + + await waitFor(() => replicated, () => true) + + deepStrictEqual((await db1.all()).map(e => e.value), ['A', 'B', 'C']) + deepStrictEqual((await db2.all()).map(e => e.value), ['A', 'B', 'C']) + + await stopOrbitDB(orbitdb1) + await stopOrbitDB(orbitdb2) + await rimraf('./orbitdb1') + await rimraf('./orbitdb2') + }) + + it.only('replicates a database between four OrbitDB instances', async () => { + let replicated1 = false + let replicated2 = false + + const orbitdb1 = await startOrbitDB({ directory: './orbitdb1' }) + const orbitdb2 = await startOrbitDB({ directory: './orbitdb2' }) + const orbitdb3 = await startOrbitDB({ directory: './orbitdb3' }) + const orbitdb4 = await startOrbitDB({ directory: './orbitdb4' }) + + await connectIpfsNodes(orbitdb1.ipfs, orbitdb2.ipfs) + await connectIpfsNodes(orbitdb2.ipfs, orbitdb3.ipfs) + await connectIpfsNodes(orbitdb3.ipfs, orbitdb4.ipfs) + + const db1 = await orbitdb1.open('db2') + const db2 = await orbitdb2.open(db1.address) + + await db1.add('A') + await db1.add('B') + await db1.add('C') + + const db3 = await orbitdb3.open(db1.address) + const onJoin1 = () => (replicated1 = true) + db3.events.on('join', onJoin1) + + await waitFor(() => replicated1, () => true) + + const db4 = await orbitdb4.open(db1.address) + const onJoin2 = () => (replicated2 = true) + db4.events.on('join', onJoin2) + + await waitFor(() => replicated2, () => true) + + deepStrictEqual((await db1.all()).map(e => e.value), ['A', 'B', 'C']) + deepStrictEqual((await db2.all()).map(e => e.value), ['A', 'B', 'C']) + deepStrictEqual((await db3.all()).map(e => e.value), ['A', 'B', 'C']) + deepStrictEqual((await db4.all()).map(e => e.value), ['A', 'B', 'C']) + + await stopOrbitDB(orbitdb1) + await stopOrbitDB(orbitdb2) + await stopOrbitDB(orbitdb3) + await stopOrbitDB(orbitdb4) + await rimraf('./orbitdb1') + await rimraf('./orbitdb2') + await rimraf('./orbitdb3') + await rimraf('./orbitdb4') + }) +})