Skip to content

Commit

Permalink
Merge pull request #634 from airgap-it/feat/add-new-server-regions
Browse files Browse the repository at this point in the history
feat(p2p): add new server regions
  • Loading branch information
AndreasGassmann authored Nov 20, 2023
2 parents 5f0b170 + 475a0dd commit 472489e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 73 deletions.
26 changes: 13 additions & 13 deletions examples/dapp.html
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -51,7 +53,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 {
Expand All @@ -60,6 +66,10 @@ interface BeaconInfoResponse {
timestamp: number
}

const sleep = (time: number) => {
return new Promise((resolve) => setTimeout(resolve, time))
}

/**
* @internalapi
*/
Expand Down Expand Up @@ -143,18 +153,32 @@ export class P2PCommunicationClient extends CommunicationClient {
return info
}

public async findBestRegion(): Promise<Regions> {
/**
* 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<boolean>()

keys.forEach((key) => {
const nodes = this.ENABLED_RELAY_SERVERS[key] ?? []
Expand All @@ -163,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 }> {
Expand Down Expand Up @@ -223,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<BeaconInfoResponse> {
Expand Down Expand Up @@ -635,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)
Expand Down

0 comments on commit 472489e

Please sign in to comment.