"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.error = exports.warn = exports.swapUnjoinRequestQueues = exports.nodeListFromStates = exports.computeSelectionNum = exports.verifyJoinRequestSignature = exports.verifyJoinRequestTypes = exports.computeNodeId = exports.validateJoinRequest = exports.fetchJoinedV2 = exports.fetchJoined = exports.submitJoinV2 = exports.firstJoin = exports.addJoinRequest = exports.createJoinRequest = exports.queueUnjoinRequest = exports.queueJoinRequest = exports.queueStandbyRefreshRequest = exports.queueFinishedSyncingRequest = exports.queueStartedSyncingRequest = exports.queueRequest = exports.sendRequests = exports.parseRecord = exports.updateRecord = exports.dropInvalidTxs = exports.validateRecordTypes = exports.getTxs = exports.calculateToAccept = exports.getNodeRequestingJoin = exports.reset = exports.init = exports.finishedSyncingCycle = exports.getAllowBogon = exports.setAllowBogon = void 0;
const package_json_1 = require("../../../package.json");
const http = __importStar(require("../../http"));
const logger_1 = require("../../logger");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const utils = __importStar(require("../../utils"));
const utils_1 = require("../../utils");
const Comms = __importStar(require("../Comms"));
const Context_1 = require("../Context");
const CycleChain = __importStar(require("../CycleChain"));
const CycleCreator = __importStar(require("../CycleCreator"));
const NodeList = __importStar(require("../NodeList"));
const Self = __importStar(require("../Self"));
const Utils_1 = require("../Utils");
const checkIP_1 = require("../../utils/functions/checkIP");
const nestedCounters_1 = require("../../utils/nestedCounters");
const ModeSystemFuncs_1 = require("../ModeSystemFuncs");
const routes_1 = require("./routes");
const v2_1 = require("./v2");
const neverthrow_1 = require("neverthrow");
const select_1 = require("./v2/select");
const unjoin_1 = require("./v2/unjoin");
const syncStarted_1 = require("./v2/syncStarted");
const syncFinished_1 = require("./v2/syncFinished");
//import { getLastCycleStandbyRefreshRequest, resetLastCycleStandbyRefreshRequests, drainNewStandbyRefreshRequests } from './v2/standbyRefresh'
const standbyRefresh_1 = require("./v2/standbyRefresh");
const rfdc_1 = __importDefault(require("rfdc"));
const lib_types_2 = require("@shardeum-foundation/lib-types");
const Active_1 = require("../Active");
/** STATE */
let p2pLogger;
let mainLogger;
const clone = (0, rfdc_1.default)();
let requests;
let seen;
let queuedReceivedJoinRequests = [];
let queuedJoinRequestsForGossip = [];
let queuedStartedSyncingId;
let queuedFinishedSyncingId;
let queuedStandbyRefreshPubKeys = [];
let queuedUnjoinRequestsForNextCycle = [];
let queuedUnjoinRequestsForThisCycle = [];
let cyclesToDelaySyncStarted = -1;
let cyclesToDelaySyncFinished = -1;
// whats this for? I was just going to use newStandbyRefreshRequests
//let keepInStandbyCollector: Map<string, StandbyRefreshRequest>
//let localStandbyCheckerJobs: Set<string>
let lastLoggedCycle = 0;
let allowBogon = false;
function setAllowBogon(value) {
    allowBogon = value;
}
exports.setAllowBogon = setAllowBogon;
function getAllowBogon() {
    return allowBogon;
}
exports.getAllowBogon = getAllowBogon;
let mode = null;
exports.finishedSyncingCycle = -1;
// let hasSubmittedJoinRequest = false
// export function getHasSubmittedJoinRequest(): boolean {
//   return hasSubmittedJoinRequest
// }
/** FUNCTIONS */
/** CycleCreator Functions */
function init() {
    p2pLogger = Context_1.logger.getLogger('p2p');
    mainLogger = Context_1.logger.getLogger('main');
    // Init state
    reset();
    // Register routes
    for (const route of routes_1.routes.external) {
        Context_1.network._registerExternal(route.method, route.name, route.handler);
    }
    for (const [name, handler] of Object.entries(routes_1.routes.gossip)) {
        Comms.registerGossipHandler(name, handler);
    }
}
exports.init = init;
function reset() {
    requests = [];
    seen = new Set();
    //keepInStandbyCollector = new Map()
}
exports.reset = reset;
function getNodeRequestingJoin() {
    const nodes = [];
    for (const request of requests) {
        if (request && request.nodeInfo) {
            nodes.push(request.nodeInfo);
        }
    }
    return nodes;
}
exports.getNodeRequestingJoin = getNodeRequestingJoin;
/** calculateToAccept - calculates the number of nodes to accept into the network */
function calculateToAccept() {
    const desired = CycleChain.newest.desired;
    const active = CycleChain.newest.active;
    let maxJoin = Context_1.config.p2p.maxJoinedPerCycle; // [TODO] allow autoscaling to change this
    const syncing = NodeList.byJoinOrder.length - active;
    const expired = CycleChain.newest.expired;
    maxJoin = Math.floor(maxJoin * CycleCreator.scaleFactor);
    // If in safetyMode, set syncMax to safetyNum
    let syncMax = CycleChain.newest.safetyMode === true
        ? CycleChain.newest.safetyNum
        : Math.floor(Context_1.config.p2p.maxSyncingPerCycle * CycleCreator.scaleFactor * CycleCreator.scaleFactorSyncBoost);
    //The first batch of nodes to join the network after the seed node server can join at a higher rate if firstCycleJoin is set.
    //This first batch will sync the full data range from the seed node, which should be very little data.
    //This gets the network rolling faster, but also allows us to use a slightly higher base join rate because
    //we are not worrying on how it performs with small networks. < 25 nodes.
    if (active === 0 && Context_1.config.p2p.firstCycleJoin) {
        maxJoin = Math.max(Context_1.config.p2p.firstCycleJoin, maxJoin);
        syncMax += Context_1.config.p2p.firstCycleJoin;
    }
    //For a few cycles we can boost the max sync to account for firstCycleJoin nodes.
    if (CycleChain.newest.counter < 10 && Context_1.config.p2p.firstCycleJoin) {
        syncMax += Context_1.config.p2p.firstCycleJoin;
    }
    if (active > 0) {
        const syncMaxLimit = 150; //todo make config
        if (syncMax > syncMaxLimit) {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('networkSize', `limit syncmax ${syncMax}=>${syncMaxLimit} cyc:${CycleCreator.currentCycle}`);
            syncMax = syncMaxLimit;
        }
    }
    const canSync = syncMax - syncing;
    let needed = 0;
    // Always set needed to (desired - (active + syncing)) if its positive
    //if (desired > active + syncing) {
    // updating to make sure this math gets applied all the time
    if (desired > 0) {
        needed = desired - (active + syncing);
    }
    // If rotation is on, add expired to needed
    if (Context_1.config.p2p.maxRotatedPerCycle > 0) {
        //only can accept as many as could actually rotate out in one cycle
        const maxToLeave = Math.min(expired, Context_1.config.p2p.maxRotatedPerCycle);
        needed += maxToLeave;
    }
    // Limit needed by canSync and maxJoin
    if (needed > canSync) {
        needed = canSync;
    }
    if (needed > maxJoin) {
        needed = maxJoin;
    }
    if (needed < 0) {
        needed = 0;
    }
    const cycle = CycleChain.newest.counter;
    if (cycle > lastLoggedCycle) {
        lastLoggedCycle = cycle;
        info('scale dump:' +
            lib_types_2.Utils.safeStringify({
                cycle,
                scaleFactor: CycleCreator.scaleFactor,
                needed,
                desired,
                active,
                syncing,
                canSync,
                syncMax,
                maxJoin,
                expired,
                scaleFactorSyncBoost: CycleCreator.scaleFactorSyncBoost,
            }));
    }
    return needed;
}
exports.calculateToAccept = calculateToAccept;
function getTxs() {
    return {
        standbyAdd: (0, v2_1.drainNewJoinRequests)(),
        startedSyncing: (0, syncStarted_1.drainSyncStarted)(),
        finishedSyncing: (0, syncFinished_1.drainFinishedSyncingRequest)(),
        standbyRefresh: (0, standbyRefresh_1.drainNewStandbyRefreshRequests)(),
        standbyRemove: (0, unjoin_1.drainNewUnjoinRequests)(),
    };
}
exports.getTxs = getTxs;
function validateRecordTypes(rec) {
    let err = (0, utils_1.validateTypes)(rec, { syncing: 'n', joinedConsensors: 'a' });
    if (err)
        return err;
    for (const item of rec.joinedConsensors) {
        err = (0, utils_1.validateTypes)(item, {
            activeTimestamp: 'n',
            address: 's',
            externalIp: 's',
            externalPort: 'n',
            internalIp: 's',
            internalPort: 'n',
            joinRequestTimestamp: 'n',
            publicKey: 's',
            cycleJoined: 's',
            counterRefreshed: 'n',
            id: 's',
        });
        if (err)
            return 'in joinedConsensors array ' + err;
    }
    return '';
}
exports.validateRecordTypes = validateRecordTypes;
function dropInvalidTxs(txs) {
    // TODO drop any invalid join requests. NOTE: this has never been implemented
    // yet, so this task is not a side effect of any work on join v2.
    return {
        standbyAdd: txs.standbyAdd,
        startedSyncing: [],
        finishedSyncing: [],
        standbyRefresh: [],
        standbyRemove: [],
    };
}
exports.dropInvalidTxs = dropInvalidTxs;
function updateRecord(txs, record) {
    record.syncing = NodeList.syncingByIdOrder.length;
    record.standbyAdd = [];
    record.standbyRemove = [];
    record.startedSyncing = [];
    record.lostAfterSelection = [];
    record.finishedSyncing = [];
    record.standbyRefresh = [];
    record.standbyAdd = [];
    if (Context_1.config.p2p.useJoinProtocolV2) {
        // for join v2, add new standby nodes to the standbyAdd field ...
        for (const request of txs.standbyAdd) {
            record.standbyAdd.push(request);
        }
        for (const request of txs.startedSyncing) {
            const publicKey = request.sign.owner;
            const node = NodeList.byPubKey.get(publicKey);
            if (node) {
                record.startedSyncing.push(request.nodeId);
            }
            else {
                /* prettier-ignore */ if (logger_1.logFlags.important_as_error)
                    warn(`join:updateRecord:startedSyncing: node not found: ${publicKey}`);
            }
        }
        record.syncing += record.startedSyncing.length;
        // these nodes are being repeated in lost and apop
        for (const nodeId of (0, syncStarted_1.drainLostAfterSelectionNodes)()) {
            record.lostAfterSelection.push(nodeId);
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `added node to lostAfterSelection`);
            /* prettier-ignore */ if (logger_1.logFlags.verbose)
                console.log(`added node to lostAfterSelection`);
        }
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            console.log('newSyncFinished nodes ', syncFinished_1.newSyncFinishedNodes);
        // add node id from newSyncFinishedNodes to the finishedSyncing list to update readyByTimeAndIdOrder when parsed
        for (const request of txs.finishedSyncing) {
            const publicKey = request.sign.owner;
            const node = NodeList.byPubKey.get(publicKey);
            if (node) {
                /* prettier-ignore */ if (logger_1.logFlags.verbose)
                    console.log(`drainFinishedSyncingRequest: ${request.nodeId}`);
                record.finishedSyncing.push(request.nodeId);
            }
            else {
                /* prettier-ignore */ if (logger_1.logFlags.important_as_error)
                    warn(`join:updateRecord:finishedSyncing: node not found: ${publicKey}`);
            }
        }
        // drain our list of standby refresh TXs to the list
        // not sure full network gossip is the way to go here...
        // what about TX sharing in cycle process?
        // the standby nodes would use a tell to get an active node to submit the tx?
        // only the node that the refresh request was posted to would have the tx?
        for (const request of txs.standbyRefresh) {
            const publicKey = request.sign.owner;
            const node = (0, v2_1.getStandbyNodesInfoMap)().get(publicKey);
            if (node) {
                record.standbyRefresh.push(request.publicKey);
            }
            else {
                /* prettier-ignore */ if (logger_1.logFlags.important_as_error)
                    warn(`join:updateRecord:standbyRefresh: node not found: ${publicKey}`);
            }
        }
        let standbyRemoved_Age = 0;
        //let standbyRemoved_joined = 0
        let standbyRemoved_App = 0;
        let skipped = 0;
        const standbyListMap = (0, v2_1.getStandbyNodesInfoMap)();
        const standbyList = (0, v2_1.getLastHashedStandbyList)();
        if (Context_1.config.p2p.standbyAgeScrub) {
            // scrub the stanby list of nodes that have been in it too long.  > standbyListCyclesTTL num cycles
            for (const joinRequest of standbyList) {
                /*
                const maxAge = record.duration * config.p2p.standbyListCyclesTTL
                const key = joinRequest.nodeInfo.publicKey
                let lastStandbyTimerRefresh = joinRequest.nodeInfo.joinRequestTimestamp
                //check the refresh map to get a more up to date cycle age for this node
                if(standbyNodesRefresh.has(key)){
                  lastStandbyTimerRefresh = standbyNodesRefresh.get(key)
                }
                //check 2 cycles before we would remove this node
                if (record.start - lastStandbyTimerRefresh > maxAge - 2) {
        
                  if (standbyListMap.has(key) === false) {
                    skipped++
                    continue
                  }
        
                  // is this done? what else is needed?
                  //TODO deterministic selection of a node to query if the standby node is up
                  //this checked response will become a cycle TX for the next cycle.
        
                  const offset = getPrefixInt(key, 8)
                  const numActiveNodes = NodeList.activeByIdOrder.length
                  const numCheckerNodes = 1
                  const queueStandbyCheck = fastIsPicked(ourIndex, numActiveNodes, numCheckerNodes, offset)
        
                  //schedule a job or us to check on
                  if(queueStandbyCheck){
                    localStandbyCheckerJobs.add(key)
                  }
        
                  // should be in parseRecord then
                  //WE only set this map when digesting the cycle record if this node did a proper refresh action
                  //use this cycle start time as an updated time in the mapp of standby nodes refresh
                  //standbyNodesRefresh.set(key, record.start)
                }
                */
                const lastRefreshed = joinRequest.nodeInfo.refreshedCounter;
                if (record.counter - lastRefreshed >= Context_1.config.p2p.standbyListCyclesTTL) {
                    if (standbyListMap.has(joinRequest.nodeInfo.publicKey) === false) {
                        skipped++;
                        continue;
                    }
                    if (record.standbyRefresh.includes(joinRequest.nodeInfo.publicKey)) {
                        skipped++;
                        continue;
                    }
                    record.standbyRemove.push(joinRequest.nodeInfo.publicKey);
                    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `removed standby node that did not refresh`);
                    /* prettier-ignore */ if (logger_1.logFlags.verbose)
                        console.log(`removed standby node that did not refresh`);
                    // this is a relic from the old code, but I suppose we can leave it in as an upper limit per cycle
                    standbyRemoved_Age++;
                    if (standbyRemoved_Age >= Context_1.config.p2p.standbyListMaxRemoveTTL) {
                        break;
                    }
                }
            }
        }
        /*
        if (config.p2p.standbyAgeScrub) {
          // scrub the stanby list of nodes that have been in it too long.  > standbyListCyclesTTL num cycles
          for (const joinRequest of standbyList) {
            const maxAge = record.duration * config.p2p.standbyListCyclesTTL
            const key = joinRequest.nodeInfo.publicKey
            let lastStandbyTimerRefresh = joinRequest.nodeInfo.joinRequestTimestamp
            //check the refresh map to get a more up to date cycle age for this node
            if(standbyNodesRefresh.has(key)){
              lastStandbyTimerRefresh = standbyNodesRefresh.get(key)
            }
            if (record.start - lastStandbyTimerRefresh > maxAge) {
              if (standbyListMap.has(key) === false) {
                skipped++
                continue
              }
              record.standbyRemove.push(key)
              standbyRemoved_Age++
              if (standbyRemoved_Age >= config.p2p.standbyListMaxRemoveTTL) {
                break
              }
            }
          }
        }
        */
        if (Context_1.config.p2p.standbyVersionScrub) {
            for (const joinRequest of standbyList) {
                const key = joinRequest.nodeInfo.publicKey;
                if (standbyListMap.has(key) === false) {
                    skipped++;
                    continue;
                }
                const { canStay, reason } = Context_1.shardus.app.canStayOnStandby(joinRequest);
                if (canStay === false) {
                    record.standbyRemove.push(key);
                    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                        console.log(`join:updateRecord cycle number: ${record.counter} removed standby node ${key} reason: ${reason}`);
                    standbyRemoved_App++;
                    if (standbyRemoved_App >= Context_1.config.p2p.standbyListMaxRemoveTTL) {
                        break;
                    }
                }
            }
            if (Context_1.config.p2p.enableMaxStandbyCount) {
                // Calculate the effective standby list size after planned removals
                const effectiveStandbyListSize = standbyList.length - record.standbyRemove.length;
                // Check if the effective size exceeds the maximum allowed size
                if (effectiveStandbyListSize > Context_1.config.p2p.maxStandbyCount) {
                    // Calculate the number of nodes to remove to meet the size limit
                    const nodesToRemoveCount = effectiveStandbyListSize - Context_1.config.p2p.maxStandbyCount;
                    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                        console.log(`join:updateRecord cycle number: ${record.counter} removing ${nodesToRemoveCount} from standby list reason: standby node list size above maxStandbyCount of ${Context_1.config.p2p.maxStandbyCount}`);
                    // Convert record.standbyRemove array to a Set for faster lookup
                    const standbyRemoveSet = new Set(record.standbyRemove);
                    let removeCount = 0;
                    // Select the most recent nodes for removal
                    for (let i = standbyList.length - 1; i >= 0 && removeCount < nodesToRemoveCount; i--) {
                        const node = standbyList[i];
                        // check if the node is not already marked for removal
                        if (!standbyRemoveSet.has(node.nodeInfo.publicKey)) {
                            // If not, mark the node for removal
                            record.standbyRemove.push(node.nodeInfo.publicKey); // You might maintain this as an array for other uses
                            standbyRemoveSet.add(node.nodeInfo.publicKey); // Also add to the set for consistent and quick lookups
                            removeCount++;
                        }
                    }
                }
            }
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:updateRecord cycle number: ${record.counter} skipped: ${skipped} removedTTLCount: ${standbyRemoved_Age}  removed list: ${record.standbyRemove} `);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                (0, v2_1.debugDumpJoinRequestList)(standbyList, `join.updateRecord: last-hashed ${record.counter}`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                (0, v2_1.debugDumpJoinRequestList)(Array.from((0, v2_1.getStandbyNodesInfoMap)().values()), `join.updateRecord: standby-map ${record.counter}`);
        }
        record.standbyAdd.sort((a, b) => (a.nodeInfo.publicKey > b.nodeInfo.publicKey ? 1 : -1));
        record.standbyRemove.sort();
        record.standbyRefresh.sort();
        record.finishedSyncing.sort();
        //let standbyActivated = false
        // ... then add any standby nodes that are now allowed to join
        const selectedPublicKeys = (0, select_1.drainSelectedPublicKeys)();
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal && logger_1.logFlags.console)
            console.log('selected public keys', selectedPublicKeys);
        record.joinedConsensors = record.joinedConsensors || [];
        for (const publicKey of selectedPublicKeys) {
            const standbyInfo = (0, v2_1.getStandbyNodesInfoMap)().get(publicKey);
            // the standbyInfo *should* exist, but if it doesn't, continue without
            // adding its node
            if (!standbyInfo)
                continue;
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal && logger_1.logFlags.console)
                console.log('selected standby node', standbyInfo);
            // prepare information for the joinedConsensors list
            const { nodeInfo, cycleMarker: cycleJoined } = standbyInfo;
            const id = computeNodeId(nodeInfo.publicKey, standbyInfo.cycleMarker);
            const counterRefreshed = record.counter;
            if (Context_1.config.p2p.addFoundationNodeAttribute) {
                const foundationNode = standbyInfo.appJoinData.adminCert?.goldenTicket === true ? true : false;
                record.joinedConsensors.push({ ...nodeInfo, cycleJoined, counterRefreshed, id, foundationNode });
            }
            else {
                record.joinedConsensors.push({ ...nodeInfo, cycleJoined, counterRefreshed, id });
            }
        }
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            console.log(`standbyRemoved_Age: ${standbyRemoved_Age} standbyRemoved_App: ${standbyRemoved_App}`);
        record.joinedConsensors.sort();
        // add unjoining nodes to the standbyRemove field ...
        // unless they were selected last cycle, in which case they are placed in removed
        for (const signedUnjoinRequest of txs.standbyRemove) {
            const nodeIfSelectedLastCycle = CycleChain.newest.joinedConsensors.find((node) => node.publicKey === signedUnjoinRequest.publicKey);
            const nodeIfSelectedThisCycle = record.joinedConsensors.find((node) => node.publicKey === signedUnjoinRequest.publicKey);
            if (nodeIfSelectedLastCycle) {
                record.apoptosized.push(nodeIfSelectedLastCycle.id);
                nestedCounters_1.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);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `node that requested to unjoin but was selected to go active was added to apoptosized`);
            }
            else {
                record.standbyRemove.push(signedUnjoinRequest.publicKey);
            }
        }
        if (CycleCreator.currentQuarter === 3) {
            // transfer join requests received this cycle to queue for gossipping next cycle
            queuedJoinRequestsForGossip = queuedReceivedJoinRequests;
            queuedReceivedJoinRequests = [];
        }
    }
    else {
        // old protocol handling
        record.joinedConsensors = txs.standbyAdd
            .map((joinRequest) => {
            const { nodeInfo, cycleMarker: cycleJoined } = joinRequest;
            const id = computeNodeId(nodeInfo.publicKey, cycleJoined);
            const counterRefreshed = record.counter;
            // const foundationNode = false
            // return { ...nodeInfo, cycleJoined, counterRefreshed, id, foundationNode }
            if (Context_1.config.p2p.addFoundationNodeAttribute) {
                const foundationNode = false;
                return { ...nodeInfo, cycleJoined, counterRefreshed, id, foundationNode };
            }
            else {
                return { ...nodeInfo, cycleJoined, counterRefreshed, id };
            }
        })
            .sort();
        /* prettier-ignore */ if (logger_1.logFlags && logger_1.logFlags.verbose)
            console.log("new desired count: ", record.desired);
    }
}
exports.updateRecord = updateRecord;
function parseRecord(record) {
    const added = record.joinedConsensors;
    const removed = [];
    const finishedSyncing = record.finishedSyncing;
    for (const node of added) {
        node.syncingTimestamp = record.start;
        // finally, remove the node from the standby list
        const publicKey = node.publicKey;
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            console.log(`join:parseRecord node-selcted cycle: ${record.counter} removed standby node ${publicKey}`);
        (0, unjoin_1.deleteStandbyNode)(publicKey);
    }
    if (added.length > 0) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            (0, v2_1.debugDumpJoinRequestList)(Array.from((0, v2_1.getStandbyNodesInfoMap)().values()), `join.parseRecord: standby-map ${record.counter} some activated:${record.counter}`);
    }
    // update self status?
    const updated = [];
    if (record.startedSyncing.includes(Self.id)) {
        Self.updateNodeState(lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING);
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            console.log(`join:parseRecord node-selcted cycle: ${record.counter} updated self to syncing`);
    }
    for (const nodeId of record.startedSyncing) {
        if (NodeList.selectedById.has(nodeId)) {
            updated.push({
                id: nodeId,
                status: lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
            });
        }
    }
    if (finishedSyncing.includes(Self.id)) {
        Self.updateNodeState(lib_types_1.P2P.P2PTypes.NodeStatus.READY);
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            console.log(`join:parseRecord node-selcted cycle: ${record.counter} updated self to ready`);
    }
    for (const node of finishedSyncing) {
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            console.log(`parseRecord:FinishedSyncing: ${node}`);
        updated.push({
            id: node,
            status: lib_types_1.P2P.P2PTypes.NodeStatus.READY,
            readyTimestamp: record.start,
        });
    }
    const standbyMap = (0, v2_1.getStandbyNodesInfoMap)();
    for (const refreshedPubKey of record.standbyRefresh) {
        if (standbyMap.has(refreshedPubKey) === false)
            continue;
        const refreshedStandbyInfo = standbyMap.get(refreshedPubKey);
        refreshedStandbyInfo.nodeInfo.refreshedCounter = record.counter;
        v2_1.standbyNodesInfoHashes.set(refreshedPubKey, Context_1.crypto.hash(refreshedStandbyInfo));
    }
    const addedIds = added.map((node) => node.id);
    if (Context_1.config.p2p.hardenNewSyncingProtocol) {
        for (const [nodeId, cycleNumber] of NodeList.selectedById) {
            if (addedIds.includes(nodeId)) {
                // this if condition seems like its a relic of an old way I was implementing this
                // dont't want to remove it in the middle of a release, but we probably should later
                //
                // do nothing. the node was just added and isn't in the nodelist yet
                continue;
            }
            else if (record.counter > cycleNumber + Context_1.config.p2p.cyclesToWaitForSyncStarted) {
                // if the node has just appeared in startedSyncing or finishedSyncing, do nothing
                if (record.startedSyncing.includes(nodeId))
                    continue;
                if (record.finishedSyncing.includes(nodeId))
                    continue;
                if (record.lostAfterSelection.includes(nodeId))
                    continue;
                // add node to lostAfterSelection to be added to the cycle record next cycle
                syncStarted_1.lostAfterSelection.push(nodeId);
            }
        }
        // add nodes to be removed if they appear in lostAfterSelection
        for (const nodeId of record.lostAfterSelection) {
            removed.push(nodeId);
        }
        return {
            added,
            removed,
            updated,
        };
    }
    else {
        for (const [nodeId, cycleNumber] of NodeList.selectedById) {
            if (addedIds.includes(nodeId)) {
                // do nothing. the node was just added and isn't in the nodelist yet
                continue;
            }
            else if (record.counter > cycleNumber + Context_1.config.p2p.cyclesToWaitForSyncStarted) {
                syncStarted_1.lostAfterSelection.push(nodeId);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `added node to lostAfterSelection`);
                /* prettier-ignore */ if (logger_1.logFlags.verbose)
                    console.log(`added node to lostAfterSelection`);
            }
        }
        return {
            added,
            removed: [...syncStarted_1.lostAfterSelection],
            updated,
        };
    }
}
exports.parseRecord = parseRecord;
/** Not used by Join */
function sendRequests() {
    if (queuedStartedSyncingId) {
        const syncStartedTx = Context_1.crypto.sign({
            nodeId: queuedStartedSyncingId,
            cycleNumber: CycleChain.newest.counter,
        });
        queuedStartedSyncingId = undefined;
        if ((0, syncStarted_1.addSyncStarted)(syncStartedTx).success === true) {
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending sync-started gossip to network`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: sending sync-started gossip to network`);
            Comms.sendGossip('gossip-sync-started', syncStartedTx, '', null, nodeListFromStates([
                lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
                lib_types_1.P2P.P2PTypes.NodeStatus.READY,
                lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
            ]), true);
        }
        else {
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to add our own sync-started message`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: failed to add our own sync-started message`);
        }
    }
    if (queuedFinishedSyncingId) {
        const syncFinishedTx = Context_1.crypto.sign({
            nodeId: queuedFinishedSyncingId,
            cycleNumber: CycleChain.newest.counter,
        });
        queuedFinishedSyncingId = undefined;
        if ((0, syncFinished_1.addFinishedSyncing)(syncFinishedTx).success === true) {
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending sync-finished gossip to network`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: sending sync-finished gossip to network`);
            Comms.sendGossip('gossip-sync-finished', syncFinishedTx, '', null, nodeListFromStates([
                lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
                lib_types_1.P2P.P2PTypes.NodeStatus.READY,
                lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
            ]), true);
        }
        else {
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to add our own sync-finished message`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: failed to add our own sync-finished message`);
        }
    }
    if (queuedStandbyRefreshPubKeys.length > 0) {
        for (const standbyRefreshPubKey of queuedStandbyRefreshPubKeys) {
            const standbyRefreshTx = Context_1.crypto.sign({
                publicKey: standbyRefreshPubKey,
                cycleNumber: CycleChain.newest.counter,
            });
            const standbyRefreshResult = (0, standbyRefresh_1.addStandbyRefresh)(standbyRefreshTx);
            if (standbyRefreshResult.success === true) {
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending standby-refresh gossip to network`);
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    console.log(`join:sendRequests: sending standby-refresh gossip to network`);
                Comms.sendGossip('gossip-standby-refresh', standbyRefreshTx, '', null, nodeListFromStates([
                    lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
                    lib_types_1.P2P.P2PTypes.NodeStatus.READY,
                    lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
                ]), true);
            }
            else {
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to add standby-refresh message`);
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    console.log(`join:sendRequests: failed to add standby-refresh message`);
            }
        }
        queuedStandbyRefreshPubKeys = [];
    }
    if (queuedJoinRequestsForGossip.length > 0) {
        for (const joinRequest of queuedJoinRequestsForGossip) {
            // 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.
            // 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
            // re-compute selection number for the join request for the current cycle
            const selectionNumResult = computeSelectionNum(joinRequest);
            if (selectionNumResult.isErr()) {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to compute selection number`);
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    console.error(`join:sendRequests: failed to compute selection number for node ${joinRequest.nodeInfo.publicKey}:`, JSON.stringify(selectionNumResult.error));
                return;
            }
            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) {
                // since join request was already validated last cycle, we can just set seen to true directly
                seen.add(joinRequest.nodeInfo.publicKey);
                (0, v2_1.saveJoinRequest)(joinRequest);
            }
            const signedObjectWithJoinRequest = Context_1.crypto.sign({ joinRequest, sign: null });
            Comms.sendGossip('gossip-valid-join-requests', signedObjectWithJoinRequest, '', null, nodeListFromStates([
                lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
                lib_types_1.P2P.P2PTypes.NodeStatus.READY,
                lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
            ]), true);
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: saved join request and gossiped to network`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: saved join request and gossiped to network`);
        }
        queuedJoinRequestsForGossip = [];
    }
    if (queuedUnjoinRequestsForThisCycle.length > 0) {
        for (const unjoinRequest of queuedUnjoinRequestsForThisCycle) {
            // TO-DO: possible optimization
            // since we already validated the unjoin request in the unjoin handler, we could just gossip it here. there would probably not be any issue with that
            // the only real thing to consider is the edge case where a node may be removed from the standby list due to standby refresh in the interval between
            // when the initiator node validated the unjoin request and when the receiving nodes validate it. in this case, the receiving nodes would not validate
            // the unjoin request and add it to their cycle record but the initiator node would, which would cause a small amount of cycle record OOS
            const processResult = (0, unjoin_1.processNewUnjoinRequest)(unjoinRequest);
            if (processResult.isErr()) {
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: failed to process unjoin request; failed to process unjoin request`);
                /* prettier-ignore */ if (logger_1.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;
            }
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:sendRequests: sending unjoin gossip to network`);
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                console.log(`join:sendRequests: sending unjoin gossip to network`);
            Comms.sendGossip('gossip-unjoin', unjoinRequest, '', null, nodeListFromStates([
                lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
                lib_types_1.P2P.P2PTypes.NodeStatus.READY,
                lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
            ]), true);
        }
    }
    return;
}
exports.sendRequests = sendRequests;
/** Not used by Join */
function queueRequest() {
    return;
}
exports.queueRequest = queueRequest;
async function queueStartedSyncingRequest() {
    if (Self.isFirst === false && Context_1.config.debug.startedSyncingDelay > 0) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:queueStartedSyncingRequest: delaying started syncing by ${Context_1.config.debug.startedSyncingDelay} seconds`);
        await utils.sleep(Context_1.config.debug.startedSyncingDelay * 1000);
    }
    queuedStartedSyncingId = Self.id;
}
exports.queueStartedSyncingRequest = queueStartedSyncingRequest;
async function queueFinishedSyncingRequest() {
    if (Active_1.neverGoActive)
        return;
    if (Self.isFirst === false && Context_1.config.debug.finishedSyncingDelay > 0) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join:queueFinishedSyncingRequest: delaying finished syncing by ${Context_1.config.debug.finishedSyncingDelay} seconds`);
        await utils.sleep(Context_1.config.debug.finishedSyncingDelay * 1000);
    }
    queuedFinishedSyncingId = Self.id;
    exports.finishedSyncingCycle = CycleCreator.currentCycle;
}
exports.queueFinishedSyncingRequest = queueFinishedSyncingRequest;
function queueStandbyRefreshRequest(publicKey) {
    queuedStandbyRefreshPubKeys.push(publicKey);
}
exports.queueStandbyRefreshRequest = queueStandbyRefreshRequest;
function queueJoinRequest(joinRequest) {
    queuedReceivedJoinRequests.push(joinRequest);
}
exports.queueJoinRequest = queueJoinRequest;
function queueUnjoinRequest(unjoinRequest) {
    queuedUnjoinRequestsForNextCycle.push(unjoinRequest);
}
exports.queueUnjoinRequest = queueUnjoinRequest;
/** Module Functions */
async function createJoinRequest(cycleRecord) {
    const cycleMarker = cycleRecord.previous ?? '0'.repeat(64);
    // Build and return a join request
    const nodeInfo = Self.getThisNodeInfo();
    nodeInfo.refreshedCounter = cycleRecord.counter ?? 0;
    // TO-DO: Think about if the selection number still needs to be signed
    const proofOfWork = {
        compute: await Context_1.crypto.getComputeProofOfWork(cycleMarker, Context_1.config.p2p.difficulty),
    };
    const joinReq = {
        nodeInfo,
        cycleMarker,
        proofOfWork: lib_types_2.Utils.safeStringify(proofOfWork),
        version: package_json_1.version,
        selectionNum: undefined,
    };
    if (typeof Context_1.shardus.app.getJoinData === 'function') {
        try {
            const appJoinData = Context_1.shardus.app.getJoinData();
            if (appJoinData) {
                joinReq['appJoinData'] = appJoinData;
            }
        }
        catch (e) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn(`shardus.app.getJoinData failed due to ${utils.formatErrorMessage(e)}`);
            return;
        }
    }
    const signedJoinReq = Context_1.crypto.sign(joinReq);
    if (logger_1.logFlags.p2pNonFatal)
        info(`Join request created... Join request: ${lib_types_2.Utils.safeStringify(signedJoinReq)}`);
    return signedJoinReq;
}
exports.createJoinRequest = createJoinRequest;
/**
 * Processes a join request by validating the joining node's information,
 * ensuring compatibility with the network's version, checking cryptographic signatures, and responding
 * with either an acceptance or rejection based on the provided criteria.
 *
 * This function serves as a critical part of the network's security, allowing only valid and authenticated
 * nodes to participate in the network activities.
 *
 * @function
 * @param {P2P.JoinTypes.JoinRequest} joinRequest - The request object containing information about the joining node.
 * @returns {JoinRequestResponse} The result of the join request, with details about acceptance or rejection.
 * @throws {Error} Throws an error if the validation of the join request fails.
 *
 *
 */
function addJoinRequest(joinRequest) {
    if (Self.p2pIgnoreJoinRequests === true) {
        if (logger_1.logFlags.p2pNonFatal)
            info(`Join request ignored. p2pIgnoreJoinRequests === true`);
        return {
            success: false,
            fatal: false,
            reason: `Join request ignored. p2pIgnoreJoinRequests === true`,
        };
    }
    const response = validateJoinRequest(joinRequest);
    if (response) {
        return response;
    }
    // validation has passed so far
    if (logger_1.logFlags.p2pNonFatal)
        info(`Got join request for ${joinRequest.nodeInfo.externalIp}:${joinRequest.nodeInfo.externalPort}`);
    if (!Context_1.config.p2p.useJoinProtocolV2) {
        return decideNodeSelection(joinRequest);
    }
    else {
        return {
            success: false,
            reason: 'Join Protocol v2 is enabled and selection will happen eventually. Wait your turn!',
            fatal: false,
        };
    }
}
exports.addJoinRequest = addJoinRequest;
async function firstJoin() {
    let marker;
    let record;
    if (CycleChain.newest) {
        // TODO: add extra check if newest.mode === 'shutdown' later after shutdown is implemented
        // If there is a cycle provided by the archiver, use it
        marker = CycleChain.newest['previous'];
        record = CycleChain.newest;
    }
    else {
        // Create join request from 000... cycle marker
        const zeroMarker = '0'.repeat(64);
        const zeroRecord = {};
        marker = zeroMarker;
        record = zeroRecord;
    }
    const request = await createJoinRequest(record);
    // Add own join request
    utils.insertSorted(requests, request);
    if (Context_1.config.p2p.useJoinProtocolV2) {
        (0, v2_1.saveJoinRequest)(request, true);
        (0, select_1.forceSelectSelf)();
    }
    // Return node ID
    return computeNodeId(Context_1.crypto.keypair.publicKey, marker);
}
exports.firstJoin = firstJoin;
// export async function submitJoin(
//   nodes: P2P.P2PTypes.Node[],
//   joinRequest: P2P.JoinTypes.JoinRequest & P2P.P2PTypes.SignedObject
// ): Promise<void> {
//   // Send the join request to a handful of the active node all at once
//   const selectedNodes = utils.getRandom(nodes, Math.min(nodes.length, 5))
//   const promises = []
//   if (logFlags.p2pNonFatal) info(`Sending join request to ${selectedNodes.map((n) => `${n.ip}:${n.port}`)}`)
//   // 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
//     }
//   }
//   nestedCountersInstance.countEvent('p2p', `join-allow-bogon-submit:${allowBogon}`)
//   //Check for bad IPs before a join request is sent out
//   if (config.p2p.rejectBogonOutboundJoin || config.p2p.forceBogonFilteringOn) {
//     if (allowBogon === false) {
//       if (isBogonIP(joinRequest.nodeInfo.externalIp)) {
//         throw new Error(`Fatal: Node cannot join with bogon external IP: ${joinRequest.nodeInfo.externalIp}`)
//       }
//     } else {
//       //even if not checking bogon still reject other invalid IPs that would be unusable
//       if (isInvalidIP(joinRequest.nodeInfo.externalIp)) {
//         throw new Error(
//           `Fatal: Node cannot join with invalid external IP: ${joinRequest.nodeInfo.externalIp}`
//         )
//       }
//     }
//   }
//   for (const node of selectedNodes) {
//     try {
//       promises.push(http.post(`${node.ip}:${node.port}/join`, joinRequest))
//     } catch (err) {
//       throw new Error(
//         `Fatal: submitJoin: Error posting join request to ${node.ip}:${node.port}: Error: ${err}`
//       )
//     }
//   }
//   try {
//     const responses = await Promise.all(promises)
//     for (const res of responses) {
//       mainLogger.info(`Join Request Response: ${JSON.stringify(res)}`)
//       if (res.fatal) {
//         throw new Error(`Fatal: Join request Reason: ${res.reason}`)
//       }
//     }
//   } catch (e) {
//     if (e instanceof HTTPError) {
//       throw new Error(`submitJoin: Error posting join request: ${JSON.stringify(e.response)}`)
//     } else {
//       throw new Error(`submitJoin: Error posting join request: ${e}`)
//     }
//   }
//   hasSubmittedJoinRequest = true
//   if (config.p2p.useJoinProtocolV2) {
//     updateNodeState(P2P.P2PTypes.NodeStatus.STANDBY)
//   }
// }
async function submitJoinV2(nodes, joinRequest) {
    // Send the join request to a handful of the active node all at once
    const selectedNodes = utils.getRandom(nodes, Math.min(nodes.length, 5));
    const promises = [];
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        info(`submitJoinV2: selectedNodes: Sent join request to ${selectedNodes.map((n) => `${n.ip}:${n.port}`)}`);
    // Check if network allows bogon IPs, set our own flag accordingly
    if (Context_1.config.p2p.dynamicBogonFiltering && Context_1.config.p2p.forceBogonFilteringOn === false) {
        if (nodes.some((node) => (0, checkIP_1.isBogonIP)(node.ip))) {
            allowBogon = true;
        }
    }
    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-allow-bogon-submit:${allowBogon}`);
    //Check for bad IPs before a join request is sent out
    if (Context_1.config.p2p.rejectBogonOutboundJoin || Context_1.config.p2p.forceBogonFilteringOn) {
        if (allowBogon === false) {
            if ((0, checkIP_1.isBogonIP)(joinRequest.nodeInfo.externalIp)) {
                throw new Error(`Fatal: Node cannot join with bogon external IP: ${joinRequest.nodeInfo.externalIp}`);
            }
        }
        else {
            //even if not checking bogon still reject other invalid IPs that would be unusable
            if ((0, checkIP_1.isInvalidIP)(joinRequest.nodeInfo.externalIp)) {
                throw new Error(`Fatal: Node cannot join with invalid external IP: ${joinRequest.nodeInfo.externalIp}`);
            }
        }
    }
    for (const node of selectedNodes) {
        try {
            //set timeout to 5000 for debugging
            const postPromise = http.post(`${node.ip}:${node.port}/join`, joinRequest, false, 5000);
            promises.push(postPromise);
        }
        catch (err) {
            //seems like this is eleveated too high... can it throw a wrench in the join process..
            // throw new Error(
            //   `Fatal: submitJoin: Error posting join request to ${node.ip}:${node.port}: Error: ${err}`
            // )
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                error(`submitJoin: Error posting join request to ${node.ip}:${node.port}: Error: ${utils.formatErrorMessage(err)}`);
        }
    }
    const responses = await Promise.all(promises);
    const errs = [];
    let goodCount = 0;
    let unreachable = 0;
    for (const res of responses) {
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            info(`Join Request Response: ${lib_types_2.Utils.safeStringify(res)}`);
        if (res && res.fatal) {
            errs.push(res);
            //special case for unreachable because the very nature of it means some timeouts could prevent us from hearing back
            if (res.reason && res.reason.startsWith('IP or Port is not reachable')) {
                unreachable++;
            }
        }
        if (res && res.success === true) {
            goodCount++;
        }
    }
    if (unreachable >= 2) {
        throw new Error(`Fatal: submitJoin: our node was reported to not be reachable by 2 or more nodes ${unreachable}`);
    }
    if (errs.length >= responses.length) {
        throw new Error(`Fatal: submitJoin: All join requests failed: ${errs.map((e) => e.reason).join(', ')}`);
    }
    //it is important to check that we go one good response.  this was the past cause of nodes giving up
    if (goodCount === 0) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `submitJoin: no join success repsonses`);
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            info(`submitJoin: no join success repsonses: ${responses.map((e) => e.reason).join(', ')}`);
        //throw new Error(`submitJoin: no join success repsonses: ${responses.map((e) => e.reason).join(', ')}`)
    }
    //does not seem to chekc the join response. assumes fatal
}
exports.submitJoinV2 = submitJoinV2;
async function fetchJoined(activeNodes) {
    const queryFn = async (node) => {
        const publicKey = Context_1.crypto.keypair.publicKey;
        const res = await http.get(`${node.ip}:${node.port}/joined/${publicKey}`);
        return res;
    };
    try {
        const { topResult: response } = await (0, Utils_1.robustQuery)(activeNodes, queryFn);
        if (!response)
            return;
        if (!response.node)
            return;
        let err = utils.validateTypes(response, { node: 'o' });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('fetchJoined invalid response response.node' + err);
            return;
        }
        err = (0, utils_1.validateTypes)(response.node, { id: 's' });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('fetchJoined invalid response response.node.id' + err);
            return;
        }
        return response.node.id;
    }
    catch (err) {
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn('Self: fetchNodeId: robustQuery failed: ', err);
    }
}
exports.fetchJoined = fetchJoined;
async function fetchJoinedV2(activeNodes) {
    const queryFn = async (node) => {
        const publicKey = Context_1.crypto.keypair.publicKey;
        const res = await http.get(`${node.ip}:${node.port}/joinedV2/${publicKey}`);
        return res;
    };
    try {
        const { topResult: response } = await (0, Utils_1.robustQuery)(activeNodes, queryFn);
        if (!response)
            return;
        if (!response.id) {
            return { id: undefined, isOnStandbyList: response.isOnStandbyList };
        }
        let err = utils.validateTypes(response, { id: 's' });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('fetchJoined invalid response response.id' + err);
            return;
        }
        err = (0, utils_1.validateTypes)(response, { isOnStandbyList: 'b' });
        if (err) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('fetchJoined invalid response response.isOnStandbyList' + err);
            return;
        }
        return { id: response.id, isOnStandbyList: response.isOnStandbyList };
    }
    catch (err) {
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn('Self: fetchNodeId: robustQuery failed: ', utils.formatErrorMessage(err));
    }
}
exports.fetchJoinedV2 = fetchJoinedV2;
/**
 * Returns a `JoinRequestResponse` object if the given `joinRequest` is invalid or rejected for any reason.
 */
function validateJoinRequest(joinRequest) {
    // 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));
}
exports.validateJoinRequest = validateJoinRequest;
/**
 * Returns an error response if the given `joinRequest` is invalid or rejected
 * based on its IP address.
 */
function validateJoinRequestHost(joinRequest) {
    try {
        //test or bogon IPs and reject the join request if they appear
        if (allowBogon === false) {
            if ((0, checkIP_1.isBogonIP)(joinRequest.nodeInfo.externalIp)) {
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    warn('Got join request from Bogon IP');
                nestedCounters_1.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 ((0, checkIP_1.isInvalidIP)(joinRequest.nodeInfo.externalIp)) {
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    warn('Got join request from invalid reserved IP');
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-reserved`);
                return {
                    success: false,
                    reason: `Bad ip, reserved ip not accepted`,
                    fatal: true,
                };
            }
        }
    }
    catch (er) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-bogon-ex:${er}`);
    }
    return null;
}
function computeNodeId(publicKey, cycleMarker) {
    const obj = { publicKey, cycleMarker };
    const nodeId = Context_1.crypto.hash(obj);
    if (logger_1.logFlags.p2pNonFatal) {
        info(`Node ID computation: publicKey: ${publicKey}, cycleMarker: ${cycleMarker}`);
        info(`Node ID is: ${nodeId}`);
    }
    return nodeId;
}
exports.computeNodeId = computeNodeId;
/**
 * 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`.
 */
function verifyJoinRequestTypes(joinRequest) {
    // Validate joinReq
    let err = utils.validateTypes(joinRequest, {
        cycleMarker: 's',
        nodeInfo: 'o',
        sign: 'o',
        version: 's',
    });
    if (err) {
        /* prettier-ignore */ if (logger_1.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 (logger_1.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 (logger_1.logFlags.p2pNonFatal)
            warn('join bad joinRequest.sign ' + err);
        return {
            success: false,
            reason: 'Bad signature object structure within join request',
            fatal: true,
        };
    }
    return null;
}
exports.verifyJoinRequestTypes = verifyJoinRequestTypes;
/**
 * 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) {
    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 (logger_1.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 (logger_1.logFlags.p2pNonFatal)
            info(message, lib_types_2.Utils.safeStringify(NodeList.byIpPort.get(ipPort)));
        if (logger_1.logFlags.p2pNonFatal)
            nestedCounters_1.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) {
    if ((0, checkIP_1.isIPv6)(joinRequest.nodeInfo.externalIp)) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            warn('Got join request from IPv6');
        nestedCounters_1.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`.
 *
 * Note: 'null' means the version is valid to join or that we are not checking versions.
 */
function validateVersion(joinRequestVersion) {
    // If version checking is not enabled, return null
    if (Context_1.config.p2p.checkVersion !== true) {
        return null;
    }
    const versionValidationResult = utils.meetsMinimumVersion(package_json_1.version, joinRequestVersion);
    if (versionValidationResult === utils.VersionValidationResult.Success) {
        return null;
    }
    let errorMessage = '';
    switch (versionValidationResult) {
        case utils.VersionValidationResult.ComparisonFailed:
            errorMessage = `Version too old - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
        case utils.VersionValidationResult.ControlVersionParseFailure:
            errorMessage = `Error parsing core version - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
        case utils.VersionValidationResult.InvalidControlVersion:
            errorMessage = `Invalid core version - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
        case utils.VersionValidationResult.TestVersionParseFailure:
            errorMessage = `Error parsing join request version - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
        case utils.VersionValidationResult.InvalidTestVersion:
            errorMessage = `Invalid join request version - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
        default:
            errorMessage = `Unexpected validation result (${versionValidationResult}) - Core: ${package_json_1.version}, Join Request: ${joinRequestVersion}`;
            break;
    }
    warn(errorMessage);
    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-version-validation`);
    return { success: false, reason: errorMessage, 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) {
    //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 }}`);
        nestedCounters_1.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) {
    // Check if this node has already been seen this cycle
    if (seen.has(publicKey)) {
        if (logger_1.logFlags.p2pNonFatal)
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-skip-seen-pubkey`);
        if (logger_1.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) {
    //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) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-not-meet-lowerbound`);
        /* prettier-ignore */ if (logger_1.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) {
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-skip-timestamp-beyond-upperbound`);
        /* prettier-ignore */ if (logger_1.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
 * is the public key of the node that sent the join request.
 */
function getSelectionKey(joinRequest) {
    if (typeof Context_1.shardus.app.validateJoinRequest === 'function') {
        try {
            mode = CycleChain.newest.mode || null;
            const validationResponse = Context_1.shardus.app.validateJoinRequest(joinRequest, mode, CycleChain.newest, 
            // use minNodes instead of baselineNodes here since validateJoinRequest's arguemnt minNodes is used check for adminCert when not in processing mode
            Context_1.config.p2p.minNodes);
            if (validationResponse.success !== true) {
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    error(`Validation of join request data is failed due to ${validationResponse.reason || 'unknown reason'}`);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-dapp`);
                return (0, neverthrow_1.err)({
                    success: validationResponse.success,
                    reason: validationResponse.reason,
                    fatal: validationResponse.fatal,
                });
            }
            if (typeof validationResponse.data === 'string') {
                return (0, neverthrow_1.ok)(validationResponse.data);
            }
        }
        catch (e) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`shardus.app.validateJoinRequest failed due to ${utils.formatErrorMessage(e)}`);
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-ex ${e.message}`);
            return (0, neverthrow_1.err)({
                success: false,
                reason: `Could not validate join request due to Error`,
                fatal: true,
            });
        }
    }
    return (0, neverthrow_1.ok)(joinRequest.nodeInfo.publicKey);
}
function verifyJoinRequestSignature(joinRequest) {
    if (!Context_1.crypto.verify(joinRequest, joinRequest.nodeInfo.publicKey)) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            warn('join bad sign ' + lib_types_2.Utils.safeStringify(joinRequest));
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-reject-bad-sign`);
        return {
            success: false,
            reason: 'Bad signature',
            fatal: true,
        };
    }
    return null;
}
exports.verifyJoinRequestSignature = verifyJoinRequestSignature;
/**
 * Computes a selection number given a join request.
 */
function computeSelectionNum(joinRequest) {
    // get the selection key
    const selectionKeyResult = getSelectionKey(joinRequest);
    if (selectionKeyResult.isErr()) {
        return (0, neverthrow_1.err)(selectionKeyResult.error);
    }
    const selectionKey = selectionKeyResult.value;
    // calculate the selection number based on the selection key
    const obj = {
        cycleNumber: CycleChain.newest.counter,
        selectionKey,
    };
    const selectionNum = Context_1.crypto.hash(obj);
    return (0, neverthrow_1.ok)(selectionNum);
}
exports.computeSelectionNum = computeSelectionNum;
/**
 * Selects the join request's node to join the network, or doesn't.
 * Returns a `JoinRequestResponse` signifying whether the join request was
 * accepted or rejected.
 *
 * This is the logic used in Join Protocol v1.
 */
function decideNodeSelection(joinRequest) {
    // Compute how many join request to accept
    let toAccept = calculateToAccept(); // I think we can remove this line; as toAccept would be overriden by calculateToAcceptV2 in the next line
    /* prettier-ignore */ if (Context_1.config.debug.verboseNestedCounters)
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `results of calculateToAccept: toAccept: ${toAccept}`);
    /* prettier-ignore */ if (logger_1.logFlags && logger_1.logFlags.verbose)
        console.log("results of calculateToAccept: ", toAccept);
    const { add, remove } = (0, ModeSystemFuncs_1.calculateToAcceptV2)(CycleChain.newest);
    /* prettier-ignore */ if (Context_1.config.debug.verboseNestedCounters)
        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `results of calculateToAcceptV2: add: ${add}, remove: ${remove}`);
    /* prettier-ignore */ if (logger_1.logFlags && logger_1.logFlags.verbose) {
        console.log(`results of calculateToAcceptV2: add: ${add}, remove: ${remove}`);
    }
    toAccept = add;
    // Check if we are better than the lowest selectionNum
    const last = requests.length > 0 ? requests[requests.length - 1] : undefined;
    /*
      (This is implemented on 22/12/2021 in commit 9bf8b052673d03e7b7ba0e36321bb8d2fee5cc37)
      To calculate selectionNumber, we now use the hash of selectionKey and cycle number
      Selection key is provided by the application , and we can hash that with the cycle number.
      For example the application may want to use the staking address or the POW.
      It should be something that the node cannot easily change to
      guess a high selection number. If we generate a network
      random number we have to be careful that a node inside the network
      does not have an advantage by having access to this info and
      is able to create a stronger selectionNum. If no selectionKey is provided,
      joining node public key and cycle number are hashed to calculate selectionNumber.
    */
    const selectionNumResult = computeSelectionNum(joinRequest);
    if (selectionNumResult.isErr())
        return selectionNumResult.error;
    const selectionNum = selectionNumResult.value;
    if (last && requests.length >= toAccept && !Context_1.crypto.isGreaterHash(selectionNum, last.selectionNum)) {
        if (logger_1.logFlags.p2pNonFatal)
            info('Join request not better than lowest, not added.');
        if (logger_1.logFlags.p2pNonFatal)
            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `join-skip-hash-not-good-enough`);
        return {
            success: false,
            reason: 'Join request not better than lowest, not added',
            fatal: false,
        };
    }
    // TODO: call into application
    // ----- application should decide the ranking order of the join requests
    // ----- if hook doesn't exist, then we go with default order based on selection number
    // ----- hook signature = (currentList, newJoinRequest, numDesired) returns [newOrder, added]
    // ----- should create preconfigured hooks for adding POW, allowing join based on netadmin sig, etc.
    // Check the signature as late as possible since it is expensive
    const validationErr = verifyJoinRequestSignature(joinRequest);
    if (validationErr)
        return validationErr;
    // Insert sorted into best list if we made it this far
    utils.insertSorted(requests, { ...joinRequest, selectionNum }, (a, b) => a.selectionNum < b.selectionNum ? 1 : a.selectionNum > b.selectionNum ? -1 : 0);
    if (logger_1.logFlags.p2pNonFatal)
        info(`Added join request for ${joinRequest.nodeInfo.externalIp}:${joinRequest.nodeInfo.externalPort}`);
    // If we have > maxJoinedPerCycle requests, trim them down
    if (logger_1.logFlags.p2pNonFatal)
        info(`Requests: ${requests.length}, toAccept: ${toAccept}`);
    if (requests.length > toAccept) {
        const over = requests.length - toAccept;
        requests.splice(-over);
        //    info(`Over maxJoinedPerCycle; removed ${over} requests from join requests`)
    }
    return {
        success: true,
        reason: 'Join request accepted',
        fatal: false,
    };
}
function nodeListFromStates(states) {
    if (Self.isRestartNetwork)
        return NodeList.byIdOrder;
    const { NodeStatus } = lib_types_1.P2P.P2PTypes;
    const stateMappings = {
        [NodeStatus.ACTIVE]: NodeList.activeByIdOrder,
        [NodeStatus.READY]: NodeList.readyByTimeAndIdOrder,
        [NodeStatus.SYNCING]: NodeList.syncingByIdOrder,
        [NodeStatus.STANDBY]: NodeList.standbyByIdOrder,
        [NodeStatus.SELECTED]: NodeList.selectedByIdOrder,
    };
    let result = [];
    for (const state of states) {
        if (stateMappings[state]) {
            result = result.concat(stateMappings[state]);
        }
    }
    const self = NodeList.byJoinOrder.find((node) => node.id === Self.id);
    if (self && !result.some((node) => node.id === self.id)) {
        result.push(self);
    }
    return result;
}
exports.nodeListFromStates = nodeListFromStates;
function swapUnjoinRequestQueues() {
    queuedUnjoinRequestsForThisCycle = queuedUnjoinRequestsForNextCycle;
    queuedUnjoinRequestsForNextCycle = [];
}
exports.swapUnjoinRequestQueues = swapUnjoinRequestQueues;
function info(...msg) {
    const entry = `Join: ${msg.join(' ')}`;
    p2pLogger.info(entry);
}
function warn(...msg) {
    const entry = `Join: ${msg.join(' ')}`;
    p2pLogger.warn(entry);
}
exports.warn = warn;
function error(...msg) {
    const entry = `Join: ${msg.join(' ')}`;
    p2pLogger.error(entry);
}
exports.error = error;
//# sourceMappingURL=index.js.map