"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.getBestCycleCerts = exports.shutdown = exports.schedule = exports.calcIncomingTimes = exports.makeCycleMarker = exports.makeRecordZero = exports.startCycles = exports.init = exports.netConfig = exports.scaleFactorSyncBoost = exports.scaleFactor = exports.q1SendRequests = exports.nextQ1Start = exports.currentStart = exports.currentCycle = exports.currentQuarter = exports.submodules = exports.hasAlreadyEnteredProcessing = void 0;
const logger_1 = require("../logger");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const utils = __importStar(require("../utils"));
// don't forget to add new modules here
const Active = __importStar(require("./Active"));
const Apoptosis = __importStar(require("./Apoptosis"));
const Archivers = __importStar(require("./Archivers"));
const Comms = __importStar(require("./Comms"));
const Context_1 = require("./Context");
const CycleAutoScale = __importStar(require("./CycleAutoScale"));
const CycleChain = __importStar(require("./CycleChain"));
const Join = __importStar(require("./Join"));
const Lost = __importStar(require("./Lost"));
const NodeList = __importStar(require("./NodeList"));
const profiler_1 = require("../utils/profiler");
const Refresh = __importStar(require("./Refresh"));
const Rotation = __importStar(require("./Rotation"));
const SafetyMode = __importStar(require("./SafetyMode"));
const Modes = __importStar(require("./Modes"));
const Self = __importStar(require("./Self"));
const ServiceQueue = __importStar(require("./ServiceQueue"));
const LostArchivers = __importStar(require("./LostArchivers"));
const Utils_1 = require("./Utils");
const utils_1 = require("../utils");
const nestedCounters_1 = require("../utils/nestedCounters");
const lib_crypto_utils_1 = require("@shardeum-foundation/lib-crypto-utils");
const Sync_1 = require("./Sync");
const network_1 = require("../network");
const InternalRouteEnum_1 = require("../types/enum/InternalRouteEnum");
const TypeIdentifierEnum_1 = require("../types/enum/TypeIdentifierEnum");
const CompareCertResp_1 = require("../types/CompareCertResp");
const CompareCertReq_1 = require("../types/CompareCertReq");
const Helpers_1 = require("../types/ajv/Helpers");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const Helpers_2 = require("../types/Helpers");
const RequestErrorEnum_1 = require("../types/enum/RequestErrorEnum");
const ResponseError_1 = require("../types/ResponseError");
const lib_types_2 = require("@shardeum-foundation/lib-types");
const AJVSchemaEnum_1 = require("../types/enum/AJVSchemaEnum");
/** CONSTANTS */
const SECOND = 1000;
const BEST_CERTS_WANTED = 3;
const DESIRED_CERT_MATCHES = 3;
const MAX_CYCLES_TO_KEEP = 20;
/** STATE */
const filePath = path_1.default.join(process.cwd(), 'data-logs', 'cycleRecords1.txt');
let modeModuleMigrationApplied = false;
exports.hasAlreadyEnteredProcessing = false;
let p2pLogger;
let cycleLogger;
// don't forget to add new modules here
//   need to keep the Lost module after the Apoptosis module
exports.submodules = [
    Archivers,
    Join,
    Active,
    Rotation,
    Refresh,
    Apoptosis,
    Lost,
    // SafetyMode,
    Modes,
    CycleAutoScale,
    LostArchivers,
    ServiceQueue,
];
exports.currentQuarter = -1; // means we have not started creating cycles
exports.currentCycle = 0;
exports.currentStart = 0;
exports.nextQ1Start = 0;
exports.q1SendRequests = false; // if we are in q1 this lets us know if we can send requests also
exports.scaleFactor = 1;
exports.scaleFactorSyncBoost = 1;
exports.netConfig = {};
let createCycleTag = 0;
let madeCycle = false; // True if we successfully created the last cycle record, otherwise false
// not used anymore
//let madeCert = false // set to True after we make our own cert and try to gossip it
let txs;
let record;
let marker;
let cert;
let prevMarkerCached;
let prevMarkerLastUpdate = -1;
let bestRecord;
let bestMarker;
let bestCycleCert;
let bestCertScore;
const timers = {};
let cacheOfCycleCerts;
// Keeps track of the last saved record in the DB in order to update it
let lastSavedData;
// Keeps track of consecutive fetchLatestCycle fails to initiate apoptosis if it happens too many times
let fetchLatestRecordFails = 0;
const maxFetchLatestRecordFails = 5;
// const compareCertRoute: P2P.P2PTypes.InternalHandler<
//   CompareCertReq,
//   CompareCertRes,
//   P2P.NodeListTypes.Node['id']
// > = async (payload, respond, sender) => {
//   profilerInstance.scopedProfileSectionStart('compareCert')
//   await respond(compareCycleCertEndpoint(payload, sender, 'compareCycleCertEndpoint'))
//   profilerInstance.scopedProfileSectionEnd('compareCert')
// }
const gossipCertRoute = (payload, sender, tracker) => {
    gossipHandlerCycleCert(payload, sender, tracker);
};
const compareCertBinaryHandler = {
    name: InternalRouteEnum_1.InternalRouteEnum.binary_compare_cert,
    handler: async (payload, respond, header, sign) => {
        const route = InternalRouteEnum_1.InternalRouteEnum.binary_compare_cert;
        nestedCounters_1.nestedCountersInstance.countEvent('internal', route);
        profiler_1.profilerInstance.scopedProfileSectionStart(route);
        const errorHandler = (errorType, opts) => (0, Helpers_2.requestErrorHandler)(route, errorType, header, opts);
        try {
            let resp = { certs: [], record: null };
            const requestStream = (0, Helpers_2.getStreamWithTypeCheck)(payload, TypeIdentifierEnum_1.TypeIdentifierEnum.cCompareCertReq);
            if (!requestStream) {
                errorHandler(RequestErrorEnum_1.RequestErrorEnum.InvalidRequest);
                return respond((0, ResponseError_1.BadRequest)('Invalid CompareCert request stream'), ResponseError_1.serializeResponseError);
            }
            const req = (0, CompareCertReq_1.deserializeCompareCertReq)(requestStream);
            const errors = (0, Helpers_1.verifyPayload)(AJVSchemaEnum_1.AJVSchemaEnum.CompareCertReq, req);
            if (errors && errors.length > 0) {
                p2pLogger.error(`compareCert request validation errors: ${errors}`);
                return respond((0, ResponseError_1.BadRequest)('Request validation errors'), ResponseError_1.serializeResponseError);
            }
            const compareCertReq = {
                certs: req.certs,
                record: req.record,
            };
            resp = compareCycleCertEndpoint(compareCertReq, header.sender_id, 'compareCycleCertBinaryEndpoint');
            respond(resp, CompareCertResp_1.serializeCompareCertResp);
        }
        catch (errors) {
            nestedCounters_1.nestedCountersInstance.countEvent('internal', `${route}-exception`);
            /* prettier-ignore */ if (logger_1.logFlags.error)
                p2pLogger.error(`${route} exception executing request: ${(0, utils_1.errorToStringFull)(errors)}`);
            return respond((0, ResponseError_1.InternalError)('Exception executing request'), ResponseError_1.serializeResponseError);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd(route);
        }
    },
};
const routes = {
    internal: {
    // 'compare-cert': compareCertRoute,
    },
    gossip: {
        'gossip-cert': gossipCertRoute,
    },
    internal2: {
        [compareCertBinaryHandler.name]: compareCertBinaryHandler,
    },
};
/** CONTROL FUNCTIONS */
function init() {
    // Get a handle to write to p2p.log
    p2pLogger = Context_1.logger.getLogger('p2p');
    cycleLogger = Context_1.logger.getLogger('cycle');
    cacheOfCycleCerts = new utils.FIFOCache(100);
    for (const submodule of exports.submodules) {
        if (submodule.init)
            submodule.init();
    }
    // Init state
    reset();
    // Register routes
    for (const [name, handler] of Object.entries(routes.internal)) {
        Comms.registerInternal(name, handler);
    }
    for (const [name, handler] of Object.entries(routes.gossip)) {
        Comms.registerGossipHandler(name, handler);
    }
    for (const [name, handler] of Object.entries(routes.internal2)) {
        Comms.registerInternalBinary(name, handler.handler);
    }
}
exports.init = init;
function moduleMigration() {
    // removing SafetyMode from submodules and adding Modes
    exports.submodules = exports.submodules.filter((submodule) => submodule !== SafetyMode);
    if (!exports.submodules.includes(Modes)) {
        exports.submodules.push(Modes);
        exports.submodules[exports.submodules.length - 1].init();
    }
}
function updateScaleFactor() {
    let activeNodeCount = NodeList.activeByIdOrder.length;
    let consensusRange = Math.min(Context_1.config.sharding.nodesPerConsensusGroup, activeNodeCount); //if we have less activeNodeCount than consensus radius
    //  we can only count the minumum of the two. otherwise it would over boost scaling
    let networkParSize = 100; //num nodes where we want scale to be 1.0.   should be 50-100, can set to 5 for small network testing
    let consenusParSize = 5; //consenus size where we want the scale to be 1.0
    // this is a bit hard coded, but basicaly the first 400 nodes in a network get a boost to max syncing allowes
    // this should smooth out the joining process but make it so we dont need such high defaults for syncMax that
    // could become a problem when there are more nodes in the network later on.
    if (Context_1.config.p2p.syncBoostEnabled) {
        if (activeNodeCount < 10) {
            exports.scaleFactorSyncBoost = 1;
        }
        else if (activeNodeCount < 200) {
            exports.scaleFactorSyncBoost = 2;
        }
        else if (activeNodeCount < 400) {
            exports.scaleFactorSyncBoost = 1.5;
        }
        else {
            exports.scaleFactorSyncBoost = 1.5;
        }
    }
    else {
        exports.scaleFactorSyncBoost = 1;
    }
    //ITN3 example numbers  (128 / 5) * (640 / 100) = 25.6 * 6.4 = 163.84
    exports.scaleFactor = Math.max((consensusRange / consenusParSize) * (activeNodeCount / networkParSize), 1);
}
// Resets CycleCreator and submodules
function reset() {
    updateScaleFactor();
    // Reset submodules
    for (const module of exports.submodules)
        module.reset();
    // Reset CycleCreator
    txs = collectCycleTxs();
    const oldMarker = marker;
    ({ record, marker, cert } = makeCycleData(txs, CycleChain.newest || undefined));
    /* prettier-ignore */ if (logger_1.logFlags.p2pSyncDebug)
        info(`updateMarker: reset C${exports.currentCycle} Q${exports.currentQuarter} counter: ${record.counter} oldMarker: ${oldMarker} marker: ${marker} prevMarker: ${prevMarkerCached}`);
    //todo some logging here.
    bestRecord = undefined;
    bestMarker = undefined;
    bestCycleCert = new Map();
    bestCertScore = new Map();
}
/**
 * Entrypoint for cycle record creation. Sets things up then kicks off the
 * scheduler (cycleCreator) to start scheduling the callbacks for cycle record
 * creation.
 */
async function startCycles() {
    if (Self.isFirst) {
        // If first node, create cycle record 0, set bestRecord to it
        const recordZero = makeRecordZero();
        bestRecord = recordZero;
        madeCycle = true;
        // Schedule the scheduler to run at cycle zero start
        const { startQ1 } = calcIncomingTimes(recordZero);
        await schedule(cycleCreator, startQ1);
        return;
    }
    // Otherwise, set bestRecord to newest record in cycle chain
    bestRecord = CycleChain.newest;
    madeCycle = true;
    await cycleCreator();
}
exports.startCycles = startCycles;
/**
 * Schedules itself to run at the start of each cycle, and schedules callbacks
 * to run for every quarter of the cycle.
 */
async function cycleCreator() {
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        console.log('cycleCreator: currentCycle', exports.currentCycle);
    // Set current quater to 0 while we are setting up the previous record
    //   Routes should use this to not process and just single-forward gossip
    // I think setting the current cycle here as well is intuitive, but I don't want to do it right now
    exports.currentQuarter = 0;
    createCycleTag++;
    let callTag = `cct${createCycleTag}`;
    /* prettier-ignore */ if (logger_1.logFlags.verbose)
        info(`cc: start C${exports.currentCycle} Q${exports.currentQuarter} madeCycle: ${madeCycle} bestMarker: ${bestMarker} ${callTag}`);
    try {
        // Get the previous record
        //let prevRecord = madeCycle ? bestRecord : await fetchLatestRecord()
        let prevRecord = bestRecord;
        if (!prevRecord) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`cc: !prevRecord. Fetech now. ${callTag}`);
            prevRecord = await fetchLatestRecord();
        }
        while (!prevRecord) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`cc: cycleCreator: Could not get fetch prevRecord. Trying again in 1 sec...  ${callTag}`);
            await utils.sleep(1 * SECOND);
            prevRecord = await fetchLatestRecord();
        }
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: prevRecord.counter: ${prevRecord.counter} ${callTag}`);
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info(`cc: prevRecord.counter: ${prevRecord.counter} ${callTag}`);
        const networkModeBefore = Modes.networkMode; // Before applying new record
        //WE complete Sync.digestCycle each cycle even thought we are failing later to get to cycleLogger.info
        // Apply the previous records changes to the NodeList
        //if (madeCycle) {
        if (!CycleChain.newest || CycleChain.newest.counter < prevRecord.counter) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`cc: digest cycle ${prevRecord.counter} ${callTag}`);
            (0, Sync_1.digestCycle)(prevRecord, 'cycleCreator');
        }
        //}
        let data = undefined;
        try {
            // Save the previous record to the DB
            const marker = makeCycleMarker(prevRecord);
            const certificate = makeCycleCert(marker);
            const data = { ...prevRecord, marker, certificate };
            if (lastSavedData) {
                await Context_1.storage.updateCycle({ networkId: lastSavedData.networkId }, data);
            }
            else {
                // if node list hashes are not set at this point, set them to an empty array hash to avoid null values
                data.nodeListHash = data.nodeListHash || '';
                data.archiverListHash = data.archiverListHash || '';
                data.standbyNodeListHash = data.standbyNodeListHash || '';
                await Context_1.storage.addCycles(data);
            }
            lastSavedData = data;
            /* prettier-ignore */ if (logger_1.logFlags.verbose)
                info(`cc: cycle data created and stored. data.counter:${data.counter} ${callTag}`);
            // this event is currently only handled by non active snapshot system
            Self.emitter.emit('new_cycle_data', data);
        }
        catch (er) {
            /* prettier-ignore */ warn(`cc: Could not save prevRecord to DB. C${exports.currentCycle} ${(0, utils_1.formatErrorMessage)(er)}`);
        }
        // We are not always making it to this point. every 60s:
        // Print combined cycle log entry
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            cycleLogger.info(CycleChain.getDebug() + NodeList.getDebug());
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: recorded to cycle.log ${callTag}`);
        // Prune the cycle chain
        pruneCycleChain();
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: pruned ${callTag}`);
        // Send last cycle record, state hashes and receipt hashes to any subscribed archivers
        Archivers.sendData();
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: acrhiver data sent ${callTag}`); //todo list time delta
        let expectedCycle = exports.currentCycle + 1;
        ({ cycle: exports.currentCycle, quarter: exports.currentQuarter } = currentCycleQuarterByTime(prevRecord));
        if (expectedCycle !== exports.currentCycle) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`cc: expectedCycle: ${expectedCycle} currentCycle: ${exports.currentCycle} ${callTag}`);
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `cycleCreator:expectedCycle !== currentCycle ex${expectedCycle}!=${exports.currentCycle}} tag:${callTag}`);
        }
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: current cycle and quarter updated C${exports.currentCycle} Q${exports.currentQuarter} ${callTag}`);
        Join.swapUnjoinRequestQueues();
        const { quarterDuration, startQ1, startQ2, startQ3, startQ4, end } = calcIncomingTimes(prevRecord);
        exports.nextQ1Start = end;
        // make some more logging .. posibly even to cycle log with a timestamp also.
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: inc times ${lib_types_2.Utils.safeStringify({ quarterDuration, startQ1, startQ2, startQ3, startQ4, end })}  ${callTag}`);
        // Reset cycle marker and cycle certificate creation state
        reset();
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: cycle data was reset record.counter: ${record.counter}  ${callTag}`);
        // Omar moved this to before scheduling the quarters; should not make a difference
        madeCycle = false;
        // swapping out SafetyMode module for Modes if feature flag is true and if we haven't done it before
        if (Context_1.config.p2p.useNetworkModes === true && modeModuleMigrationApplied === false) {
            modeModuleMigrationApplied = true;
            moduleMigration();
        }
        // I think once the network has reached to processing mode and some nodes drop to 10% and goes into safety mode
        // At this point, the new node might find that active count from previous record is still less than minNodes and would not flip hasAlreadyEnteredProcessing to true
        // I think we should flip hasAlreadyEnteredProcessing to true any mode other than forming in the previous record
        // using minNodes since this is dealing with going into processing mode. minNodes and baselineNodes are the same value at start
        if (prevRecord.active >= Context_1.config.p2p.minNodes && exports.hasAlreadyEnteredProcessing === false) {
            exports.hasAlreadyEnteredProcessing = true;
        }
        const networkModeAfter = Modes.networkMode; // After applying new record
        if (networkModeBefore === 'recovery' && networkModeAfter === 'restore') {
            NodeList.changeNodeListInRestore(prevRecord.start);
        }
        if (prevRecord.mode === 'shutdown') {
            console.log(`❌ Shutdown mode activated at Cycle ${prevRecord.counter}. Exiting Network after ${prevRecord.duration}s.`);
            await utils.sleep(prevRecord.duration * SECOND);
            Self.emitter.emit('invoke-exit', 'Shutdown-Mode'); // Terminating the validator process
        }
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: scheduling currentCycle:${exports.currentCycle} ${callTag} ${startQ1}`);
        schedule(runQ1, startQ1, { runEvenIfLateBy: quarterDuration - 1 * SECOND }); // if there's at least one sec before Q2 starts, we can start Q1 now
        schedule(runQ2, startQ2);
        schedule(runQ3, startQ3);
        schedule(runQ4, startQ4);
        schedule(cycleCreator, end, { runEvenIfLateBy: Infinity });
    }
    finally {
        /* prettier-ignore */ if (logger_1.logFlags.verbose)
            info(`cc: end C${exports.currentCycle} Q${exports.currentQuarter} madeCycle: ${madeCycle} bestMarker: ${bestMarker} ${callTag}`);
    }
}
// network:  |q1           |q2           |q3            |q4          |q1           |q2           |q3            |q4           |
//                                                                 r
//                                                                *
// us:   |q1           |q2           |q3            |q4          |q1           |q2           |q3            |q4           |
//      |                                                                                           |
//  |             2                                                             13              |
/**
 * Handles cycle record creation tasks for quarter 1
 */
async function runQ1() {
    exports.q1SendRequests = false;
    exports.currentQuarter = 1;
    Self.emitter.emit('cycle_q1_start');
    profiler_1.profilerInstance.profileSectionStart('CycleCreator-runQ1');
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info(`C${exports.currentCycle} Q${exports.currentQuarter}`);
    const SECOND = 1000;
    const cycleDuration = record.duration * SECOND;
    const quarterDuration = cycleDuration / 4;
    //we sleep for 1/8 of the quarter duration before we send messages to improve tolerance of
    //what cycle nodes think they are in
    await utils.sleep(quarterDuration * Context_1.config.p2p.q1DelayPercent);
    //set this to true to allow other code that may be waiting for a safe time to send Q1 gossip
    //to proceed
    exports.q1SendRequests = true;
    // Tell submodules to sign and send their requests
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info('Triggering submodules to send requests...');
    for (const submodule of exports.submodules)
        submodule.sendRequests();
    profiler_1.profilerInstance.profileSectionEnd('CycleCreator-runQ1');
}
/**
 * Handles cycle record creation tasks for quarter 2
 */
function runQ2() {
    exports.currentQuarter = 2;
    Self.emitter.emit('cycle_q2_start');
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info(`C${exports.currentCycle} Q${exports.currentQuarter}`);
    // making the prevMarker now since it should ready before we start scoring certs, which happens in Q3
    prevMarkerCached = makeCycleMarker(CycleChain.newest);
}
/**
 * Handles cycle record creation tasks for quarter 3
 */
/*
  During Q1 and Q2 nodes build a list of valid txs they have seen.
  At the start of Q3 they generate a hash of all the txs they have seen.
  The cycle tx_hash is a hash of all the hashes of each valid tx
  the node has seen and validated. If the node is queried for the
  cycle_tx_hash it returns this even if sees and adds new txs to
  the list of txs the node has seen and validated.
  During Q3 the node does a robust query to get the most common
  cycle_tx_hash and queries the node that provided it to get the
  associated txs. The node validates these transactions and if
  any one of the txs is invalid it does not use what it got from
  the query. Otherwise as long as 2 or more nodes provided the
  cycle_tx_hash it switches to using this.
  Once the node has decided on the cycle_tx_hash it generates
  the a cycle_tx_hash_vote by signing the cycle_tx_hash and gossips
  that to other nodes. A node can only sign one cycle_tx_hash. If
  it tries to sign more than one it can be punished and all votes
  it submitted are ignored. The vote value is determined using
  XOR of the node_id hash and the cycle_tx_hash. A cycle_cert is
  created by combining the 3 highest value votes for a cycle_tx_hash.
  A node regossips if the gossip it received improved the value
  of the cert for the given cycle_tx_hash. The node regossips the
  all or up to the 3 best votes it has for the cycle tx_hash.
  The node also uses robust query to compare the best cycle_cert
  it has with other nodes.
*/
async function runQ3() {
    exports.currentQuarter = 3;
    Self.emitter.emit('cycle_q3_start');
    if (logger_1.logFlags.p2pNonFatal)
        info(`C${exports.currentCycle} Q${exports.currentQuarter}`);
    profiler_1.profilerInstance.profileSectionStart('CycleCreator-runQ3');
    // Get txs and create this cycle's record, marker, and cert
    txs = collectCycleTxs();
    const oldMarker = marker;
    ({ record, marker, cert } = makeCycleData(txs, CycleChain.newest));
    /* prettier-ignore */ if (logger_1.logFlags.p2pSyncDebug)
        info(`updateMarker: runQ3 C${exports.currentCycle} Q${exports.currentQuarter} counter: ${record.counter} oldMarker: ${oldMarker} marker: ${marker} prevMarker: ${prevMarkerCached}`);
    //prevMarker = oldMarker
    if (Context_1.config.debug.enableCycleRecordDebugTool || Context_1.config.debug.localEnableCycleRecordDebugTool) {
        if (exports.currentQuarter === 3 && Self.isActive) {
            const cycleData = lib_types_2.Utils.safeStringify({
                port: Self.port,
                cycleNumber: record.counter,
                cycleRecord: record,
            }) + '\n';
            fs_1.default.appendFile(filePath, cycleData, (err) => {
                if (err) {
                    console.error('Error appending to file:', err);
                }
            });
        }
    }
    /* prettier-ignore */ if (logger_1.logFlags && logger_1.logFlags.verbose)
        console.log("cycle record: ", record);
    /*
    info(`
      Original cycle txs: ${JSON.stringify(txs)}
      Original cycle record: ${JSON.stringify(record)}
      Original cycle marker: ${JSON.stringify(marker)}
      Original cycle cert: ${JSON.stringify(cert)}
    `)
    */
    // Compare this cycle's marker with the network
    const myC = exports.currentCycle;
    const myQ = exports.currentQuarter;
    // Omar - decided that we can get by with not doing a round of compareCycleMarkers
    //     and instead going straight to comparing cycle certificates.
    /*
    const matched = await compareCycleMarkers(myC, myQ, DESIRED_MARKER_MATCHES)
    if (!matched){
      warn(`In Q3 no match from compareCycleMarker with DESIRED_MARKER_MATCHES of ${DESIRED_MARKER_MATCHES}`)
      return
    }
    if (cycleQuarterChanged(myC, myQ)){
      warn(`In Q3 ran out of time waiting for match from compareCycleMarker`)
      return
    }
  
    info(`
      Compared cycle txs: ${JSON.stringify(txs)}
      Compared cycle record: ${JSON.stringify(record)}
      Compared cycle marker: ${JSON.stringify(marker)}
      Compared cycle cert: ${JSON.stringify(cert)}
    `)
    */
    madeCycle = true;
    // Gossip your cert for this cycle with the network
    gossipMyCycleCert();
    ServiceQueue.processNetworkTransactions(record);
    profiler_1.profilerInstance.profileSectionEnd('CycleCreator-runQ3');
}
/**
 * Handles cycle record creation tasks for quarter 4
 */
async function runQ4() {
    exports.currentQuarter = 4;
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info(`C${exports.currentCycle} Q${exports.currentQuarter}`);
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info(`Q4: start: C${exports.currentCycle} Q${exports.currentQuarter}`);
    // Don't do cert comparison if you didn't make the cycle
    if (madeCycle === false) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            warn('In Q4 nothing to do since we madeCycle is false.');
        return;
    }
    profiler_1.profilerInstance.profileSectionStart('CycleCreator-runQ4');
    // Compare your cert for this cycle with the network
    const myC = exports.currentCycle;
    const myQ = exports.currentQuarter;
    const enterTime = (0, network_1.shardusGetTime)();
    const cycleDuration = Context_1.config.p2p.cycleDuration * SECOND;
    try {
        let matched;
        do {
            matched = await compareCycleCert(myC, myQ, DESIRED_CERT_MATCHES);
            if (!matched) {
                if (cycleQuarterChanged(myC, myQ)) {
                    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                        warn(`In Q4 ran out of time waiting for compareCycleCert with DESIRED_CERT_MATCHES of ${DESIRED_CERT_MATCHES}`);
                    profiler_1.profilerInstance.profileSectionEnd('CycleCreator-runQ4');
                    return;
                }
                await utils.sleep(100);
                if (enterTime + cycleDuration < (0, network_1.shardusGetTime)()) {
                    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                        warn(`In Q4 waited ${Context_1.config.p2p.cycleDuration} seconds for compareCycleCert with DESIRED_CERT_MATCHES of ${DESIRED_CERT_MATCHES}`);
                    //profilerInstance.profileSectionEnd('CycleCreator-runQ4')
                    //return
                    // we should return, but want to catch this get stuck to confirm it is not happening
                    await utils.sleep(1000);
                }
            }
        } while (!matched);
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info(`
    Certified cycle record: ${lib_types_2.Utils.safeStringify(record)}
    Certified cycle marker: ${lib_types_2.Utils.safeStringify(marker)}
    Certified cycle cert: ${lib_types_2.Utils.safeStringify(cert)}
  `);
        cacheOfCycleCerts.set(exports.currentCycle, bestCycleCert.get(bestMarker));
    }
    finally {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info(`Q4: END: myC:${myC}  C${exports.currentCycle} Q${exports.currentQuarter} Certified cycle record: ${lib_types_2.Utils.safeStringify(record.counter)}`);
        // Dont need this any more since we are not doing anything after this
        // if (cycleQuarterChanged(myC, myQ)) return
        profiler_1.profilerInstance.profileSectionEnd('CycleCreator-runQ4');
    }
}
/** HELPER FUNCTIONS */
function makeRecordZero() {
    const txs = collectCycleTxs();
    return makeCycleRecord(txs, CycleChain.newest);
}
exports.makeRecordZero = makeRecordZero;
function makeCycleData(txs, prevRecord) {
    const record = makeCycleRecord(txs, prevRecord);
    const marker = makeCycleMarker(record);
    const cert = makeCycleCert(marker);
    return { record, marker, cert };
}
function collectCycleTxs() {
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        console.log('collectCycleTxs: inside collectCycleTxs');
    // Collect cycle txs from all submodules
    const txs = exports.submodules.map((submodule) => submodule.getTxs());
    return Object.assign({}, ...txs);
}
function makeCycleRecord(cycleTxs, prevRecord) {
    const baseRecord = {
        networkId: prevRecord ? prevRecord.networkId : (0, lib_crypto_utils_1.randomBytes)(32),
        counter: prevRecord ? prevRecord.counter + 1 : 0,
        previous: prevRecord ? makeCycleMarker(prevRecord) : '0'.repeat(64),
        start: prevRecord && prevRecord.mode !== 'shutdown' ? prevRecord.start + prevRecord.duration : utils.getTime('s'),
        duration: prevRecord ? prevRecord.duration : Context_1.config.p2p.cycleDuration,
        networkConfigHash: makeNetworkConfigHash(),
    };
    exports.currentStart = baseRecord.start;
    const cycleRecord = Object.assign(baseRecord, {
        joined: [],
        returned: [],
        lost: [],
        lostSyncing: [],
        refuted: [],
        apoptosized: [],
        nodeListHash: '',
        archiverListHash: '',
        standbyNodeListHash: '',
        random: Context_1.config.debug.randomCycleData ? Math.floor(Math.random() * 1000) + 1 : 0,
        txadd: [],
        txremove: [],
        txlisthash: '',
    });
    exports.submodules.map((submodule) => submodule.updateRecord(cycleTxs, cycleRecord, prevRecord));
    //Updating Cycle Record if network has entered 'Shutdown' Mode
    if (Context_1.config.p2p.initShutdown || cycleRecord.mode === 'shutdown') {
        console.log('Updating Shutdown Mode Cycle Record...');
        cycleRecord.removed = ['all'];
        cycleRecord.archiversAtShutdown = Array.from(Archivers.archivers.values());
    }
    return cycleRecord;
}
function makeCycleMarker(record) {
    const marker = Context_1.crypto.hash(record);
    info(`makeCycleCert for cycle ${record.counter} marker: ${marker} cycleRecord: ${lib_types_2.Utils.safeStringify(record)}`);
    return marker;
}
exports.makeCycleMarker = makeCycleMarker;
function makeCycleCert(marker) {
    const cert = Context_1.crypto.sign({ marker });
    console.log(`makeCycleCert:  ${lib_types_2.Utils.safeStringify(cert)}`);
    return cert;
}
function makeNetworkConfigHash() {
    exports.netConfig = {
        crypto: Context_1.config.crypto,
        heartbeatInterval: Context_1.config.heartbeatInterval,
        loadDetection: Context_1.config.loadDetection,
        network: Context_1.config.network,
        rateLimiting: Context_1.config.rateLimiting,
        sharding: Context_1.config.sharding,
        transactionExpireTime: Context_1.config.transactionExpireTime,
        p2p: { ...Context_1.config.p2p },
        stateManager: Context_1.config.stateManager,
        debug: Context_1.config.debug,
    };
    delete exports.netConfig.p2p.existingArchivers;
    return Context_1.crypto.hash(exports.netConfig);
}
function unseenTxs(ours, theirs) {
    const unseen = {};
    for (const field in theirs) {
        if (theirs[field] && ours[field]) {
            if (Context_1.crypto.hash(theirs[field]) !== Context_1.crypto.hash(ours[field])) {
                // Go through each tx of theirs and see if ours has it
                const ourTxHashes = new Set(ours[field].map((tx) => Context_1.crypto.hash(tx)));
                for (const tx of theirs[field]) {
                    if (!ourTxHashes.has(Context_1.crypto.hash(tx))) {
                        // If it doesn't, add it to unseen
                        if (!unseen[field])
                            unseen[field] = [];
                        unseen[field].push(tx);
                    }
                }
            }
        }
        else {
            // Add the whole field from theirs to unseen
            unseen[field] = theirs[field];
        }
    }
    return unseen;
}
function dropInvalidTxs(txs) {
    // [TODO] Call into each module to validate its relevant CycleTxs
    return txs;
}
/**
 * Syncs the CycleChain to the newest cycle record of the network, and returns
 * the newest cycle record.
 */
async function fetchLatestRecord() {
    try {
        const oldCounter = CycleChain.newest.counter;
        await (0, Sync_1.syncNewCycles)(NodeList.activeOthersByIdOrder);
        if (CycleChain.newest.counter <= oldCounter) {
            // We didn't actually sync
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn(`CycleCreator: fetchLatestRecord: synced record not newer CycleChain.newest.counter: ${CycleChain.newest.counter} oldCounter: ${oldCounter}`);
            fetchLatestRecordFails++;
            if (fetchLatestRecordFails > maxFetchLatestRecordFails) {
                /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                    error('CycleCreator: fetchLatestRecord_A: fetchLatestRecordFails > maxFetchLatestRecordFails. apoptosizeSelf ');
                // this.fatalLogger.fatal(
                //   'CycleCreator: fetchLatestRecord_A: fetchLatestRecordFails > maxFetchLatestRecordFails. apoptosizeSelf '
                // )
                nestedCounters_1.nestedCountersInstance.countEvent('fetchLatestRecord', `fetchLatestRecord_A fail and apop self. ${(0, network_1.shardusGetTime)()}`);
                Apoptosis.apoptosizeSelf('Apoptosized within fetchLatestRecord() => src/p2p/CycleCreator.ts', 'Node stopped. Unable to sync newest cycles due to node performance or network issues.');
            }
            return null;
        }
    }
    catch (err) {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            warn('CycleCreator: fetchLatestRecord: syncNewCycles failed:', (0, utils_1.errorToStringFull)(err));
        fetchLatestRecordFails++;
        if (fetchLatestRecordFails > maxFetchLatestRecordFails) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                error('CycleCreator: fetchLatestRecord_B: fetchLatestRecordFails > maxFetchLatestRecordFails. apoptosizeSelf ');
            // this.fatalLogger.fatal(
            //   'CycleCreator: fetchLatestRecord_B: fetchLatestRecordFails > maxFetchLatestRecordFails. apoptosizeSelf ',
            //   utils.formatErrorMessage(err)
            // )
            nestedCounters_1.nestedCountersInstance.countEvent('fetchLatestRecord', `fetchLatestRecord_B fail and apop self. ${(0, network_1.shardusGetTime)()}`);
            Apoptosis.apoptosizeSelf('Apoptosized within fetchLatestRecord() => src/p2p/CycleCreator.ts', 'Node stopped. Unable to sync newest cycles due to node performance or network issues.');
        }
        return null;
    }
    fetchLatestRecordFails = 0;
    return CycleChain.newest;
}
/**
 * Returns what the current cycle counter and quarter would be from the given
 * cycle record.
 *
 * @param record CycleRecord
 */
function currentCycleQuarterByTime(record) {
    const SECOND = 1000;
    const cycleDuration = record.duration * SECOND;
    const quarterDuration = cycleDuration / 4;
    const start = record.start * SECOND + cycleDuration;
    const now = (0, network_1.shardusGetTime)();
    const elapsed = now - start;
    const elapsedQuarters = elapsed / quarterDuration;
    const cycle = record.counter + 1 + Math.trunc(elapsedQuarters / 4);
    const quarter = Math.abs(Math.ceil(elapsedQuarters % 4));
    return { cycle, quarter };
}
/**
 * Returns the timestamp of each quarter and the timestamp of the end of the
 * cycle record AFTER the given cycle record.
 *
 * @param record CycleRecord
 */
function calcIncomingTimes(record) {
    const cycleDuration = record.duration * SECOND;
    const quarterDuration = cycleDuration / 4;
    const start = record.start * SECOND + cycleDuration;
    const startQ1 = start;
    const startQ2 = start + 1 * quarterDuration;
    const startQ3 = start + 2 * quarterDuration;
    const startQ4 = start + 3 * quarterDuration;
    const end = start + cycleDuration;
    return { quarterDuration, startQ1, startQ2, startQ3, startQ4, end };
}
exports.calcIncomingTimes = calcIncomingTimes;
/**
 * Schedules a callback to run at a certain time. It will run the callback even
 * if its time has passed, as long as it has not gone past runEvenIfLateBy ms.
 *
 * @param callback
 * @param time
 * @param opts
 * @param args
 */
function schedule(callback, time, { runEvenIfLateBy = 0 } = {}, ...args) {
    return new Promise((resolve) => {
        const now = (0, network_1.shardusGetTime)();
        if (now >= time) {
            if (now - time <= runEvenIfLateBy) {
                setImmediate(async () => {
                    await callback(...args);
                    resolve();
                });
            }
            return;
        }
        const toWait = time - now;
        if (timers[callback.name])
            clearTimeout(timers[callback.name]);
        timers[callback.name] = setTimeout(async () => {
            await callback(...args);
            resolve();
        }, toWait);
    });
}
exports.schedule = schedule;
function shutdown() {
    warn('Cycle creator shutdown');
    for (const timer of Object.keys(timers)) {
        warn(`clearing timer ${timer}`);
        clearTimeout(timers[timer]);
    }
    warn(`current cycle and quarter is: C${exports.currentCycle} Q${exports.currentQuarter}`);
    exports.currentCycle += 1;
    exports.currentQuarter = 0; // to stop functions which check if we are in the same quarter
    warn(`changed cycle and quarter to: C${exports.currentCycle} Q${exports.currentQuarter}`);
}
exports.shutdown = shutdown;
function cycleQuarterChanged(cycle, quarter) {
    return cycle !== exports.currentCycle || quarter !== exports.currentQuarter;
}
function scoreCert(cert, prevMarker) {
    try {
        const id = NodeList.byPubKey.get(cert.sign.owner).id; // get node id from cert pub key
        const obj = { id };
        const hid = Context_1.crypto.hash(obj); // Omar - use hash of id so the cert is not made by nodes that are near based on node id
        // This is what scoring has used for the marker for years:
        const inProgressCertMarker = cert.marker;
        //// Some test code to compare JIT marker creation with the prevMarker.
        if (logger_1.logFlags.p2pSyncDebug && logger_1.logFlags.verbose) {
            const jitPrevMarker = makeCycleMarker(CycleChain.newest);
            info(`scoreCert cycle:${CycleChain?.newest?.counter}  scoreCert: calcPrevMarker: ${jitPrevMarker} prevMarker: ${prevMarker} inProgressCertMarker: ${inProgressCertMarker}  id: ${id} `);
            if (jitPrevMarker != prevMarker) {
                info(`scoreCert error: jitPrevMarker != prevMarker: ${jitPrevMarker} != ${prevMarker} `);
            }
        }
        let markerToScore = inProgressCertMarker;
        if (Context_1.config.p2p.newCycleCertScoring) {
            //this is an update to use the prevMarker instead of the inProgressCertMarker
            markerToScore = prevMarker;
        }
        // take the marker we want to use and XOR it with the hid
        const out = utils.XOR(markerToScore, hid);
        // if the cert is from a non-foundation node, reduce the potential score
        if (Context_1.config.p2p.nerfNonFoundationCertScores && NodeList.byPubKey.get(cert.sign.owner).foundationNode === false) {
            return out & 0x0fffffff;
        }
        return out;
    }
    catch (err) {
        error('scoreCert ERR:', err);
        return 0;
    }
}
function validateCertSign(certs, sender) {
    for (const cert of certs) {
        const cleanCert = {
            marker: cert.marker,
            sign: cert.sign,
        };
        if (NodeList.byPubKey.has(cleanCert.sign.owner) === false) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn('validateCertSign: bad owner');
            return false;
        }
        if (!Context_1.crypto.verify(cleanCert)) {
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                warn('validateCertSign: bad sig');
            return false;
        }
    }
    return true;
}
function validateCerts(certs, record, sender, callerTag) {
    if (!certs || !Array.isArray(certs) || certs.length <= 0) {
        /* prettier-ignore */ warn(`validateCerts: bad certificate format;  ${callerTag}`);
        /* prettier-ignore */ warn(`validateCerts:   sent by: port:${NodeList.nodes.get(sender).externalPort} id:${lib_types_2.Utils.safeStringify(sender)}`);
        return false;
    }
    if (!record || record === null || typeof record !== 'object')
        return false;
    //  make sure the cycle counter is what we expect
    if (record.counter !== CycleChain.newest.counter + 1) {
        /* prettier-ignore */ warn(`validateCerts: bad cycle record counter; ${callerTag} expected ${CycleChain.newest.counter + 1} but got ${record.counter} `);
        /* prettier-ignore */ warn(`validateCerts:   sent by: port:${NodeList.nodes.get(sender).externalPort} id:${lib_types_2.Utils.safeStringify(sender)}`);
        return false;
    }
    // make sure all the certs are for the same cycle marker
    const inpMarker = Context_1.crypto.hash(record);
    for (let i = 1; i < certs.length; i++) {
        if (inpMarker !== certs[i].marker) {
            /* prettier-ignore */ warn(`validateCerts: certificates marker does not match hash of record;  ${callerTag}`);
            /* prettier-ignore */ warn(`validateCerts:   sent by: port:${NodeList.nodes.get(sender).externalPort} id:${lib_types_2.Utils.safeStringify(sender)}`);
            return false;
        }
    }
    // make sure that the certs are from different owners and not the same node
    const seen = {};
    for (let i = 0; i < certs.length; i++) {
        if (seen[certs[i].sign.owner]) {
            /* prettier-ignore */ warn(`validateCerts: multiple certificate from same owner; ${callerTag} certs: ${lib_types_2.Utils.safeStringify(certs)}`);
            /* prettier-ignore */ warn(`validateCerts:   sent by: port:${NodeList.nodes.get(sender).externalPort} id:${lib_types_2.Utils.safeStringify(sender)}`);
            return false;
        }
        seen[certs[i].sign.owner] = true;
    }
    //  checks signatures; more expensive
    if (!validateCertSign(certs, sender)) {
        /* prettier-ignore */ warn(`validateCerts: certificate has bad sign;  ${callerTag} certs:${lib_types_2.Utils.safeStringify(certs)}`);
        /* prettier-ignore */ warn(`validateCerts:   sent by: port:${NodeList.nodes.get(sender).externalPort} id:${lib_types_2.Utils.safeStringify(sender)}`);
        return false;
    }
    return true;
}
function validateCertsRecordTypes(inp, caller) {
    let err = utils.validateTypes(inp, { certs: 'a', record: 'o' });
    if (err) {
        warn(caller + ' bad input: ' + err + ' ' + lib_types_2.Utils.safeStringify(inp));
        return false;
    }
    for (const cert of inp.certs) {
        err = utils.validateTypes(cert, { marker: 's', score: 'n', sign: 'o' });
        if (err) {
            warn(caller + ' bad input.certs: ' + err);
            return false;
        }
        err = utils.validateTypes(cert.sign, { owner: 's', sig: 's' });
        if (err) {
            warn(caller + ' bad input.sign: ' + err);
            return false;
        }
    }
    err = utils.validateTypes(inp.record, {
        activated: 'a',
        activatedPublicKeys: 'a',
        active: 'n',
        apoptosized: 'a',
        counter: 'n',
        desired: 'n',
        duration: 'n',
        expired: 'n',
        joined: 'a',
        joinedArchivers: 'a',
        joinedConsensors: 'a',
        lost: 'a',
        previous: 's',
        refreshedArchivers: 'a',
        refreshedConsensors: 'a',
        refuted: 'a',
        removed: 'a',
        start: 'n',
        syncing: 'n',
    });
    if (err) {
        warn(caller + ' bad input.record: ' + err);
        return false;
    }
    //  submodules need to validate their part of the record
    for (const submodule of exports.submodules) {
        err = submodule.validateRecordTypes(inp.record);
        if (err) {
            warn(caller + ' bad input.record.* ' + err);
            return false;
        }
    }
    return true;
}
// Given an array of valid cycle certs, go through them and see if we can improve our best cert
// return true if we improved it
// We assume the certs have already been checked
function improveBestCert(inpCerts, inpRecord) {
    //  warn(`improveBestCert: certs:${JSON.stringify(certs)}`)
    //  warn(`improveBestCert: record:${JSON.stringify(record)}`)
    let improved = false;
    if (inpCerts.length <= 0) {
        return false;
    }
    let bscore = 0;
    if (bestMarker) {
        if (bestCertScore.get(bestMarker)) {
            bscore = bestCertScore.get(bestMarker);
        }
    }
    //  warn(`improveBestCert: bscore:${JSON.stringify(bscore)}`)
    const bcerts = bestCycleCert.get(inpCerts[0].marker);
    //  warn(`improveBestCert: bcerts:${JSON.stringify(bcerts)}`)
    const have = {};
    if (bcerts) {
        for (const cert of bcerts) {
            have[cert.sign.owner] = true;
        }
    }
    //// logic moved back to q2 for now
    // if(prevMarkerLastUpdate < CycleChain.newest.counter) {
    //   prevMarkerLastUpdate = CycleChain.newest.counter
    //   const lastPrevMarker = prevMarkerCached
    //   prevMarkerCached = makeCycleMarker(CycleChain.newest)
    //   info(`improveBestCert: newest.counter:${CycleChain.newest.counter} updated prevMarker:${prevMarkerCached} lastPrevMarker:${lastPrevMarker}`)
    // }
    //  warn(`improveBestCert: have:${JSON.stringify(have)}`)
    for (const cert of inpCerts) {
        if (Active.enableSkipActivatedCert && Active.activated.includes(cert.sign.owner)) {
            continue;
        }
        // make sure we don't store more than one cert from the same owner with the same marker
        if (have[cert.sign.owner])
            continue;
        cert.score = scoreCert(cert, prevMarkerCached);
        if (!bestCycleCert.get(cert.marker)) {
            bestCycleCert.set(cert.marker, [cert]);
        }
        else {
            let added = false;
            const bcerts = bestCycleCert.get(cert.marker);
            let i = 0;
            for (; i < bcerts.length; i++) {
                if (bcerts[i].score < cert.score) {
                    bcerts.splice(i, 0, cert);
                    bcerts.splice(BEST_CERTS_WANTED);
                    added = true;
                    break;
                }
            }
            if (!added && i < BEST_CERTS_WANTED) {
                bcerts.splice(i, 0, cert);
            }
        }
    }
    for (const cert of inpCerts) {
        if (Active.enableSkipActivatedCert && Active.activated.includes(cert.sign.owner)) {
            continue;
        }
        let score = 0;
        const bcerts = bestCycleCert.get(cert.marker);
        for (const bcert of bcerts) {
            score += bcert.score;
        }
        bestCertScore.set(cert.marker, score);
        if (score > bscore) {
            bestMarker = cert.marker;
            bestRecord = inpRecord;
            improved = true;
        }
    }
    //  info(`improveBestCert: bestScore:${bestCertScore.get(bestMarker)}`)
    //  info(`improveBestCert: bestMarker:${bestMarker}`)
    //  info(`improveBestCert: bestCerts:${JSON.stringify(bestCycleCert.get(bestMarker))}`)
    //  info(`improveBestCert: improved:${improved}`)
    return improved;
}
function compareCycleCertEndpoint(inp, sender, endpoint_tag) {
    if (bestMarker === undefined) {
        // This should almost never happen since we generate and gossip our
        //   cert at the begining of Q3 and don't start comparing certs until
        //   the begining of Q4.
        warn(`${endpoint_tag} - bestMarker is undefined`);
        return { certs: [], record: record }; // receiving node will igore our response
    }
    if (!validateCertsRecordTypes(inp, endpoint_tag)) {
        return { certs: bestCycleCert.get(bestMarker), record: bestRecord };
    }
    const { certs: inpCerts, record: inpRecord } = inp;
    if (!validateCerts(inpCerts, inpRecord, sender, endpoint_tag)) {
        return { certs: bestCycleCert.get(bestMarker), record: bestRecord };
    }
    const inpMarker = inpCerts[0].marker;
    if (inpMarker !== makeCycleMarker(inpRecord)) {
        return { certs: bestCycleCert.get(bestMarker), record: bestRecord };
    }
    improveBestCert(inpCerts, inpRecord);
    return { certs: bestCycleCert.get(bestMarker), record: bestRecord };
}
async function compareCycleCert(myC, myQ, matches) {
    const queryFn = async (node) => {
        const req = {
            certs: bestCycleCert.get(bestMarker),
            record: bestRecord,
        };
        // not enough data to start comparing
        if (!req.certs || !req.record) {
            return [null, node];
        }
        let resp;
        // if (config.p2p.useBinarySerializedEndpoints && config.p2p.compareCertBinary) {
        let reqSerialized = req;
        resp = await Comms.askBinary(node, InternalRouteEnum_1.InternalRouteEnum.binary_compare_cert, reqSerialized, CompareCertReq_1.serializeCompareCertReq, CompareCertResp_1.deserializeCompareCertResp, {});
        // } else {
        // resp = await Comms.ask(node, 'compare-cert', req)
        // }
        if (!validateCertsRecordTypes(resp, 'compareCycleCert'))
            return [null, node];
        if (!(resp && resp.certs && resp.certs[0].marker && resp.record)) {
            throw new Error('compareCycleCert: Invalid query response');
        }
        return [resp, node];
    };
    const compareFn = (respArr) => {
        /**
         * [IMPORTANT] Don't change things if the awaited call took too long
         */
        if (cycleQuarterChanged(myC, myQ))
            return Utils_1.Comparison.ABORT;
        const [resp, node] = respArr;
        if (resp === null)
            return Utils_1.Comparison.WORSE;
        if (resp.certs[0].marker === bestMarker) {
            // Our markers match
            return Utils_1.Comparison.EQUAL;
        }
        else if (!validateCerts(resp.certs, resp.record, node.id, 'compareCycleCert')) {
            return Utils_1.Comparison.WORSE;
        }
        else if (improveBestCert(resp.certs, resp.record)) {
            // Their marker is better, change to it and their record
            // don't need the following line anymore since improveBestCert sets bestRecord if it improved
            // bestRecord = resp.record
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countRareEvent('cycle', `improved cert (better node) ${node.internalIp}:${node.internalPort}`);
            nestedCounters_1.nestedCountersInstance.countRareEvent('cycle', `improved cert (our node) ${Self.ip}:${Self.port}`);
            return Utils_1.Comparison.BETTER;
        }
        else {
            // Their marker was worse
            return Utils_1.Comparison.WORSE;
        }
    };
    // Make sure matches makes sense
    if (matches > NodeList.activeOthersByIdOrder.length) {
        matches = NodeList.activeOthersByIdOrder.length;
    }
    /**
     * [NOTE] The number of nodesToAsk should be limited based on the amount of
     * time we have in the quarter
     */
    // We shuffle to spread out the network load of cert comparison
    const nodesToAsk = [...NodeList.activeOthersByIdOrder];
    utils.shuffleArray(nodesToAsk);
    // If anything compares better than us, compareQuery starts over
    const errors = await (0, Utils_1.compareQuery)(nodesToAsk, queryFn, compareFn, matches);
    if (errors.length > 0) {
        warn(`compareCycleCertEndpoint: errors: ${lib_types_2.Utils.safeStringify(errors)}`);
    }
    // Anything that's not an error, either matched us or compared worse than us
    return NodeList.activeOthersByIdOrder.length - errors.length >= matches;
}
async function gossipMyCycleCert() {
    // If we're not active dont gossip, unless we are first
    if (!Self.isActive && !Self.isFirst)
        return;
    profiler_1.profilerInstance.profileSectionStart('CycleCreator-gossipMyCycleCert');
    // We may have already received certs from other other nodes so gossip only if our cert improves it
    // madeCert = true  // not used
    if (logger_1.logFlags.p2pNonFatal)
        info('About to improveBestCert with our cert...');
    if (improveBestCert([cert], record)) {
        // don't need the following line anymore since improveBestCert sets bestRecord if it improved
        // bestRecord = record
        if (logger_1.logFlags.p2pNonFatal)
            info('bestRecord was set to our record');
        await gossipCycleCert(Self.id);
    }
    profiler_1.profilerInstance.profileSectionEnd('CycleCreator-gossipMyCycleCert');
}
function gossipHandlerCycleCert(inp, sender, tracker) {
    profiler_1.profilerInstance.profileSectionStart('CycleCreator-gossipHandlerCycleCert');
    if (!validateCertsRecordTypes(inp, 'gossipHandlerCycleCert'))
        return;
    const { certs: inpCerts, record: inpRecord } = inp;
    if (!validateCerts(inpCerts, inpRecord, sender, 'gossipHandlerCycleCert')) {
        return;
    }
    if (improveBestCert(inpCerts, inpRecord)) {
        // don't need the following line anymore since improveBestCert sets bestRecord if it improved
        // bestRecord = inpRecord
        gossipCycleCert(sender, tracker);
    }
    profiler_1.profilerInstance.profileSectionEnd('CycleCreator-gossipHandlerCycleCert');
}
// This gossips the best cert we have
async function gossipCycleCert(sender, tracker) {
    const certGossip = {
        certs: bestCycleCert.get(bestMarker),
        record: bestRecord,
    };
    const signedCertGossip = Context_1.crypto.sign(certGossip);
    Comms.sendGossip('gossip-cert', signedCertGossip, tracker, sender, Join.nodeListFromStates([
        lib_types_1.P2P.P2PTypes.NodeStatus.ACTIVE,
        lib_types_1.P2P.P2PTypes.NodeStatus.READY,
        lib_types_1.P2P.P2PTypes.NodeStatus.SYNCING,
    ]), true);
}
function pruneCycleChain() {
    if (Context_1.config.p2p.useSyncProtocolV2) {
        CycleChain.prune(MAX_CYCLES_TO_KEEP);
    }
    else {
        // Determine number of cycle records to keep
        const keep = Refresh.cyclesToKeep();
        // Throws away extra cycles
        CycleChain.prune(keep);
    }
}
function getBestCycleCerts(counter) {
    return cacheOfCycleCerts.get(counter) ?? [];
}
exports.getBestCycleCerts = getBestCycleCerts;
function info(...msg) {
    const entry = `CycleCreator: ${msg.join(' ')}`;
    p2pLogger.info(entry);
}
function warn(...msg) {
    const entry = `CycleCreator: ${msg.join(' ')}`;
    p2pLogger.warn(entry);
}
function error(...msg) {
    const entry = `CycleCreator: ${msg.join(' ')}`;
    p2pLogger.error(entry);
}
function fatal(...msg) {
    const entry = `CycleCreator: ${msg.join(' ')}`;
    p2pLogger.fatal(entry);
}
//# sourceMappingURL=CycleCreator.js.map