"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.VerificationPeer = void 0;
const logger_1 = __importStar(require("../../logger"));
const block_1 = require("../../models/block");
const coin_1 = require("../../models/coin");
const transaction_1 = require("../../models/transaction");
const p2p_1 = require("../../modules/bitcoin/p2p");
const chain_state_1 = require("../../providers/chain-state");
const utils_1 = require("../../utils");
class VerificationPeer extends p2p_1.BitcoinP2PWorker {
    constructor() {
        super(...arguments);
        this.prevBlockNum = 0;
        this.prevHash = '';
        this.nextBlockHash = '';
        this.deepScan = false;
    }
    enableDeepScan() {
        this.deepScan = true;
    }
    disableDeepScan() {
        this.deepScan = false;
    }
    setupListeners() {
        this.pool.on('peerready', peer => {
            logger_1.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${peer.host}:${peer.port.toString().padEnd(5)} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.pool.on('peerdisconnect', peer => {
            logger_1.default.warn(`${(0, logger_1.timestamp)()} | Not connected to peer: ${peer.host}:${peer.port.toString().padEnd(5)} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.pool.on('peertx', async (peer, message) => {
            const hash = message.transaction.hash;
            logger_1.default.debug('peer tx received: %o', {
                peer: `${peer.host}:${peer.port}`,
                chain: this.chain,
                network: this.network,
                hash
            });
            this.events.emit('transaction', message.transaction);
        });
        this.pool.on('peerblock', async (peer, message) => {
            const { block } = message;
            const { hash } = block;
            logger_1.default.debug('peer block received: %o', {
                peer: `${peer.host}:${peer.port}`,
                chain: this.chain,
                network: this.network,
                hash
            });
            this.events.emit(hash, message.block);
            this.events.emit('block', message.block);
        });
        this.pool.on('peerheaders', (peer, message) => {
            logger_1.default.debug('peerheaders message received: %o', {
                peer: `${peer.host}:${peer.port}`,
                chain: this.chain,
                network: this.network,
                count: message.headers.length
            });
            this.events.emit('headers', message.headers);
        });
        this.pool.on('peerinv', (peer, message) => {
            const filtered = message.inventory.filter(inv => {
                const hash = this.bitcoreLib.encoding
                    .BufferReader(inv.hash)
                    .readReverse()
                    .toString('hex');
                return !this.isCachedInv(inv.type, hash);
            });
            if (filtered.length) {
                peer.sendMessage(this.messages.GetData(filtered));
            }
        });
    }
    async getBlockForNumber(currentHeight) {
        const { chain, network } = this;
        const [{ hash }] = await block_1.BitcoinBlockStorage.collection
            .find({ chain, network, height: currentHeight })
            .limit(1)
            .toArray();
        return this.getBlock(hash);
    }
    async resync(start, end) {
        const { chain, network } = this;
        let currentHeight = Math.max(1, start);
        while (currentHeight < end) {
            const locatorHashes = await chain_state_1.ChainStateProvider.getLocatorHashes({
                chain,
                network,
                startHeight: Math.max(1, currentHeight - 30),
                endHeight: currentHeight
            });
            const headers = await this.getHeaders(locatorHashes);
            if (!headers.length) {
                logger_1.default.info(`${chain}:${network} up to date.`);
                break;
            }
            const headerCount = Math.min(headers.length, end - currentHeight);
            logger_1.default.info(`Re-Syncing ${headerCount} blocks for ${chain} ${network}`);
            let lastLog = Date.now();
            for (const header of headers) {
                if (currentHeight <= end) {
                    const block = await this.getBlock(header.hash);
                    await block_1.BitcoinBlockStorage.processBlock({ chain, network, block, initialSyncComplete: true });
                    const nextBlock = await block_1.BitcoinBlockStorage.collection.findOne({
                        chain,
                        network,
                        previousBlockHash: block.hash
                    });
                    if (nextBlock) {
                        await block_1.BitcoinBlockStorage.collection.updateOne({ chain, network, hash: block.hash }, { $set: { nextBlockHash: nextBlock.hash } });
                    }
                }
                currentHeight++;
                if (Date.now() - lastLog > 100) {
                    logger_1.default.info('Re-Sync: %o', {
                        chain,
                        network,
                        height: currentHeight
                    });
                    lastLog = Date.now();
                }
            }
        }
    }
    async validateDataForBlock(blockNum, tipHeight, log = false) {
        let success = true;
        const { chain, network } = this;
        const atTipOfChain = blockNum === tipHeight;
        const errors = new Array();
        const [block, blockTxs] = await Promise.all([
            block_1.BitcoinBlockStorage.collection.findOne({
                chain,
                network,
                height: blockNum,
                processed: true
            }),
            transaction_1.TransactionStorage.collection.find({ chain, network, blockHeight: blockNum }).toArray()
        ]);
        if (!block) {
            success = false;
            const error = {
                model: 'block',
                err: true,
                type: 'MISSING_BLOCK',
                payload: { blockNum }
            };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
            return { success, errors };
        }
        const blockTxids = blockTxs.map(t => t.txid);
        const firstHash = blockTxs[0] ? blockTxs[0].blockHash : block.hash;
        const [coinsForTx, mempoolTxs, blocksForHash, blocksForHeight, p2pBlock] = await Promise.all([
            coin_1.CoinStorage.collection.find({ chain, network, mintTxid: { $in: blockTxids } }).toArray(),
            transaction_1.TransactionStorage.collection.find({ chain, network, blockHeight: -1, txid: { $in: blockTxids } }).toArray(),
            block_1.BitcoinBlockStorage.collection.countDocuments({ chain, network, hash: firstHash }),
            block_1.BitcoinBlockStorage.collection.countDocuments({
                chain,
                network,
                height: blockNum,
                processed: true
            }),
            this.deepScan ? this.getBlockForNumber(blockNum) : Promise.resolve({})
        ]);
        const seenTxs = {};
        const linearProgress = this.prevBlockNum && this.prevBlockNum == blockNum - 1;
        const prevHashMismatch = this.prevHash && block.previousBlockHash != this.prevHash;
        const nextHashMismatch = this.nextBlockHash && block.hash != this.nextBlockHash;
        this.prevHash = block.hash;
        this.nextBlockHash = block.nextBlockHash;
        this.prevBlockNum = blockNum;
        const missingLinearData = linearProgress && (prevHashMismatch || nextHashMismatch);
        const missingNextBlockHash = !atTipOfChain && !block.nextBlockHash;
        const missingPrevBlockHash = !block.previousBlockHash;
        const missingData = missingNextBlockHash || missingPrevBlockHash || missingLinearData;
        if (!block || block.transactionCount != blockTxs.length || missingData) {
            success = false;
            const error = {
                model: 'block',
                err: true,
                type: 'CORRUPTED_BLOCK',
                payload: { blockNum, txCount: block.transactionCount, foundTxs: blockTxs.length }
            };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
        }
        if (block && this.deepScan && p2pBlock) {
            const txs = p2pBlock.transactions ? p2pBlock.transactions.slice(1) : [];
            const spends = txs.flatMap(tx => tx.inputs.map(input => input.toObject()));
            for (const spend of spends) {
                const found = await coin_1.CoinStorage.collection.findOne({
                    chain,
                    network,
                    mintTxid: spend.prevTxId,
                    mintIndex: spend.outputIndex
                });
                if (found && found.spentHeight !== block.height) {
                    success = false;
                    const error = { model: 'coin', err: true, type: 'COIN_SHOULD_BE_SPENT', payload: { coin: found, blockNum } };
                    errors.push(error);
                    if (log) {
                        console.log(JSON.stringify(error));
                    }
                }
                else {
                    if (!found && spend.prevTxId != '0000000000000000000000000000000000000000000000000000000000000000') {
                        success = false;
                        const error = {
                            model: 'coin',
                            err: true,
                            type: 'MISSING_INPUT',
                            payload: { coin: { mintTxid: spend.prevTxId, mintIndex: spend.outputIndex }, blockNum }
                        };
                        errors.push(error);
                        if (log) {
                            console.log(JSON.stringify(error));
                        }
                    }
                }
            }
        }
        for (const tx of mempoolTxs) {
            success = false;
            const error = { model: 'transaction', err: true, type: 'DUPE_TRANSACTION', payload: { tx, blockNum } };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
        }
        const seenTxCoins = {};
        for (const tx of blockTxs) {
            if (tx.fee < 0) {
                success = false;
                const error = { model: 'transaction', err: true, type: 'NEG_FEE', payload: { tx, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
            if (seenTxs[tx.txid]) {
                success = false;
                const error = { model: 'transaction', err: true, type: 'DUPE_TRANSACTION', payload: { tx, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
            else {
                seenTxs[tx.txid] = tx;
            }
        }
        for (const coin of coinsForTx) {
            if (seenTxCoins[coin.mintTxid] && seenTxCoins[coin.mintTxid][coin.mintIndex]) {
                success = false;
                const error = { model: 'coin', err: true, type: 'DUPE_COIN', payload: { coin, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
            else {
                seenTxCoins[coin.mintTxid] = seenTxCoins[coin.mintTxid] || {};
                seenTxCoins[coin.mintTxid][coin.mintIndex] = coin;
            }
        }
        const mintHeights = (0, utils_1.uniq)(coinsForTx.map(c => c.mintHeight));
        if (mintHeights.length > 1) {
            success = false;
            const error = { model: 'coin', err: true, type: 'COIN_HEIGHT_MISMATCH', payload: { blockNum } };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
        }
        for (const txid of Object.keys(seenTxs)) {
            const coins = seenTxCoins[txid];
            if (!coins) {
                success = false;
                const error = { model: 'coin', err: true, type: 'MISSING_COIN_FOR_TXID', payload: { txid, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
        }
        for (const txid of Object.keys(seenTxCoins)) {
            const tx = seenTxs[txid];
            const coins = seenTxCoins[txid];
            if (!tx) {
                success = false;
                const error = { model: 'transaction', err: true, type: 'MISSING_TX', payload: { txid, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
            else {
                const sum = Object.values(coins).reduce((prev, cur) => prev + cur.value, 0);
                if (sum != tx.value) {
                    success = false;
                    const error = {
                        model: 'coin+transactions',
                        err: true,
                        type: 'VALUE_MISMATCH',
                        payload: { tx, coins, blockNum }
                    };
                    errors.push(error);
                    if (log) {
                        console.log(JSON.stringify(error));
                    }
                }
            }
        }
        if (blocksForHeight === 0) {
            success = false;
            const error = {
                model: 'block',
                err: true,
                type: 'MISSING_BLOCK',
                payload: { blockNum }
            };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
        }
        if (blocksForHeight > 1) {
            success = false;
            const error = {
                model: 'block',
                err: true,
                type: 'DUPE_BLOCKHEIGHT',
                payload: { blockNum, blocksForHeight }
            };
            errors.push(error);
            if (log) {
                console.log(JSON.stringify(error));
            }
        }
        // blocks with same hash
        if (blockTxs.length > 0) {
            const hashFromTx = blockTxs[0].blockHash;
            if (blocksForHash > 1) {
                success = false;
                const error = { model: 'block', err: true, type: 'DUPE_BLOCKHASH', payload: { hash: hashFromTx, blockNum } };
                errors.push(error);
                if (log) {
                    console.log(JSON.stringify(error));
                }
            }
        }
        return { success, errors };
    }
}
exports.VerificationPeer = VerificationPeer;
//# sourceMappingURL=VerificationPeer.js.map