Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make sure local peer's address in in invitation link #2268

Merged
merged 6 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/e2e-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jobs:
with:
timeout_minutes: 25
max_attempts: 3
shell: bash
command: cd packages/e2e-tests && npm run test userProfile.test.ts

- name: Run invitation link test - Includes 2 separate application clients
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[unreleased]

# Fixes:

* Make sure address of the inviting peer is in the invitation link

[2.1.1]

# Fixes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ describe('ConnectionsManagerService', () => {
})

it('launches community on init if its data exists in local db', async () => {
const remotePeer = createLibp2pAddress(
'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
)
const launchCommunityPayload: InitCommunityPayload = {
id: community.id,
peerId: userIdentity.peerId,
Expand All @@ -102,12 +106,7 @@ describe('ConnectionsManagerService', () => {
key: userIdentity.userCsr?.userKey,
CA: [communityRootCa],
},
peers: [
createLibp2pAddress(
'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
),
],
peers: [remotePeer],
}

await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload)
Expand All @@ -116,7 +115,11 @@ describe('ConnectionsManagerService', () => {
const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue()

await connectionsManagerService.init()
expect(launchCommunitySpy).toHaveBeenCalledWith(launchCommunityPayload)

const localPeerAddress = createLibp2pAddress(userIdentity.hiddenService.onionAddress, userIdentity.peerId.id)
const updatedLaunchCommunityPayload = { ...launchCommunityPayload, peers: [localPeerAddress, remotePeer] }

expect(launchCommunitySpy).toHaveBeenCalledWith(updatedLaunchCommunityPayload)
})

it('does not launch community on init if its data does not exist in local db', async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/nest/libp2p/libp2p.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { mplex } from '@libp2p/mplex'
import { multiaddr } from '@multiformats/multiaddr'
import { Inject, Injectable } from '@nestjs/common'
import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common'
import { ConnectionProcessInfo, PeerId, SocketActionTypes } from '@quiet/types'
import { ConnectionProcessInfo, type NetworkDataPayload, PeerId, SocketActionTypes } from '@quiet/types'
import crypto from 'crypto'
import { EventEmitter } from 'events'
import { Agent } from 'https'
Expand Down Expand Up @@ -188,12 +188,12 @@ export class Libp2pService extends EventEmitter {

this.connectedPeers.delete(remotePeerId)
this.logger(`${localPeerId} is connected to ${this.connectedPeers.size} peers`)

this.emit(Libp2pEvents.PEER_DISCONNECTED, {
const peerStat: NetworkDataPayload = {
peer: remotePeerId,
connectionDuration,
lastSeen: connectionEndTime,
})
}
this.emit(Libp2pEvents.PEER_DISCONNECTED, peerStat)
})

await this.processInChunksService.process()
Expand Down
15 changes: 11 additions & 4 deletions packages/backend/src/nest/local-db/local-db.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Inject, Injectable } from '@nestjs/common'
import { Level } from 'level'
import { NetworkStats } from '@quiet/types'
import { filterAndSortPeers } from '@quiet/common'
import { InitCommunityPayload, NetworkStats } from '@quiet/types'
import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common'
import { LEVEL_DB } from '../const'
import { LocalDBKeys, LocalDbStatus } from './local-db.types'
import Logger from '../common/logger'
import { create } from 'mock-fs/lib/filesystem'

@Injectable()
export class LocalDbService {
Expand Down Expand Up @@ -35,7 +36,7 @@ export class LocalDbService {
try {
data = await this.db.get(key)
} catch (e) {
this.logger.error(`Getting '${key}'`, e)
this.logger(`Getting '${key}'`, e)
return null
}
return data
Expand Down Expand Up @@ -74,7 +75,13 @@ export class LocalDbService {
public async getSortedPeers(peers: string[] = []): Promise<string[]> {
const peersStats = (await this.get(LocalDBKeys.PEERS)) || {}
const stats: NetworkStats[] = Object.values(peersStats)
return filterAndSortPeers(peers, stats)
const community: InitCommunityPayload = await this.get(LocalDBKeys.COMMUNITY)
if (!community) {
return filterAndSortPeers(peers, stats)
}
const localPeerAddress = createLibp2pAddress(community.hiddenService.onionAddress, community.peerId.id)
this.logger('Local peer', localPeerAddress)
return filterAndSortPeers(peers, stats, localPeerAddress)
}

public async putOwnerOrbitDbIdentity(id: string): Promise<void> {
Expand Down
14 changes: 13 additions & 1 deletion packages/common/src/libp2p.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { filterAndSortPeers } from './sortPeers'

describe('filterValidAddresses', () => {
it('filters out invalid addresses', () => {
const localAddress =
'/dns4/f3lupwnhaqplbn4djaut5rtipwmlotlb57flfvjzgexek2yezlpjddid.onion/tcp/443/ws/p2p/Qmd35TsAvtskei8zWY3A65ifNWcY4x4SdqkQDHMkH5xPF9'
const valid = [
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/80/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
Expand All @@ -15,6 +17,16 @@ describe('filterValidAddresses', () => {
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrj.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbK',
]
expect(filterAndSortPeers(addresses, [])).toEqual(valid)
expect(filterAndSortPeers(addresses, [], localAddress)).toEqual([localAddress, ...valid])
})

it('sets local address as first without duplicating it', () => {
const localAddress =
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/80/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
const addresses = [
localAddress,
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
]
expect(filterAndSortPeers(addresses, [], localAddress)).toEqual([localAddress, addresses[1]])
})
})
17 changes: 13 additions & 4 deletions packages/common/src/sortPeers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ This is the very simple algorithm for evaluating the most wanted peers.
2. Two sorted arrays are created - one sorted by last seen and other by most uptime shared.
3. Arrays are merged taking one element from list one and one element from the second list. Duplicates are ommited
4. We end up with mix of last seen and most uptime descending array of peers, the it is enchanced to libp2p address.
5. Prioritize local peer if given
*/
export const filterAndSortPeers = (peersAddresses: string[], stats: NetworkStats[]): string[] => {
export const filterAndSortPeers = (
peersAddresses: string[],
stats: NetworkStats[],
localPeerAddress?: string
): string[] => {
peersAddresses = filterValidAddresses(peersAddresses)
const lastSeenSorted = [...stats].sort((a, b) => {
return b.lastSeen - a.lastSeen
Expand Down Expand Up @@ -44,8 +49,12 @@ export const filterAndSortPeers = (peersAddresses: string[], stats: NetworkStats
})
})

return peerList
.concat(peersAddresses)
.filter(address => address !== null)
return [
...new Set([
localPeerAddress, // Set local peer as first
...peerList.concat(peersAddresses),
]),
]
.filter(address => address !== null && address !== '')
.filter(isDefined)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { FC, useState } from 'react'
import { useSelector } from 'react-redux'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'
import { InviteComponent } from './Invite.component'

export const Invite: FC = () => {
const invitationLink = useSelector(communities.selectors.invitationUrl)
const invitationLink = useSelector(connection.selectors.invitationUrl)

const [revealInputValue, setRevealInputValue] = useState<boolean>(false)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'
import { useSelector } from 'react-redux'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'

import { QRCodeComponent } from './QRCode.component'
import { Site } from '@quiet/common'

export const QRCode: React.FC = () => {
const invitationLink = useSelector(communities.selectors.invitationUrl) || Site.MAIN_PAGE
const invitationLink = useSelector(connection.selectors.invitationUrl) || Site.MAIN_PAGE
return <QRCodeComponent value={invitationLink} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Share } from 'react-native'

import Clipboard from '@react-native-clipboard/clipboard'

import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'

import { navigationSelectors } from '../../../store/navigation/navigation.selectors'

Expand All @@ -21,7 +21,7 @@ export const InvitationContextMenu: FC = () => {
const dispatch = useDispatch()

const screen = useSelector(navigationSelectors.currentScreen)
const invitationLink = useSelector(communities.selectors.invitationUrl)
const invitationLink = useSelector(connection.selectors.invitationUrl)

const invitationContextMenu = useContextMenu(MenuName.Invitation)

Expand Down
4 changes: 2 additions & 2 deletions packages/mobile/src/screens/QRCode/QRCode.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FC, useCallback, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import Share from 'react-native-share'
import SVG from 'react-native-svg'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'
import { navigationActions } from '../../store/navigation/navigation.slice'
import { ScreenNames } from '../../const/ScreenNames.enum'

Expand All @@ -14,7 +14,7 @@ export const QRCodeScreen: FC = () => {

const svgRef = useRef<SVG>()

const invitationLink = useSelector(communities.selectors.invitationUrl) || Site.MAIN_PAGE
const invitationLink = useSelector(connection.selectors.invitationUrl) || Site.MAIN_PAGE

const handleBackButton = useCallback(() => {
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { type Store } from '@reduxjs/toolkit'
import { getFactory } from '../../utils/tests/factories'
import { prepareStore } from '../../utils/tests/prepareStore'
import { connectionSelectors } from './connection.selectors'
import { type communitiesActions } from '../communities/communities.slice'
import { communitiesActions } from '../communities/communities.slice'
import { connectionActions } from './connection.slice'
import { type FactoryGirl } from 'factory-girl'
import { type Community } from '@quiet/types'
import { createLibp2pAddress, invitationShareUrl } from '@quiet/common'

describe('communitiesSelectors', () => {
setupCrypto()
Expand All @@ -31,7 +32,7 @@ describe('communitiesSelectors', () => {
],
})

// This peer should be first in the list as it is the most revently seen one.
// This peer should be first in the list as it is the most recently seen one.
store.dispatch(
connectionActions.updateNetworkData({
peer: 'Qmd35TsAvtskei8zWY3A65ifNWcY4x4SdqkQDHMkH5xPF9',
Expand Down Expand Up @@ -91,4 +92,47 @@ describe('communitiesSelectors', () => {

expect(socketIOSecret2).toEqual(secret)
})

it('invitationUrl selector does not break if there is no community', () => {
const { store } = prepareStore()
const invitationUrl = connectionSelectors.invitationUrl(store.getState())
expect(invitationUrl).toEqual('')
})

it('invitationUrl selector returns proper url', async () => {
const { store } = prepareStore()
const factory = await getFactory(store)
const peerList = [
createLibp2pAddress(
'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'
),
]
const psk = '12345'
const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity'
await factory.create<ReturnType<typeof communitiesActions.addNewCommunity>['payload']>('Community', {
peerList,
ownerOrbitDbIdentity,
})
store.dispatch(communitiesActions.savePSK(psk))
const selectorInvitationUrl = connectionSelectors.invitationUrl(store.getState())
const expectedUrl = invitationShareUrl(peerList, psk, ownerOrbitDbIdentity)
expect(expectedUrl).not.toEqual('')
expect(selectorInvitationUrl).toEqual(expectedUrl)
})

it('invitationUrl selector returns empty string if state lacks psk', async () => {
const { store } = prepareStore()
const factory = await getFactory(store)
await factory.create<ReturnType<typeof communitiesActions.addNewCommunity>['payload']>('Community', {
peerList: [
createLibp2pAddress(
'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'
),
],
})
const invitationUrl = connectionSelectors.invitationUrl(store.getState())
expect(invitationUrl).toEqual('')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { StoreKeys } from '../store.keys'
import { createSelector } from 'reselect'
import { type CreatedSelectors, type StoreState } from '../store.types'
import { allUsers, areCertificatesLoaded } from '../users/users.selectors'
import { communitiesSelectors } from '../communities/communities.selectors'
import { peersStatsAdapter } from './connection.adapter'
import { connectedPeers, isCurrentCommunityInitialized } from '../network/network.selectors'
import { type NetworkStats } from './connection.types'
import { type User } from '../users/users.types'
import { filterAndSortPeers } from '@quiet/common'
import { filterAndSortPeers, invitationShareUrl } from '@quiet/common'
import { areMessagesLoaded, areChannelsLoaded } from '../publicChannels/publicChannels.selectors'
import { identitySelectors } from '../identity/identity.selectors'
import { communitiesSelectors } from '../communities/communities.selectors'

const connectionSlice: CreatedSelectors[StoreKeys.Connection] = (state: StoreState) => state[StoreKeys.Connection]

Expand All @@ -22,21 +23,38 @@ export const connectionProcess = createSelector(connectionSlice, reducerState =>

export const socketIOSecret = createSelector(connectionSlice, reducerState => reducerState.socketIOSecret)

const peerStats = createSelector(connectionSlice, reducerState => {
let stats: NetworkStats[]
if (reducerState.peersStats === undefined) {
stats = []
} else {
stats = peersStatsAdapter.getSelectors().selectAll(reducerState.peersStats)
}
return stats
})

export const peerList = createSelector(
connectionSlice,
communitiesSelectors.currentCommunity,
(reducerState, community) => {
identitySelectors.currentPeerAddress,
peerStats,
(community, localPeerAddress, stats) => {
if (!community) return []
const arr = [...(community.peerList || [])]

let stats: NetworkStats[]
if (reducerState.peersStats === undefined) {
stats = []
} else {
stats = peersStatsAdapter.getSelectors().selectAll(reducerState.peersStats)
}
const arr = [...(community.peerList || [])]
return filterAndSortPeers(arr, stats, localPeerAddress)
}
)

return filterAndSortPeers(arr, stats)
export const invitationUrl = createSelector(
communitiesSelectors.psk,
communitiesSelectors.ownerOrbitDbIdentity,
peerList,
(communityPsk, ownerOrbitDbIdentity, sortedPeerList) => {
if (!sortedPeerList || sortedPeerList?.length === 0) return ''
if (!communityPsk) return ''
if (!ownerOrbitDbIdentity) return ''
const initialPeers = sortedPeerList.slice(0, 3)
return invitationShareUrl(initialPeers, communityPsk, ownerOrbitDbIdentity)
}
)

Expand Down Expand Up @@ -70,6 +88,7 @@ export const connectionSelectors = {
lastConnectedTime,
connectedPeersMapping,
peerList,
invitationUrl,
torBootstrapProcess,
connectionProcess,
isTorInitialized,
Expand Down
Loading
Loading