From 38b3b3487236d19cbd14f43186e14bdf244470e8 Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Wed, 25 Oct 2023 15:06:43 +0200 Subject: [PATCH 1/4] feat(p2p): add new server regions --- examples/dapp.html | 26 ++++++++-------- .../P2PCommunicationClient.ts | 31 +++++++++++-------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/examples/dapp.html b/examples/dapp.html index 9fa78f786..d77fa0dfe 100644 --- a/examples/dapp.html +++ b/examples/dapp.html @@ -191,19 +191,19 @@ projectId: '97f804b46f0db632c52af0556586a5f3', relayUrl: 'wss://relay.walletconnect.com' }, - matrixNodes: { - [beacon.Regions.EUROPE_WEST]: [ - 'beacon-node-1.diamond.papers.tech', - 'beacon-node-1.sky.papers.tech', - 'beacon-node-2.sky.papers.tech', - 'beacon-node-1.hope.papers.tech', - 'beacon-node-1.hope-2.papers.tech', - 'beacon-node-1.hope-3.papers.tech', - 'beacon-node-1.hope-4.papers.tech', - 'beacon-node-1.hope-5.papers.tech' - ], - [beacon.Regions.NORTH_AMERICA_EAST]: [] - }, + // matrixNodes: { + // [beacon.Regions.EUROPE_WEST]: [ + // 'beacon-node-1.diamond.papers.tech', + // 'beacon-node-1.sky.papers.tech', + // 'beacon-node-2.sky.papers.tech', + // 'beacon-node-1.hope.papers.tech', + // 'beacon-node-1.hope-2.papers.tech', + // 'beacon-node-1.hope-3.papers.tech', + // 'beacon-node-1.hope-4.papers.tech', + // 'beacon-node-1.hope-5.papers.tech' + // ], + // [beacon.Regions.NORTH_AMERICA_EAST]: [] + // }, preferredNetwork: beacon.NetworkType.GHOSTNET, featuredWallets: ['airgap', 'metamask'] // network: { diff --git a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts index de2ec08d1..228376b6b 100644 --- a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts +++ b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts @@ -51,7 +51,11 @@ const REGIONS_AND_SERVERS: NodeDistributions = { 'beacon-node-1.hope-3.papers.tech', 'beacon-node-1.hope-4.papers.tech', 'beacon-node-1.hope-5.papers.tech' - ] + ], + [Regions.NORTH_AMERICA_EAST]: ['beacon-node-1.beacon-server-1.papers.tech'], + [Regions.NORTH_AMERICA_WEST]: ['beacon-node-1.beacon-server-2.papers.tech'], + [Regions.ASIA_EAST]: ['beacon-node-1.beacon-server-3.papers.tech'], + [Regions.AUSTRALIA]: ['beacon-node-1.beacon-server-4.papers.tech'] } interface BeaconInfoResponse { @@ -634,9 +638,12 @@ export class P2PCommunicationClient extends CommunicationClient { logger.log(`Waiting for join... Try: ${retry}`) return new Promise((resolve) => { - setTimeout(async () => { - resolve(this.waitForJoin(roomId, retry + 1)) - }, 100 * (retry > 50 ? 10 : 1)) // After the initial 5 seconds, retry only once per second + setTimeout( + async () => { + resolve(this.waitForJoin(roomId, retry + 1)) + }, + 100 * (retry > 50 ? 10 : 1) + ) // After the initial 5 seconds, retry only once per second }) } else { throw new Error(`No one joined after ${retry} tries.`) @@ -710,17 +717,15 @@ export class P2PCommunicationClient extends CommunicationClient { ? new PeerManager(this.storage, StorageKey.TRANSPORT_P2P_PEERS_DAPP) : new PeerManager(this.storage, StorageKey.TRANSPORT_P2P_PEERS_WALLET) const peers = await manager.getPeers() - const promiseArray = peers.map( - async (peer) => { - const hash = `@${await getHexHash(Buffer.from(peer.publicKey, 'hex'))}` - if (hash === senderHash) { - if (peer.relayServer !== relayServer) { - peer.relayServer = relayServer - await manager.addPeer(peer as ExtendedP2PPairingResponse) - } + const promiseArray = peers.map(async (peer) => { + const hash = `@${await getHexHash(Buffer.from(peer.publicKey, 'hex'))}` + if (hash === senderHash) { + if (peer.relayServer !== relayServer) { + peer.relayServer = relayServer + await manager.addPeer(peer as ExtendedP2PPairingResponse) } } - ) + }) await Promise.all(promiseArray) } From fa883ad8521a2728095a5eb08e141f97c06e28bf Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Wed, 25 Oct 2023 15:51:28 +0200 Subject: [PATCH 2/4] fix(p2p): tests --- .../beacon-transport-matrix/__tests__/tests/p2p-client.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-transport-matrix/__tests__/tests/p2p-client.spec.ts b/packages/beacon-transport-matrix/__tests__/tests/p2p-client.spec.ts index 4c91c9abf..9e3143529 100644 --- a/packages/beacon-transport-matrix/__tests__/tests/p2p-client.spec.ts +++ b/packages/beacon-transport-matrix/__tests__/tests/p2p-client.spec.ts @@ -25,7 +25,7 @@ describe(`P2PCommunicationClient`, () => { }) it(`should have more than 1 node per region available`, async () => { - const keyValue: [string, string][] = Object.values((client as any).ENABLED_RELAY_SERVERS) + const keyValue: [string, string][] = Object.entries((client as any).ENABLED_RELAY_SERVERS) expect(keyValue.length >= 1).to.be.true keyValue.forEach((kv) => { expect(kv[1].length >= 1).to.be.true From 9f050cd774dcb27c3a0cb61ef0b0f6e496dd13e3 Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Mon, 20 Nov 2023 08:33:27 +0100 Subject: [PATCH 3/4] feat: improve region selection --- .../P2PCommunicationClient.ts | 147 +++++++++++------- 1 file changed, 90 insertions(+), 57 deletions(-) diff --git a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts index 228376b6b..89ce11450 100644 --- a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts +++ b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts @@ -41,6 +41,8 @@ import { encode } from '@stablelib/utf8' const logger = new Logger('P2PCommunicationClient') +const RESPONSE_WAIT_TIME_MS: number = 1000 + const REGIONS_AND_SERVERS: NodeDistributions = { [Regions.EUROPE_WEST]: [ 'beacon-node-1.diamond.papers.tech', @@ -64,6 +66,10 @@ interface BeaconInfoResponse { timestamp: number } +const sleep = (time: number) => { + return new Promise((resolve) => setTimeout(resolve, time)) +} + /** * @internalapi */ @@ -147,18 +153,32 @@ export class P2PCommunicationClient extends CommunicationClient { return info } - public async findBestRegion(): Promise { + /** + * To get the fastest region, we can't simply do one request, because sometimes, + * DNS and SSL handshakes make "faster" connections slower. So we need to do 2 requests + * and check which request was the fastest after 1s. + */ + public async findBestRegionAndGetServer(): Promise< + { server: string; timestamp: number } | undefined + > { + // Select random server from each region + // Start request to server from each region + // After first response, do request again (this is because the first request can be delayed by DNS/SSL/etc.) + // After a specified amount of time, we select the fastest response time + if (this.selectedRegion) { - return this.selectedRegion + return this.relayServer?.promiseResult } const keys: Regions[] = Object.keys(this.ENABLED_RELAY_SERVERS) as any - const allPromises: Promise<{ - region: Regions - server: string - response: BeaconInfoResponse - }>[] = [] + const results: { time: number; server: string; region: Regions; result: BeaconInfoResponse }[] = + [] + + const allResponsesReceived = new ExposedPromise() + let expectedNumberOfResponses = 0 + + const timeoutPromise = new ExposedPromise() keys.forEach((key) => { const nodes = this.ENABLED_RELAY_SERVERS[key] ?? [] @@ -167,33 +187,62 @@ export class P2PCommunicationClient extends CommunicationClient { return } - const index = Math.floor(Math.random() * nodes.length) - allPromises.push( - this.getBeaconInfo(nodes[index]) - .then((res) => ({ + expectedNumberOfResponses += 2 + + const doRequest = (isFinalRequest: boolean = true) => { + const timeStart = Date.now() + Promise.race([this.getBeaconInfo(server), timeoutPromise.promise]).then((res) => { + if (typeof res === 'boolean') { + return + } + results.push({ + time: Date.now() - timeStart, + server: server, region: key, - server: nodes[index], - response: res - })) - .catch( - (err) => - new Promise((_resolve, reject) => { - // This workaround is done because Promise.race stops at the first failure, but we need the first success. - // TODO: If all promises have been rejected, let's not wait 2000 and abort earlier. - setTimeout(() => reject(err), 2000) - }) - ) - ) + result: res + }) + + // If we have received all expected responses, we can continue and don't need to wait anymore + if (results.length >= expectedNumberOfResponses) { + allResponsesReceived.resolve(undefined) + } + + if (!isFinalRequest) { + doRequest(true) + } + }) + } + + const index = Math.floor(Math.random() * nodes.length) + const server = nodes[index] + doRequest(false) }) - const region = await Promise.race(allPromises) - this.selectedRegion = region.region + // Sleep for a specified amount of time to let responses come in + await Promise.race([allResponsesReceived.promise, sleep(RESPONSE_WAIT_TIME_MS)]) + + let retryCount: number = 0 + while (results.length <= 0) { + // If we have no results yet, we will wait until we get one + if (retryCount >= 100) { + // If we do not have any server response after 5s, throw error + throw new Error('No server responded.') + } + await sleep(50) + retryCount++ + } + + // We have a result after the maximum amount of time, resolve the promise to abort all pending requests + timeoutPromise.resolve(true) + + // Select fastest response time + const lowestTimeEntry = results.reduce((prev, curr) => { + return prev.time < curr.time ? prev : curr + }) - return region.region + this.selectedRegion = lowestTimeEntry.region - // Select random server from each region. - // Start request to random server from each region - // Fastest response wins, region is selected + return { server: lowestTimeEntry.server, timestamp: lowestTimeEntry.result.timestamp } } public async getRelayServer(): Promise<{ server: string; timestamp: number }> { @@ -227,39 +276,23 @@ export class P2PCommunicationClient extends CommunicationClient { return { server: node, timestamp: info.timestamp } } - const region = await this.findBestRegion() + const server = await this.findBestRegionAndGetServer() - const regionNodes = this.ENABLED_RELAY_SERVERS[region] - if (!regionNodes) { - throw new Error(`No servers found for region ${region}`) + if (!server) { + throw new Error(`No servers found`) } - const nodes = [...regionNodes] + this.storage + .set(StorageKey.MATRIX_SELECTED_NODE, server.server) + .catch((error) => logger.log(error)) - while (nodes.length > 0) { - const index = Math.floor(Math.random() * nodes.length) - const server = nodes[index] - - try { - const response = await this.getBeaconInfo(server) - this.storage - .set(StorageKey.MATRIX_SELECTED_NODE, server) - .catch((error) => logger.log(error)) - - this.relayServer.resolve({ - server, - timestamp: response.timestamp, - localTimestamp: new Date().getTime() - }) - return { server, timestamp: response.timestamp } - } catch (relayError) { - logger.log(`Ignoring server "${server}", trying another one...`) - nodes.splice(index, 1) - } - } + this.relayServer.resolve({ + server: server.server, + timestamp: server.timestamp, + localTimestamp: new Date().getTime() + }) - this.relayServer.reject(`No matrix server reachable!`) - throw new Error(`No matrix server reachable!`) + return { server: server.server, timestamp: server.timestamp } } public async getBeaconInfo(server: string): Promise { From 4e079655d3d99b1ebfc4ed651d375ba6ce926182 Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Mon, 20 Nov 2023 16:13:42 +0100 Subject: [PATCH 4/4] chore: remove unused async --- .../src/communication-client/P2PCommunicationClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts index 89ce11450..032359db9 100644 --- a/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts +++ b/packages/beacon-transport-matrix/src/communication-client/P2PCommunicationClient.ts @@ -672,7 +672,7 @@ export class P2PCommunicationClient extends CommunicationClient { return new Promise((resolve) => { setTimeout( - async () => { + () => { resolve(this.waitForJoin(roomId, retry + 1)) }, 100 * (retry > 50 ? 10 : 1)