diff --git a/src/p2p/Join/index.ts b/src/p2p/Join/index.ts index 6e96091e9..4d97ba978 100644 --- a/src/p2p/Join/index.ts +++ b/src/p2p/Join/index.ts @@ -2,19 +2,18 @@ import deepmerge from 'deepmerge' import { version } from '../../../package.json' import * as http from '../../http' import { logFlags } from '../../logger' -import { hexstring, P2P } from '@shardus/types' +import { P2P } from '@shardus/types' import * as utils from '../../utils' -import { validateTypes, isEqualOrNewerVersion } from '../../utils' +import { validateTypes } from '../../utils' import * as Comms from '../Comms' -import { config, crypto, logger, network, shardus } from '../Context' +import { config, crypto, network, shardus } from '../Context' import * as CycleChain from '../CycleChain' import * as CycleCreator from '../CycleCreator' import * as NodeList from '../NodeList' import * as Self from '../Self' -import { getOurNodeIndex, robustQuery } from '../Utils' -import { isBogonIP, isInvalidIP, isIPv6 } from '../../utils/functions/checkIP' +import { robustQuery } from '../Utils' +import { isBogonIP, isInvalidIP } from '../../utils/functions/checkIP' import { nestedCountersInstance } from '../../utils/nestedCounters' -import { Logger } from 'log4js' import { calculateToAcceptV2 } from '../ModeSystemFuncs' import { routes } from './routes' import { @@ -29,8 +28,6 @@ import { err, ok, Result } from 'neverthrow' import { drainSelectedPublicKeys, forceSelectSelf } from './v2/select' import { deleteStandbyNode, drainNewUnjoinRequests, processNewUnjoinRequest } from './v2/unjoin' import { JoinRequest } from '@shardus/types/build/src/p2p/JoinTypes' -import { updateNodeState } from '../Self' -import { HTTPError } from 'got' import { drainLostAfterSelectionNodes, drainSyncStarted, @@ -38,20 +35,17 @@ import { addSyncStarted, } from './v2/syncStarted' import { addFinishedSyncing, drainFinishedSyncingRequest, newSyncFinishedNodes } from './v2/syncFinished' -//import { getLastCycleStandbyRefreshRequest, resetLastCycleStandbyRefreshRequests, drainNewStandbyRefreshRequests } from './v2/standbyRefresh' import { drainNewStandbyRefreshRequests, addStandbyRefresh } from './v2/standbyRefresh' -import rfdc from 'rfdc' import { Utils } from '@shardus/types' import { neverGoActive } from '../Active' +import { validateJoinRequest } from './validate' +import { error, info, initLogging, warn } from './logging' +import { JoinRequestResponse } from './types' +import { getAllowBogon, getSeen, resetSeen, setAllowBogon } from './state' /** STATE */ -let p2pLogger: Logger -let mainLogger: Logger -const clone = rfdc() - let requests: P2P.JoinTypes.JoinRequest[] -let seen: Set let queuedReceivedJoinRequests: JoinRequest[] = [] let queuedJoinRequestsForGossip: JoinRequest[] = [] let queuedStartedSyncingId: string @@ -66,14 +60,6 @@ let queuedUnjoinRequestsForThisCycle: P2P.JoinTypes.SignedUnjoinRequest[] = [] let lastLoggedCycle = 0 -let allowBogon = false -export function setAllowBogon(value: boolean): void { - allowBogon = value -} -export function getAllowBogon(): boolean { - return allowBogon -} - let mode = null export let finishedSyncingCycle = -1 @@ -88,8 +74,7 @@ export let finishedSyncingCycle = -1 /** CycleCreator Functions */ export function init(): void { - p2pLogger = logger.getLogger('p2p') - mainLogger = logger.getLogger('main') + initLogging() // Init state reset() @@ -104,7 +89,7 @@ export function init(): void { export function reset(): void { requests = [] - seen = new Set() + resetSeen() //keepInStandbyCollector = new Map() } @@ -506,10 +491,16 @@ export function updateRecord(txs: P2P.JoinTypes.Txs, record: P2P.CycleCreatorTyp if (nodeIfSelectedLastCycle) { record.apoptosized.push(nodeIfSelectedLastCycle.id) - nestedCountersInstance.countEvent('p2p', `node that requested to unjoin but was selected to go active was added to apoptosized`) + nestedCountersInstance.countEvent( + 'p2p', + `node that requested to unjoin but was selected to go active was added to apoptosized` + ) } else if (nodeIfSelectedThisCycle) { record.apoptosized.push(nodeIfSelectedThisCycle.id) - nestedCountersInstance.countEvent('p2p', `node that requested to unjoin but was selected to go active was added to apoptosized`) + nestedCountersInstance.countEvent( + 'p2p', + `node that requested to unjoin but was selected to go active was added to apoptosized` + ) } else { record.standbyRemove.push(signedUnjoinRequest.publicKey) } @@ -668,7 +659,10 @@ export function sendRequests(): void { true ) } else { - nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to add our own sync-started message`) + nestedCountersInstance.countEvent( + 'p2p', + `join:sendRequests: failed to add our own sync-started message` + ) /* prettier-ignore */ if (logFlags.p2pNonFatal) console.log(`join:sendRequests: failed to add our own sync-started message`) } } @@ -713,7 +707,10 @@ export function sendRequests(): void { const standbyRefreshResult = addStandbyRefresh(standbyRefreshTx) if (standbyRefreshResult.success === true) { - nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending standby-refresh gossip to network`) + nestedCountersInstance.countEvent( + 'p2p', + `join:sendRequests: sending standby-refresh gossip to network` + ) /* prettier-ignore */ if (logFlags.p2pNonFatal) console.log(`join:sendRequests: sending standby-refresh gossip to network`) Comms.sendGossip( 'gossip-standby-refresh', @@ -728,10 +725,7 @@ export function sendRequests(): void { true ) } else { - nestedCountersInstance.countEvent( - 'p2p', - `join:sendRequests: failed to add standby-refresh message` - ) + nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to add standby-refresh message`) /* prettier-ignore */ if (logFlags.p2pNonFatal) console.log(`join:sendRequests: failed to add standby-refresh message`) } } @@ -743,7 +737,7 @@ export function sendRequests(): void { // TODO: may need to check if node is on standby and maybe validate the request again // need to think about this more - // The point of having two arrays for joinReq gossip was that it was the simplest way I could think at the time to avoid a race conditon. + // The point of having two arrays for joinReq gossip was that it was the simplest way I could think at the time to avoid a race conditon. // however, I think its over-engineered. I think its even simpler to let a node that validated before sendRequests to just send it, // and next cycle, the other nodes should check if the node is on the standby list before sending it. This will speed up creating a network @@ -757,14 +751,14 @@ export function sendRequests(): void { joinRequest.selectionNum = selectionNumResult.value // its possible that we have already seen this join request via gossip before we send it - if (seen.has(joinRequest.nodeInfo.publicKey) === false) { + if (getSeen().has(joinRequest.nodeInfo.publicKey) === false) { // since join request was already validated last cycle, we can just set seen to true directly - seen.add(joinRequest.nodeInfo.publicKey) + getSeen().add(joinRequest.nodeInfo.publicKey) saveJoinRequest(joinRequest) } const signedObjectWithJoinRequest = crypto.sign({ joinRequest, sign: null }) - + Comms.sendGossip( 'gossip-valid-join-requests', signedObjectWithJoinRequest, @@ -777,7 +771,10 @@ export function sendRequests(): void { ]), true ) - nestedCountersInstance.countEvent('p2p', `join:sendRequests: saved join request and gossiped to network`) + nestedCountersInstance.countEvent( + 'p2p', + `join:sendRequests: saved join request and gossiped to network` + ) /* prettier-ignore */ if (logFlags.p2pNonFatal) console.log(`join:sendRequests: saved join request and gossiped to network`) } queuedJoinRequestsForGossip = [] @@ -793,9 +790,12 @@ export function sendRequests(): void { const processResult = processNewUnjoinRequest(unjoinRequest) if (processResult.isErr()) { - nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to process unjoin request; failed to process unjoin request`) + nestedCountersInstance.countEvent( + 'p2p', + `join:sendRequests: failed to process unjoin request; failed to process unjoin request` + ) /* prettier-ignore */ if (logFlags.p2pNonFatal) console.error(`join:sendRequests: will not gossip to network; failed to process unjoin request for node ${unjoinRequest.publicKey}:`, JSON.stringify(processResult.error)) - return + return } nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending unjoin gossip to network`) @@ -882,17 +882,6 @@ export async function createJoinRequest( return signedJoinReq } -export interface JoinRequestResponse { - /** Whether the join request was accepted. TODO: consider renaming to `accepted`? */ - success: boolean - - /** A message explaining the result of the join request. */ - reason: string - - /** Whether the join request could not be accepted due to some error, usually in validating a join request. TODO: consider renaming to `invalid`? */ - fatal: boolean -} - /** * Processes a join request by validating the joining node's information, * ensuring compatibility with the network's version, checking cryptographic signatures, and responding @@ -1044,14 +1033,14 @@ export async function submitJoinV2( // Check if network allows bogon IPs, set our own flag accordingly if (config.p2p.dynamicBogonFiltering && config.p2p.forceBogonFilteringOn === false) { if (nodes.some((node) => isBogonIP(node.ip))) { - allowBogon = true + setAllowBogon(true) } } - nestedCountersInstance.countEvent('p2p', `join-allow-bogon-submit:${allowBogon}`) + nestedCountersInstance.countEvent('p2p', `join-allow-bogon-submit:${getAllowBogon()}`) //Check for bad IPs before a join request is sent out if (config.p2p.rejectBogonOutboundJoin || config.p2p.forceBogonFilteringOn) { - if (allowBogon === false) { + if (getAllowBogon() === false) { if (isBogonIP(joinRequest.nodeInfo.externalIp)) { throw new Error(`Fatal: Node cannot join with bogon external IP: ${joinRequest.nodeInfo.externalIp}`) } @@ -1189,60 +1178,6 @@ export async function fetchJoinedV2( } } -/** - * Returns a `JoinRequestResponse` object if the given `joinRequest` is invalid or rejected for any reason. - */ -export function validateJoinRequest(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { - // perform validation. if any of these functions return a non-null value, - // validation fails and the join request is rejected - return ( - verifyJoinRequestTypes(joinRequest) || - validateVersion(joinRequest.version) || - verifyJoinRequestSigner(joinRequest) || - verifyNotIPv6(joinRequest) || - validateJoinRequestHost(joinRequest) || - verifyUnseen(joinRequest.nodeInfo.publicKey) || - verifyNodeUnknown(joinRequest.nodeInfo) || - validateJoinRequestTimestamp(joinRequest.nodeInfo.joinRequestTimestamp) - ) -} - -/** - * Returns an error response if the given `joinRequest` is invalid or rejected - * based on its IP address. - */ -function validateJoinRequestHost(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { - try { - //test or bogon IPs and reject the join request if they appear - if (allowBogon === false) { - if (isBogonIP(joinRequest.nodeInfo.externalIp)) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from Bogon IP') - nestedCountersInstance.countEvent('p2p', `join-reject-bogon`) - return { - success: false, - reason: `Bad ip, bogon ip not accepted`, - fatal: true, - } - } - } else { - //even if not checking bogon still reject other invalid IPs that would be unusable - if (isInvalidIP(joinRequest.nodeInfo.externalIp)) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from invalid reserved IP') - nestedCountersInstance.countEvent('p2p', `join-reject-reserved`) - return { - success: false, - reason: `Bad ip, reserved ip not accepted`, - fatal: true, - } - } - } - } catch (er) { - nestedCountersInstance.countEvent('p2p', `join-reject-bogon-ex:${er}`) - } - - return null -} - export function computeNodeId(publicKey: string, cycleMarker: string): string { const obj = { publicKey, cycleMarker } const nodeId = crypto.hash(obj) @@ -1253,226 +1188,6 @@ export function computeNodeId(publicKey: string, cycleMarker: string): string { return nodeId } -/** - * This function is a little weird because it was taken directly from - * `addJoinRequest`, but here's how it works: - * - * It validates the types of the `joinRequest`. If the types are invalid, it - * returns a `JoinRequestResponse` object with `success` set to `false` and - * `fatal` set to `true`. The `reason` field will contain a message describing - * the validation error. - * - * If the types are valid, it returns `null`. - */ -export function verifyJoinRequestTypes(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { - // Validate joinReq - let err = utils.validateTypes(joinRequest, { - cycleMarker: 's', - nodeInfo: 'o', - sign: 'o', - version: 's', - }) - if (err) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest ' + err) - return { - success: false, - reason: `Bad join request object structure`, - fatal: true, - } - } - err = utils.validateTypes(joinRequest.nodeInfo, { - activeTimestamp: 'n', - address: 's', - externalIp: 's', - externalPort: 'n', - internalIp: 's', - internalPort: 'n', - joinRequestTimestamp: 'n', - publicKey: 's', - }) - if (err) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest.nodeInfo ' + err) - return { - success: false, - reason: 'Bad nodeInfo object structure within join request', - fatal: true, - } - } - err = utils.validateTypes(joinRequest.sign, { owner: 's', sig: 's' }) - if (err) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest.sign ' + err) - return { - success: false, - reason: 'Bad signature object structure within join request', - fatal: true, - } - } - - return null -} - -/** - * Makes sure that the given `nodeInfo` is not already known to the network. - * If it is, it returns a `JoinRequestResponse` object with `success` set to - * `false` and `fatal` set to `true`. The `reason` field will contain a message - * describing the validation error. - * - * If the `nodeInfo` is not already known to the network, it returns `null`. - */ -function verifyNodeUnknown(nodeInfo: P2P.P2PTypes.P2PNode): JoinRequestResponse | null { - if (NodeList.byPubKey.has(nodeInfo.publicKey)) { - const message = 'Cannot add join request for this node, already a known node (by public key).' - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn(message) - return { - success: false, - reason: message, - fatal: false, - } - } - const ipPort = NodeList.ipPort(nodeInfo.internalIp, nodeInfo.internalPort) - if (NodeList.byIpPort.has(ipPort)) { - const message = 'Cannot add join request for this node, already a known node (by IP address).' - /* prettier-ignore */ if (logFlags.p2pNonFatal) info(message, Utils.safeStringify(NodeList.byIpPort.get(ipPort))) - if (logFlags.p2pNonFatal) nestedCountersInstance.countEvent('p2p', `join-skip-already-known`) - return { - success: false, - reason: message, - fatal: true, - } - } - - return null -} - -/** - * Makes sure that the given `joinRequest` is not from an IPv6 address. If it - * is, it returns a `JoinRequestResponse` object with `success` set to `false` - * and `fatal` set to `true`. The `reason` field will contain a message - * describing the validation error. - * - * If the `joinRequest` is not from an IPv6 address, it returns `null`. - */ -function verifyNotIPv6(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { - if (isIPv6(joinRequest.nodeInfo.externalIp)) { - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from IPv6') - nestedCountersInstance.countEvent('p2p', `join-reject-ipv6`) - return { - success: false, - reason: `Bad ip version, IPv6 are not accepted`, - fatal: true, - } - } - return null -} - -/** - * Makes sure that the given `joinRequestVersion` is not older than the - * current version of the node. If it is, it returns a `JoinRequestResponse` - * object with `success` set to `false` and `fatal` set to `true`. The `reason` - * field will contain a message describing the validation error. - * - * If the `joinRequestVersion` is not older than the current version of the - * node, it returns `null`. - */ -function validateVersion(joinRequestVersion: string): JoinRequestResponse | null { - if (config.p2p.checkVersion && !isEqualOrNewerVersion(version, joinRequestVersion)) { - /* prettier-ignore */ warn(`version number is old. Our node version is ${version}. Join request node version is ${joinRequestVersion}`) - nestedCountersInstance.countEvent('p2p', `join-reject-version ${joinRequestVersion}`) - return { - success: false, - reason: `Old shardus core version, please statisfy at least ${version}`, - fatal: true, - } - } -} - -/** - * Makes sure that the given `joinRequest` is signed by the node that is - * attempting to join. If it is not, it returns a `JoinRequestResponse` object - * with `success` set to `false` and `fatal` set to `true`. The `reason` field - * will contain a message describing the validation error. - * - * If the `joinRequest` is signed by the node that is attempting to join, it - * returns `null`. - */ -function verifyJoinRequestSigner(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { - //If the node that signed the request is not the same as the node that is joining - if (joinRequest.sign.owner != joinRequest.nodeInfo.publicKey) { - /* prettier-ignore */ warn(`join-reject owner != publicKey ${{ sign: joinRequest.sign.owner, info: joinRequest.nodeInfo.publicKey }}`) - nestedCountersInstance.countEvent('p2p', `join-reject owner != publicKey`) - return { - success: false, - reason: `Bad signature, sign owner and node attempted joining mismatched`, - fatal: true, - } - } -} - -/** - * Makes sure that the given `joinRequest`'s node has not already been seen this - * cycle. If it has, it returns a `JoinRequestResponse` object with `success` - * set to `false` and `fatal` set to `false`. The `reason` field will contain a - * message describing the validation error. - * - * If the `joinRequest`'s node has not already been seen this cycle, it returns - * `null`. - */ -function verifyUnseen(publicKey: hexstring): JoinRequestResponse | null { - // Check if this node has already been seen this cycle - if (seen.has(publicKey)) { - if (logFlags.p2pNonFatal) nestedCountersInstance.countEvent('p2p', `join-skip-seen-pubkey`) - if (logFlags.p2pNonFatal) info('Node has already been seen this cycle. Unable to add join request.') - return { - success: false, - reason: 'Node has already been seen this cycle. Unable to add join request.', - fatal: false, - } - } - - // Mark node as seen for this cycle - seen.add(publicKey) - - return null -} - -function validateJoinRequestTimestamp(joinRequestTimestamp: number): JoinRequestResponse | null { - //TODO - figure out why joinRequest is send with previous cycle marker instead of current cycle marker - /* - CONTEXT: when node create join request the cycleMarker is (current - 1). - The reason join request didn't use current cycleMarker is most likely the the current cycle is potential not agreed upon yet. - but the joinRequestTimestamp is Date.now - so checking if the timestamp is within its cycleMarker is gurantee to fail - let request cycle marker be X, then X+1 is current cycle, then we check if the timestamp is in the current cycleMarker - */ - // const cycleThisJoinRequestBelong = CycleChain.cyclesByMarker[joinRequest.cycleMarker] - // const cycleStartedAt = cycleThisJoinRequestBelong.start - // const cycleWillEndsAt = cycleStartedAt + cycleDuration - const cycleDuration = CycleChain.newest.duration - const cycleStarts = CycleChain.newest.start - const requestValidUpperBound = cycleStarts + cycleDuration - const requestValidLowerBound = cycleStarts - cycleDuration - - if (joinRequestTimestamp < requestValidLowerBound) { - nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-not-meet-lowerbound`) - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Cannot add join request for this node, timestamp is earlier than allowed cycle range') - return { - success: false, - reason: 'Cannot add join request, timestamp is earlier than allowed cycle range', - fatal: false, - } - } - - if (joinRequestTimestamp > requestValidUpperBound) { - nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-beyond-upperbound`) - /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Cannot add join request for this node, its timestamp exceeds allowed cycle range') - return { - success: false, - reason: 'Cannot add join request, timestamp exceeds allowed cycle range', - fatal: false, - } - } -} - /** * Returns the selection key pertaining to the given `joinRequest`. If * `shardus.app.validateJoinRequest` is not a function, then the selection key @@ -1658,18 +1373,3 @@ export function swapUnjoinRequestQueues(): void { queuedUnjoinRequestsForThisCycle = queuedUnjoinRequestsForNextCycle queuedUnjoinRequestsForNextCycle = [] } - -function info(...msg: string[]): void { - const entry = `Join: ${msg.join(' ')}` - p2pLogger.info(entry) -} - -export function warn(...msg: string[]): void { - const entry = `Join: ${msg.join(' ')}` - p2pLogger.warn(entry) -} - -export function error(...msg: string[]): void { - const entry = `Join: ${msg.join(' ')}` - p2pLogger.error(entry) -} diff --git a/src/p2p/Join/logging.ts b/src/p2p/Join/logging.ts new file mode 100644 index 000000000..ebe8f9af3 --- /dev/null +++ b/src/p2p/Join/logging.ts @@ -0,0 +1,26 @@ +import { Logger } from 'log4js' +import { logger } from '../Context' + +export function initLogging(): void { + p2pLogger = logger.getLogger('p2p') +} + +let p2pLogger: Logger +export function getP2PLogger(): Logger { + return p2pLogger +} + +export function info(...msg: string[]): void { + const entry = `Join: ${msg.join(' ')}` + getP2PLogger().info(entry) +} + +export function warn(...msg: string[]): void { + const entry = `Join: ${msg.join(' ')}` + getP2PLogger().warn(entry) +} + +export function error(...msg: string[]): void { + const entry = `Join: ${msg.join(' ')}` + getP2PLogger().error(entry) +} diff --git a/src/p2p/Join/routes.ts b/src/p2p/Join/routes.ts index 026d8bc07..3cedcdc3f 100644 --- a/src/p2p/Join/routes.ts +++ b/src/p2p/Join/routes.ts @@ -11,16 +11,11 @@ import { P2P } from '@shardus/types' import { addJoinRequest, computeSelectionNum, - getAllowBogon, - setAllowBogon, - validateJoinRequest, verifyJoinRequestSignature, - warn, queueStandbyRefreshRequest, queueJoinRequest, queueUnjoinRequest, - verifyJoinRequestTypes, - nodeListFromStates + nodeListFromStates, } from '.' import { config } from '../Context' import { isBogonIP } from '../../utils/functions/checkIP' @@ -43,7 +38,9 @@ import { addSyncStarted } from './v2/syncStarted' import { addStandbyRefresh } from './v2/standbyRefresh' import { Utils } from '@shardus/types' import { testFailChance } from '../../utils' -import { shardusGetTime } from '../../network' +import { validateJoinRequest } from './validate' +import { warn } from './logging' +import { getAllowBogon, setAllowBogon } from './state' const cycleMarkerRoute: P2P.P2PTypes.Route = { method: 'GET', @@ -444,14 +441,14 @@ const gossipJoinRoute: P2P.P2PTypes.GossipHandler = ( payload: { - joinRequest: P2P.JoinTypes.JoinRequest, + joinRequest: P2P.JoinTypes.JoinRequest sign: P2P.P2PTypes.Signature }, sender: P2P.NodeListTypes.Node['id'], @@ -541,10 +538,17 @@ const gossipUnjoinRequests: P2P.P2PTypes.GossipHandler { - if(!checkGossipPayload(payload, { - publicKey: 's', - sign: 'o', - }, 'gossip-unjoin', sender)) { + if ( + !checkGossipPayload( + payload, + { + publicKey: 's', + sign: 'o', + }, + 'gossip-unjoin', + sender + ) + ) { return } @@ -724,7 +728,15 @@ const gossipStandbyRefresh: P2P.P2PTypes.GossipHandler< } export const routes = { - external: [cycleMarkerRoute, joinRoute, joinedRoute, joinedV2Route, acceptedRoute, unjoinRoute, standbyRefreshRoute], + external: [ + cycleMarkerRoute, + joinRoute, + joinedRoute, + joinedV2Route, + acceptedRoute, + unjoinRoute, + standbyRefreshRoute, + ], gossip: { 'gossip-join': gossipJoinRoute, 'gossip-valid-join-requests': gossipValidJoinRequests, diff --git a/src/p2p/Join/state.ts b/src/p2p/Join/state.ts new file mode 100644 index 000000000..ea074e834 --- /dev/null +++ b/src/p2p/Join/state.ts @@ -0,0 +1,17 @@ +import { P2P } from '@shardus/types' + +let seen: Set +export function getSeen(): Set { + return seen +} +export function resetSeen(): void { + seen = new Set() +} + +let allowBogon = false +export function setAllowBogon(value: boolean): void { + allowBogon = value +} +export function getAllowBogon(): boolean { + return allowBogon +} diff --git a/src/p2p/Join/types.ts b/src/p2p/Join/types.ts new file mode 100644 index 000000000..460a75c48 --- /dev/null +++ b/src/p2p/Join/types.ts @@ -0,0 +1,10 @@ +export interface JoinRequestResponse { + /** Whether the join request was accepted. TODO: consider renaming to `accepted`? */ + success: boolean + + /** A message explaining the result of the join request. */ + reason: string + + /** Whether the join request could not be accepted due to some error, usually in validating a join request. TODO: consider renaming to `invalid`? */ + fatal: boolean +} diff --git a/src/p2p/Join/utils.ts b/src/p2p/Join/utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/p2p/Join/validate.ts b/src/p2p/Join/validate.ts new file mode 100644 index 000000000..918f5f842 --- /dev/null +++ b/src/p2p/Join/validate.ts @@ -0,0 +1,306 @@ +import { version } from '../../../package.json' +import { logFlags } from '../../logger' +import { hexstring, P2P } from '@shardus/types' +import * as utils from '../../utils' +import { isEqualOrNewerVersion } from '../../utils' +import { config } from '../Context' +import * as CycleChain from '../CycleChain' +import * as NodeList from '../NodeList' +import { isBogonIP, isInvalidIP, isIPv6 } from '../../utils/functions/checkIP' +import { nestedCountersInstance } from '../../utils/nestedCounters' +import { Utils } from '@shardus/types' +import { JoinRequestResponse } from './types' +import { info, warn } from './logging' +import { getAllowBogon, getSeen } from './state' + +/** + * Returns a `JoinRequestResponse` object if the given `joinRequest` is invalid or rejected for any reason. + */ +export function validateJoinRequest(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { + // perform validation. if any of these functions return a non-null value, + // validation fails and the join request is rejected + return ( + verifyJoinRequestTypes(joinRequest) || + validateVersion(joinRequest.version) || + verifyJoinRequestSigner(joinRequest) || + verifyNotIPv6(joinRequest.nodeInfo.externalIp) || + validateJoinRequestHost(joinRequest.nodeInfo.externalIp) || + verifyUnseen(joinRequest.nodeInfo.publicKey) || + verifyNodeUnknown(joinRequest.nodeInfo) || + validateJoinRequestTimestamp(joinRequest.nodeInfo.joinRequestTimestamp) || + null + ) +} + +export const BOGON_NOT_ACCEPTED_ERROR = { + success: false, + reason: `Bad ip, bogon ip not accepted`, + fatal: true, +} +export const RESERVED_IP_REJECTED_ERROR = { + success: false, + reason: `Bad ip, reserved ip not accepted`, + fatal: true, +} +/** + * Returns an error response if the given `externalIp` is invalid or rejected. + */ +export function validateJoinRequestHost(externalIp: string): JoinRequestResponse | null { + try { + //test or bogon IPs and reject the join request if they appear + if (!getAllowBogon()) { + if (isBogonIP(externalIp)) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from Bogon IP') + nestedCountersInstance.countEvent('p2p', `join-reject-bogon`) + return BOGON_NOT_ACCEPTED_ERROR + } + } else { + //even if not checking bogon still reject other invalid IPs that would be unusable + if (isInvalidIP(externalIp)) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from invalid reserved IP') + nestedCountersInstance.countEvent('p2p', `join-reject-reserved`) + return RESERVED_IP_REJECTED_ERROR + } + } + } catch (er) { + nestedCountersInstance.countEvent('p2p', `join-reject-bogon-ex:${er}`) + return { + success: false, + reason: `Error in checking IP: ${er}`, + fatal: true, + } + } + + return null +} + +export const BAD_STRUCTURE_ERROR = { + success: false, + reason: `Bad join request object structure`, + fatal: true, +} +export const BAD_NODE_INFO_STRUCTURE_ERROR = { + success: false, + reason: 'Bad nodeInfo object structure within join request', + fatal: true, +} +export const BAD_SIGNATURE_STRUCTURE_ERROR = { + success: false, + reason: 'Bad signature object structure within join request', + fatal: true, +} +/** + * This function is a little weird because it was taken directly from + * `addJoinRequest`, but here's how it works: + * + * It validates the types of the `joinRequest`. If the types are invalid, it + * returns a `JoinRequestResponse` object with `success` set to `false` and + * `fatal` set to `true`. The `reason` field will contain a message describing + * the validation error. + * + * If the types are valid, it returns `null`. + */ +export function verifyJoinRequestTypes(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { + // Validate joinReq + let err = utils.validateTypes(joinRequest, { + cycleMarker: 's', + nodeInfo: 'o', + sign: 'o', + version: 's', + }) + if (err) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest ' + err) + return BAD_STRUCTURE_ERROR + } + err = utils.validateTypes(joinRequest.nodeInfo, { + activeTimestamp: 'n', + address: 's', + externalIp: 's', + externalPort: 'n', + internalIp: 's', + internalPort: 'n', + joinRequestTimestamp: 'n', + publicKey: 's', + }) + if (err) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest.nodeInfo ' + err) + return BAD_NODE_INFO_STRUCTURE_ERROR + } + err = utils.validateTypes(joinRequest.sign, { owner: 's', sig: 's' }) + if (err) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('join bad joinRequest.sign ' + err) + return BAD_SIGNATURE_STRUCTURE_ERROR + } + + return null +} + +export const ALREADY_KNOWN_PK_ERROR = { + success: false, + reason: 'Cannot add join request for this node, already a known node (by public key).', + fatal: false, +} +export const ALREADY_KNOWN_IP_ERROR = { + success: false, + reason: 'Cannot add join request for this node, already a known node (by IP address).', + fatal: true, +} +/** + * Makes sure that the given `nodeInfo` is not already known to the network. + * If it is, it returns a `JoinRequestResponse` object with `success` set to + * `false` and `fatal` set to `true`. The `reason` field will contain a message + * describing the validation error. + * + * If the `nodeInfo` is not already known to the network, it returns `null`. + */ +export function verifyNodeUnknown(nodeInfo: P2P.P2PTypes.P2PNode): JoinRequestResponse | null { + if (NodeList.getByPubKeyMap().has(nodeInfo.publicKey)) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn(ALREADY_KNOWN_PK_ERROR.reason) + return ALREADY_KNOWN_PK_ERROR + } + const ipPort = NodeList.ipPort(nodeInfo.internalIp, nodeInfo.internalPort) + if (NodeList.getByIpPortMap().has(ipPort)) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) info(ALREADY_KNOWN_IP_ERROR.reason, Utils.safeStringify(NodeList.getByIpPortMap().get(ipPort))) + if (logFlags.p2pNonFatal) nestedCountersInstance.countEvent('p2p', `join-skip-already-known`) + return ALREADY_KNOWN_IP_ERROR + } + + return null +} + +export const IPV6_ERROR = { + success: false, + reason: `Bad ip version, IPv6 are not accepted`, + fatal: true, +} +/** + * Makes sure that the given `externalIp` is not an IPv6 address. If it + * is, it returns a `JoinRequestResponse` object with `success` set to `false` + * and `fatal` set to `true`. The `reason` field will contain a message + * describing the validation error. + * + * If the `externalIp` is not an IPv6 address, it returns `null`. + */ +export function verifyNotIPv6(externalIp: string): JoinRequestResponse | null { + if (isIPv6(externalIp)) { + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Got join request from IPv6') + nestedCountersInstance.countEvent('p2p', `join-reject-ipv6`) + return IPV6_ERROR + } + return null +} + +export const BAD_VERSION_ERROR = { + success: false, + reason: `Old shardus core version, please satisfy at least ${version}`, + fatal: true, +} +/** + * Makes sure that the given `joinRequestVersion` is not older than the + * current version of the node. If it is, it returns a `JoinRequestResponse` + * object with `success` set to `false` and `fatal` set to `true`. The `reason` + * field will contain a message describing the validation error. + * + * If the `joinRequestVersion` is not older than the current version of the + * node, it returns `null`. + */ +export function validateVersion(joinRequestVersion: string): JoinRequestResponse | null { + if (config.p2p.checkVersion && !isEqualOrNewerVersion(version, joinRequestVersion)) { + /* prettier-ignore */ warn(`version number is old. Our node version is ${version}. Join request node version is ${joinRequestVersion}`) + nestedCountersInstance.countEvent('p2p', `join-reject-version ${joinRequestVersion}`) + return BAD_VERSION_ERROR + } else return null +} + +export const BAD_SIGNATURE_ERROR = { + success: false, + reason: 'Bad signature, sign owner and node attempted joining mismatched', + fatal: true, +} +/** + * Makes sure that the given `joinRequest` is signed by the node that is + * attempting to join. If it is not, it returns a `JoinRequestResponse` object + * with `success` set to `false` and `fatal` set to `true`. The `reason` field + * will contain a message describing the validation error. + * + * If the `joinRequest` is signed by the node that is attempting to join, it + * returns `null`. + */ +export function verifyJoinRequestSigner(joinRequest: P2P.JoinTypes.JoinRequest): JoinRequestResponse | null { + //If the node that signed the request is not the same as the node that is joining + if (joinRequest.sign.owner != joinRequest.nodeInfo.publicKey) { + /* prettier-ignore */ warn(`join-reject owner != publicKey ${{ sign: joinRequest.sign.owner, info: joinRequest.nodeInfo.publicKey }}`) + nestedCountersInstance.countEvent('p2p', `join-reject owner != publicKey`) + return BAD_SIGNATURE_ERROR + } + + return null +} + +export const ALREADY_SEEN_ERROR = { + success: false, + reason: 'Node has already been seen this cycle. Unable to add join request.', + fatal: false, +} +/** + * Makes sure that the given `joinRequest`'s node has not already been seen this + * cycle. If it has, it returns a `JoinRequestResponse` object with `success` + * set to `false` and `fatal` set to `false`. The `reason` field will contain a + * message describing the validation error. + * + * If the `joinRequest`'s node has not already been seen this cycle, it returns + * `null`. + */ +export function verifyUnseen(publicKey: hexstring): JoinRequestResponse | null { + // Check if this node has already been seen this cycle + if (getSeen().has(publicKey)) { + if (logFlags.p2pNonFatal) nestedCountersInstance.countEvent('p2p', `join-skip-seen-pubkey`) + if (logFlags.p2pNonFatal) info(ALREADY_SEEN_ERROR.reason) + return ALREADY_SEEN_ERROR + } + + // Mark node as seen for this cycle + getSeen().add(publicKey) + + return null +} + +export const EARLY_TIMESTAMP_ERROR = { + success: false, + reason: 'Cannot add join request for this node, timestamp is earlier than allowed cycle range', + fatal: false, +} +export const LATE_TIMESTAMP_ERROR = { + success: false, + reason: 'Cannot add join request, timestamp exceeds allowed cycle range', + fatal: false, +} +function validateJoinRequestTimestamp(joinRequestTimestamp: number): JoinRequestResponse | null { + //TODO - figure out why joinRequest is send with previous cycle marker instead of current cycle marker + /* + CONTEXT: when node create join request the cycleMarker is (current - 1). + The reason join request didn't use current cycleMarker is most likely the the current cycle is potential not agreed upon yet. + but the joinRequestTimestamp is Date.now + so checking if the timestamp is within its cycleMarker is gurantee to fail + let request cycle marker be X, then X+1 is current cycle, then we check if the timestamp is in the current cycleMarker + */ + // const cycleThisJoinRequestBelong = CycleChain.cyclesByMarker[joinRequest.cycleMarker] + // const cycleStartedAt = cycleThisJoinRequestBelong.start + // const cycleWillEndsAt = cycleStartedAt + cycleDuration + const cycleDuration = CycleChain.newest.duration + const cycleStarts = CycleChain.newest.start + const requestValidUpperBound = cycleStarts + cycleDuration + const requestValidLowerBound = cycleStarts - cycleDuration + + if (joinRequestTimestamp < requestValidLowerBound) { + nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-not-meet-lowerbound`) + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn(EARLY_TIMESTAMP_ERROR.reason) + return EARLY_TIMESTAMP_ERROR + } + + if (joinRequestTimestamp > requestValidUpperBound) { + nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-beyond-upperbound`) + /* prettier-ignore */ if (logFlags.p2pNonFatal) warn('Cannot add join request for this node, its timestamp exceeds allowed cycle range') + return LATE_TIMESTAMP_ERROR + } +} diff --git a/src/p2p/NodeList.ts b/src/p2p/NodeList.ts index c11fe1569..b52ee4820 100644 --- a/src/p2p/NodeList.ts +++ b/src/p2p/NodeList.ts @@ -515,6 +515,14 @@ export function changeNodeListInRestore(cycleStartTimestamp: number) { } } +export function getByPubKeyMap() { + return byPubKey +} + +export function getByIpPortMap() { + return byIpPort +} + /** ROUTES */ function info(...msg: string[]) { diff --git a/test/unit/src/validateJoinRequest.test.ts b/test/unit/src/validateJoinRequest.test.ts new file mode 100644 index 000000000..d6228219b --- /dev/null +++ b/test/unit/src/validateJoinRequest.test.ts @@ -0,0 +1,455 @@ +import { version as packageVersion } from '../../../package.json' +import { JoinRequest } from '@shardus/types/build/src/p2p/JoinTypes' +import { + ALREADY_KNOWN_IP_ERROR, + ALREADY_KNOWN_PK_ERROR, + ALREADY_SEEN_ERROR, + BAD_SIGNATURE_ERROR, + BAD_STRUCTURE_ERROR, + BAD_VERSION_ERROR, + BOGON_NOT_ACCEPTED_ERROR, + EARLY_TIMESTAMP_ERROR, + IPV6_ERROR, + LATE_TIMESTAMP_ERROR, + RESERVED_IP_REJECTED_ERROR, + validateJoinRequest, + validateJoinRequestHost, + validateVersion, + verifyJoinRequestSigner, + verifyJoinRequestTypes, + verifyNodeUnknown, + verifyNotIPv6, +} from '../../../src/p2p/Join/validate' +import { JoinRequestResponse } from '../../../src/p2p/Join/types' +import { getAllowBogon, getSeen } from '../../../src/p2p/Join/state' +import { getByIpPortMap, getByPubKeyMap, ipPort as mkIpPort } from '../../../src/p2p/NodeList' + +// mock some required functions from the Context module +jest.mock('../../../src/p2p/Context', () => ({ + setDefaultConfigs: jest.fn(), + config: { + p2p: { + checkVersion: true, + useJoinProtocolV2: true, + }, + }, +})) + +// set up mocking for NodeList maps +jest.mock('../../../src/p2p/NodeList', () => { + const actual = jest.requireActual('../../../src/p2p/NodeList') + return { + ...actual, + getByPubKeyMap: jest.fn().mockReturnValue(new Map()), + getByIpPortMap: jest.fn().mockReturnValue(new Map()), + } +}) + +// mock some CycleChain fields +jest.mock('../../../src/p2p/CycleChain', () => ({ + newest: { + duration: 60, + start: 1723322908, // validJoinRequest.nodeInfo.joinRequestTimestamp - 10 + }, +})) + +// mock logging functions +jest.mock('../../../src/p2p/Join/logging', () => ({ + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +})) + +// mock state functions to allow modification at test-time +jest.mock('../../../src/p2p/Join/state', () => ({ + getAllowBogon: jest.fn().mockReturnValue(true), + getSeen: jest.fn().mockReturnValue(new Set()), +})) + +/** A test case for modifying and testing a JoinRequest. */ +interface ValidateJoinRequestTest { + /** `it` description of the test. */ + it: string + + /** How the `validJoinRequest` should be mutated for this test. Overrides + * fields in the `validJoinRequest` when tests are run. */ + mutation?: object + + /** The expected error response from the validation function. If `null` or + * `undefined`, the test is expected to be successful instead of failing with + * an error. */ + expectedError?: JoinRequestResponse + + /** Optional function to run before the test, for e.g. mocking adjustments. */ + before?: () => void +} + +/** + * Runs a collection of tests using the provided function, which will receive + * a mutated JoinRequest (depending on ValidateJoinRequestTest.mutation) and + * return a `JoinRequestResponse` (if unsuccessful) or `null` (if validation + * passes). + */ +const runTestsWith = ( + fn: (jr: JoinRequest) => null | JoinRequestResponse, + tests: ValidateJoinRequestTest[] +): void => { + for (const test of tests) { + // create an `it` call + it(test.it, () => { + // run before hook if provided + if (test.before) test.before() + + // warn if the test is expecting an error, but `success` is expected to be + // `true` + if (test.expectedError?.success) { + console.warn('this test is expecting an error, but `success` is expected to be `true`') + } + + // create the mutated JoinRequest, asserting that it is a valid + // JoinRequest (even though it's very likely not, haha, joke's on you, + // TypeScript) + const joinRequest = { + ...validJoinRequest, + ...(test.mutation || {}), + } as JoinRequest + + // run the validation function and get the result + const result = fn(joinRequest) + + // seen set needs to be cleared after every test + getSeen().clear() + + // check if the result is what was expected + if (!test.expectedError) { + expect(result).toBeNull() + } else { + expect(result).toStrictEqual(test.expectedError) + } + }) + } +} + +/** A valid JoinRequest, sourced organically from a local network. */ +const validJoinRequest: JoinRequest = Object.freeze({ + appJoinData: { adminCert: null, mustUseAdminCert: false, stakeCert: null, version: '1.12.1' }, + cycleMarker: '45da5452eb8e45d73eee05a973375ef824f43d8a3f67186e24222e3bdd5e7940', + nodeInfo: { + activeTimestamp: 0, + address: '3d173e0ce5ee2c81fd53f810d34093ae33bfeb76b352a05237d3575fc7c23f2d', + externalIp: '127.0.0.1', + externalPort: 9003, + internalIp: '127.0.0.1', + internalPort: 10003, + joinRequestTimestamp: 1723322918, + publicKey: '3d173e0ce5ee2c81fd53f810d34093ae33bfeb76b352a05237d3575fc7c23f2d', + readyTimestamp: 0, + refreshedCounter: 2, + syncingTimestamp: 0, + }, + proofOfWork: + '{"compute":{"hash":"28efe69b94cf3786477fdbba8da46bfbf399d02a649da97f6b82b0da8d4d6490","nonce":"f7e6cbce19eef02662e7fc40a8dc8e76f437ba0bd0e9a2dc9f5edfa12e55d754"}}', + selectionNum: 'a331b77d1413e3a71181b7a62451c49151fd4c516c2b9e5a778f11d3709feec9', + sign: { + owner: '3d173e0ce5ee2c81fd53f810d34093ae33bfeb76b352a05237d3575fc7c23f2d', + sig: '5e5e8b826526aa3f9b49731e9dc0b2497f1e4c3f1a0dde18920c70e31dc4a12d62f1f0145e39ee9ae2f5351beda0d48a26da816dbbfb6232eb8d5f403443360909b6b3625fafa0f078857197320b63d24d81960f48ec2058a6c8f7be272a8430', + }, + version: packageVersion, +}) + +describe('validateJoinRequest', () => { + const tests: ValidateJoinRequestTest[] = [ + { + it: 'should pass with valid join request', + }, + { + it: 'should fail with older join request version', + mutation: { + version: '0.0.0', + }, + expectedError: BAD_VERSION_ERROR, + }, + { + it: 'should fail with incorrect join request types', + mutation: { + cycleMarker: 1, + }, + expectedError: BAD_STRUCTURE_ERROR, + }, + { + it: 'should fail with invalid signer', + mutation: { + sign: { + ...validJoinRequest.sign, + owner: 'invalid owner', + }, + }, + expectedError: BAD_SIGNATURE_ERROR, + }, + { + it: 'should fail with IPv6', + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + externalIp: 'abcd:ef01:2345:6789:abcd:ef01:2345:6789', + internalIp: '::', + }, + }, + expectedError: IPV6_ERROR, + }, + { + it: 'should fail with invalid host', + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + externalIp: 'not an address', + internalIp: 'also not an address, but not checked', + }, + }, + expectedError: RESERVED_IP_REJECTED_ERROR, + }, + { + it: 'should fail if already seen', + before: () => { + ;(getSeen as jest.Mock).mockReturnValueOnce(new Set(['already seen'])) + }, + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + publicKey: 'already seen', + }, + sign: { + ...validJoinRequest.sign, + owner: 'already seen', + }, + }, + expectedError: ALREADY_SEEN_ERROR, + }, + { + it: 'should fail if joining node is known by public key', + before: () => { + ;(getByPubKeyMap as jest.Mock).mockReturnValueOnce(new Map([['known node', {}]])) + }, + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + publicKey: 'known node', + }, + sign: { + ...validJoinRequest.sign, + owner: 'known node', + }, + }, + expectedError: ALREADY_KNOWN_PK_ERROR, + }, + { + it: 'should fail if joining node is known by IP address', + before: () => { + ;(getByIpPortMap as jest.Mock).mockReturnValueOnce(new Map([['12.34.56.78:90', {}]])) + }, + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + internalIp: '12.34.56.78', + internalPort: 90, + }, + }, + expectedError: ALREADY_KNOWN_IP_ERROR, + }, + { + it: 'should fail with timestamp too early', + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + joinRequestTimestamp: 0, + }, + }, + expectedError: EARLY_TIMESTAMP_ERROR, + }, + { + it: 'should fail with timestamp too late', + mutation: { + nodeInfo: { + ...validJoinRequest.nodeInfo, + joinRequestTimestamp: 999999999999999, + }, + }, + expectedError: LATE_TIMESTAMP_ERROR, + }, + ] + + runTestsWith(validateJoinRequest, tests) +}) + +describe('verifyJoinRequestTypes', () => { + const tests: ValidateJoinRequestTest[] = [ + { + it: 'should pass when correct types are passed', + mutation: {}, + }, + { + it: 'should fail if cycleMarker is a number', + mutation: { cycleMarker: 1 }, + expectedError: BAD_STRUCTURE_ERROR, + }, + { + it: 'should fail if nodeInfo is a string', + mutation: { nodeInfo: '192.68.123.123' }, + expectedError: BAD_STRUCTURE_ERROR, + }, + { + it: 'should fail if sign if a string', + mutation: { sign: 'i am a hash but i should be an object' }, + expectedError: BAD_STRUCTURE_ERROR, + }, + { + it: 'should fail if version is a number', + mutation: { version: 1 }, + expectedError: BAD_STRUCTURE_ERROR, + }, + ] + + runTestsWith(verifyJoinRequestTypes, tests) +}) + +describe('validateVersion', () => { + it('should pass with equal join request version', () => { + const result = validateVersion(packageVersion) + expect(result).toBeNull() + }) + it('should pass with newer join request version', () => { + const result = validateVersion('99999.99999.99999') + expect(result).toBeNull() + }) + it('should fail with older join request version', () => { + const result = validateVersion('0.0.0') + expect(result).toStrictEqual(BAD_VERSION_ERROR) + }) +}) + +describe('verifyJoinRequestSigner', () => { + const tests = [ + { + it: 'should pass with correct signer', + mutation: {}, + shouldSucceed: true, + }, + { + it: 'should fail with wrong signature owner', + mutation: { + sign: { + owner: 'wrong owner', + }, + }, + expectedError: BAD_SIGNATURE_ERROR, + }, + { + it: 'should fail with wrong node public key', + mutation: { + nodeInfo: { + publicKey: 'wrong key', + }, + }, + expectedError: BAD_SIGNATURE_ERROR, + }, + ] + runTestsWith(verifyJoinRequestSigner, tests) +}) + +describe('verifyNotIPv6', () => { + it('should pass without IPv6', () => { + expect(verifyNotIPv6('192.168.1.1')).toBeNull() + }) + it('should fail with IPv6', () => { + expect(verifyNotIPv6('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).toStrictEqual(IPV6_ERROR) + }) +}) + +describe('validateJoinRequestHost', () => { + it('should pass with valid IP', () => { + expect(validateJoinRequestHost('123.234.123.234')).toBeNull() + }) + + // test reserved IPs + const reservedAddresses = [ + '0.1.2.3', + '192.0.0.100', + '192.0.2.4', + '198.18.4.5', + '198.51.100.5', + '203.0.113.60', + '224.239.0.2', + '239.0.123.123', + '240.1.2.3', + '254.4.5.6', + '255.255.255.255', + ] + for (const address of reservedAddresses) { + it(`should fail with reserved IP ${address}, bogon disallowed`, () => { + ;(getAllowBogon as jest.Mock).mockReturnValue(false) + expect(validateJoinRequestHost(address)).toStrictEqual(BOGON_NOT_ACCEPTED_ERROR) + }) + } + for (const address of reservedAddresses) { + it(`should fail with reserved IP ${address}, bogon allowed`, () => { + ;(getAllowBogon as jest.Mock).mockReturnValue(true) + expect(validateJoinRequestHost(address)).toStrictEqual(RESERVED_IP_REJECTED_ERROR) + }) + } + + // bogon tests + const privateAddresses = [ + '10.1.2.3', + '100.64.0.0', + '100.127.0.0', + '127.0.1.2', + '169.254.100.100', + '172.16.1.2', + '172.31.3.4', + '192.168.0.1', + ] + + // test with bogon allowed + for (const address of privateAddresses) { + it(`should pass with private IP ${address}, bogon allowed`, () => { + ;(getAllowBogon as jest.Mock).mockReturnValue(true) + expect(validateJoinRequestHost(address)).toBeNull() + }) + } + + // test with bogon disallowed + for (const address of privateAddresses) { + it(`should fail with private IP ${address}, bogon disallowed`, () => { + ;(getAllowBogon as jest.Mock).mockReturnValue(false) + expect(validateJoinRequestHost(address)).toStrictEqual(BOGON_NOT_ACCEPTED_ERROR) + }) + } +}) + +// describe('verifyUnseen', () => { +// it('should pass if node is unseen', () => {}) +// it('should fail if node is already seen', () => {}) +// }) + +describe('verifyNodeUnknown', () => { + it('should pass if node is unknown', () => { + const result = verifyNodeUnknown(validJoinRequest.nodeInfo) + expect(result).toBeNull() + }) + it('should fail if node is known by public key', () => { + ;(getByPubKeyMap as jest.Mock).mockReturnValueOnce(new Map([[validJoinRequest.nodeInfo.publicKey, {}]])) + const result = verifyNodeUnknown(validJoinRequest.nodeInfo) + expect(result).toStrictEqual(ALREADY_KNOWN_PK_ERROR) + }) + it('should fail if node is known by internal IP address', () => { + const ipPort = mkIpPort(validJoinRequest.nodeInfo.internalIp, validJoinRequest.nodeInfo.internalPort) + ;(getByIpPortMap as jest.Mock).mockReturnValueOnce(new Map([[ipPort, {}]])) + const result = verifyNodeUnknown(validJoinRequest.nodeInfo) + expect(result).toStrictEqual(ALREADY_KNOWN_IP_ERROR) + }) +}) + +// describe('validateJoinRequestTimestamp', () => { +// it('should pass with correct timestamp', () => {}) +// it('should fail with invalid timestamp', () => {}) +// })