UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

330 lines 17.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const bson_1 = require("bson"); const chai_1 = require("chai"); const sinon_1 = __importDefault(require("sinon")); const coin_1 = require("../../../src/models/coin"); const transaction_1 = require("../../../src/models/transaction"); const pruning_1 = require("../../../src/services/pruning"); require("../../../src/utils/polyfills"); const helpers_1 = require("../../helpers"); const integration_1 = require("../../helpers/integration"); const rpc_1 = require("../../../src/rpc"); const logger_1 = __importDefault(require("../../../src/logger")); const Pruning = new pruning_1.PruningService({ transactionModel: transaction_1.TransactionStorage, coinModel: coin_1.CoinStorage }); describe('Pruning Service', function () { const suite = this; this.timeout(30000); const sandbox = sinon_1.default.createSandbox(); before(integration_1.intBeforeHelper); after(async () => (0, integration_1.intAfterHelper)(suite)); beforeEach(async () => { await (0, helpers_1.resetDatabase)(); Pruning.lastRunTimeInvalid = 0; Pruning.lastRunTimeOld = 0; Pruning.registerRpcs(); Pruning.rpcs['BTC:mainnet'] = new rpc_1.RPC('user', 'pw', 'host', 'port'); sandbox.stub(Pruning.rpcs['BTC:mainnet'], 'getBlockHeight').resolves(1240); process.env.DRYRUN = 'false'; sandbox.spy(logger_1.default, 'info'); sandbox.spy(logger_1.default, 'warn'); sandbox.spy(logger_1.default, 'error'); }); afterEach(() => { process.env.DRYRUN = undefined; sandbox.restore(); }); const replacementTx = { chain: 'BTC', network: 'mainnet', blockHeight: 1234, txid: 'replacementTx', }; const invalidTx = { chain: 'BTC', network: 'mainnet', blockHeight: -3 /* SpentHeightIndicators.conflicting */, txid: 'invalidCoin', replacedByTxid: 'replacementTx' }; const invalidCoin = { chain: 'BTC', network: 'mainnet', mintHeight: -1 /* SpentHeightIndicators.pending */, mintTxid: 'invalidCoin', spentHeight: -1 /* SpentHeightIndicators.pending */, spentTxid: 'spentInMempool' }; const mempoolCoin = { chain: 'BTC', network: 'mainnet', mintHeight: -1 /* SpentHeightIndicators.pending */, mintTxid: 'spentInMempool', spentHeight: -1 /* SpentHeightIndicators.pending */, spentTxid: 'spentInMempoolAgain' }; const mempoolCoin2 = { chain: 'BTC', network: 'mainnet', mintHeight: -1 /* SpentHeightIndicators.pending */, mintTxid: 'spentInMempoolAgain' }; const oldMempoolTx = { chain: 'BTC', network: 'mainnet', blockHeight: -1 /* SpentHeightIndicators.pending */, txid: 'oldMempoolTx', blockTimeNormalized: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }; const oldMempoolTxOutput = { chain: 'BTC', network: 'mainnet', mintHeight: -1 /* SpentHeightIndicators.pending */, mintTxid: 'oldMempoolTx', spentTxid: 'oldMempoolTx2' }; const oldMempoolTx2Output = { chain: 'BTC', network: 'mainnet', mintHeight: -1 /* SpentHeightIndicators.pending */, mintTxid: 'oldMempoolTx2', spentTxid: '' }; const parentTxOutput1 = { chain: 'BTC', network: 'mainnet', mintHeight: 1234, mintTxid: 'parentTx', spentHeight: -1 /* SpentHeightIndicators.pending */, spentTxid: 'oldMempoolTx' // this output is an input for oldMempoolTx }; const parentTxOutput2 = { chain: 'BTC', network: 'mainnet', mintHeight: 1234, mintTxid: 'parentTx', spentHeight: -1 /* SpentHeightIndicators.pending */, spentTxid: 'imaginaryTx' // another imaginary tx spent this output. This is here to make sure we don't mark this output as unspent }; function modTxid(orig, i) { return orig + (i ? '_' + i : ''); } async function insertBadCoins() { await transaction_1.TransactionStorage.collection.insertOne(invalidTx); await coin_1.CoinStorage.collection.insertOne(invalidCoin); await coin_1.CoinStorage.collection.insertOne(mempoolCoin); await coin_1.CoinStorage.collection.insertOne(mempoolCoin2); await transaction_1.TransactionStorage.collection.insertOne(replacementTx); return [invalidCoin, mempoolCoin, mempoolCoin2]; } async function insertOldTx(i = 0) { await coin_1.CoinStorage.collection.insertOne({ ...parentTxOutput1, spentTxid: modTxid(parentTxOutput1.spentTxid, i) }); await coin_1.CoinStorage.collection.insertOne({ ...parentTxOutput2, spentTxid: modTxid(parentTxOutput2.spentTxid, i) }); await transaction_1.TransactionStorage.collection.insertOne({ ...oldMempoolTx, txid: modTxid(oldMempoolTx.txid, i) }); await coin_1.CoinStorage.collection.insertOne({ ...oldMempoolTxOutput, mintTxid: modTxid(oldMempoolTxOutput.mintTxid, i), spentTxid: modTxid(oldMempoolTxOutput.spentTxid, i) }); await coin_1.CoinStorage.collection.insertOne({ ...oldMempoolTx2Output, mintTxid: modTxid(oldMempoolTx2Output.mintTxid, i) }); } describe('processAllInvalidTxs', function () { it('should detect coins that should be invalid but are not', async () => { await insertBadCoins(); const { chain, network } = invalidCoin; await Pruning.processAllInvalidTxs(chain, network); const shouldBeInvalid = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } }) .toArray(); for (const coin of shouldBeInvalid) { (0, chai_1.expect)(coin.mintHeight).eq(-3 /* SpentHeightIndicators.conflicting */); } (0, chai_1.expect)(shouldBeInvalid.length).eq(3); }); it('should mark detected coins as invalid', async () => { await insertBadCoins(); const { chain, network } = invalidCoin; await Pruning.processAllInvalidTxs(chain, network); const shouldBeInvalid = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } }) .toArray(); for (const coin of shouldBeInvalid) { (0, chai_1.expect)(coin.mintHeight).eq(-3 /* SpentHeightIndicators.conflicting */); } (0, chai_1.expect)(shouldBeInvalid.length).eq(3); }); it('should not invalidate a mined tx', async function () { await insertBadCoins(); const { chain, network } = invalidCoin; await Pruning.invalidateTx(chain, network, replacementTx); const shouldBeInvalidStill = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } }) .toArray(); (0, chai_1.expect)(shouldBeInvalidStill.every(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */)).to.equal(true); (0, chai_1.expect)(shouldBeInvalidStill.length).eq(3); const msg = `Tx ${replacementTx.txid} is already mined`; (0, chai_1.expect)(logger_1.default.warn.args.find((args) => args[0] === msg)).to.exist; }); it('should not go into an infinite loop if there is a circular replacedByTxid reference', async function () { await insertBadCoins(); await transaction_1.TransactionStorage.collection.insertOne({ ...replacementTx, _id: new bson_1.ObjectId(), txid: 'replacementTx2', blockHeight: -1 /* SpentHeightIndicators.pending */, replacedByTxid: replacementTx.txid }); await transaction_1.TransactionStorage.collection.updateOne({ txid: replacementTx.txid }, { $set: { replacedByTxid: 'replacementTx2' } }); // at this point, invalidTx => replacementTx => replacementTx2 => replacementTx const { chain, network } = invalidCoin; await Pruning.processAllInvalidTxs(chain, network); const shouldBeInvalid = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } }) .toArray(); (0, chai_1.expect)(shouldBeInvalid.every(coin => coin.mintHeight === -3 /* SpentHeightIndicators.conflicting */)).to.equal(true); (0, chai_1.expect)(shouldBeInvalid.length).eq(3); }); it('should not go into an infinite loop if there is a circular, unconfirmed replacedByTxid reference', async function () { await insertBadCoins(); await transaction_1.TransactionStorage.collection.insertOne({ ...replacementTx, _id: new bson_1.ObjectId(), txid: 'replacementTx2', blockHeight: -1 /* SpentHeightIndicators.pending */, replacedByTxid: replacementTx.txid }); await transaction_1.TransactionStorage.collection.updateOne({ txid: replacementTx.txid }, { $set: { replacedByTxid: 'replacementTx2', blockHeight: -1 /* SpentHeightIndicators.pending */ } }); // at this point, invalidTx => replacementTx => replacementTx2 => replacementTx // but replacementTx is still unconfirmed const { chain, network } = invalidCoin; await Pruning.processAllInvalidTxs(chain, network); const shouldBePendingStill = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [invalidCoin.mintTxid, mempoolCoin.mintTxid, mempoolCoin2.mintTxid] } }) .toArray(); (0, chai_1.expect)(shouldBePendingStill.every(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */)).to.equal(true); (0, chai_1.expect)(shouldBePendingStill.length).eq(3); const msg = `Skipping invalidation of ${invalidTx.txid} with immature replacement => ${invalidTx.replacedByTxid}`; (0, chai_1.expect)(logger_1.default.info.args.find((args) => args[0] === msg)).to.exist; }); }); describe('processOldMempoolTxs', function () { it('should remove old transactions', async () => { sandbox.stub(rpc_1.RPC.prototype, 'getTransaction').resolves(null); await insertOldTx(); const { chain, network } = oldMempoolTx; const count = await transaction_1.TransactionStorage.collection.countDocuments({ chain, network, blockHeight: -1, blockTimeNormalized: { $lt: new Date() } }); (0, chai_1.expect)(count).eq(1); await Pruning.processOldMempoolTxs(chain, network, 29); const shouldBeExpiredTx = await transaction_1.TransactionStorage.collection .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } }) .toArray(); const shouldBeExpiredCoins = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } }) .toArray(); const parentTxOutputs = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: parentTxOutput1.mintTxid }) .toArray(); (0, chai_1.expect)(shouldBeExpiredTx.length).eq(1); (0, chai_1.expect)(shouldBeExpiredTx.every(tx => tx.blockHeight === -5 /* SpentHeightIndicators.expired */)).to.equal(true); (0, chai_1.expect)(shouldBeExpiredCoins.length).eq(2); (0, chai_1.expect)(shouldBeExpiredCoins.every(coin => coin.mintHeight === -5 /* SpentHeightIndicators.expired */)).to.equal(true); (0, chai_1.expect)(parentTxOutputs.length).eq(2); (0, chai_1.expect)(parentTxOutputs.filter(coin => coin.spentHeight === -2 /* SpentHeightIndicators.unspent */).length).to.equal(1); }); it('should skip removing transactions still in mempool', async () => { const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction'); rpcStub.onCall(0).resolves(null); rpcStub.onCall(1).resolves({}); rpcStub.onCall(2).resolves(null); await insertOldTx(0); await insertOldTx(1); await insertOldTx(2); const { chain, network } = oldMempoolTx; const count = await transaction_1.TransactionStorage.collection.countDocuments({ chain, network, blockHeight: -1 /* SpentHeightIndicators.pending */, blockTimeNormalized: { $lt: new Date() } }); (0, chai_1.expect)(count).eq(3); await Pruning.processOldMempoolTxs(chain, network, 29); const processedTxs = await transaction_1.TransactionStorage.collection .find({ chain, network, txid: { $in: [modTxid(oldMempoolTx.txid, 0), modTxid(oldMempoolTx.txid, 1), modTxid(oldMempoolTx.txid, 2)] } }) .toArray(); const processedCoins = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [modTxid(oldMempoolTxOutput.mintTxid, 0), modTxid(oldMempoolTx2Output.mintTxid, 0), modTxid(oldMempoolTxOutput.mintTxid, 1), modTxid(oldMempoolTx2Output.mintTxid, 1), modTxid(oldMempoolTxOutput.mintTxid, 2), modTxid(oldMempoolTx2Output.mintTxid, 2)] } }) .toArray(); (0, chai_1.expect)(processedTxs.length).eq(3); (0, chai_1.expect)(processedTxs.filter(tx => tx.blockHeight === -5 /* SpentHeightIndicators.expired */).length).eq(2); (0, chai_1.expect)(processedTxs.filter(tx => tx.blockHeight === -1 /* SpentHeightIndicators.pending */).length).eq(1); // still in mempool (0, chai_1.expect)(processedCoins.length).eq(6); (0, chai_1.expect)(processedCoins.filter(coin => coin.mintHeight === -5 /* SpentHeightIndicators.expired */).length).eq(4); (0, chai_1.expect)(processedCoins.filter(coin => coin.mintHeight === -1 /* SpentHeightIndicators.pending */).length).eq(2); // still in mempool }); it('should skip removing transactions on rpc error', async () => { const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction'); rpcStub.onCall(0).rejects({ code: -1, message: 'hahaha' }); await insertOldTx(); const { chain, network } = oldMempoolTx; const count = await transaction_1.TransactionStorage.collection.countDocuments({ chain, network, blockHeight: -1 /* SpentHeightIndicators.pending */, blockTimeNormalized: { $lt: new Date() } }); (0, chai_1.expect)(count).eq(1); await Pruning.processOldMempoolTxs(chain, network, 29); const shouldBeGoneTx = await transaction_1.TransactionStorage.collection .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } }) .toArray(); const shouldBeGoneCoins = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } }) .toArray(); (0, chai_1.expect)(shouldBeGoneTx.length).eq(1); (0, chai_1.expect)(shouldBeGoneCoins.length).eq(2); }); it('should skip removing transactions if coin has >0 confs', async () => { const rpcStub = sandbox.stub(rpc_1.RPC.prototype, 'getTransaction'); rpcStub.onCall(0).rejects({ code: -5, message: 'already exists' }); const oldMempoolTx2OutputHeight = oldMempoolTx2Output.mintHeight; oldMempoolTx2Output.mintHeight = 1; await insertOldTx(); oldMempoolTx2Output.mintHeight = oldMempoolTx2OutputHeight; // reset const { chain, network } = oldMempoolTx; const count = await transaction_1.TransactionStorage.collection.countDocuments({ chain, network, blockHeight: -1 /* SpentHeightIndicators.pending */, blockTimeNormalized: { $lt: new Date() } }); (0, chai_1.expect)(count).eq(1); await Pruning.processOldMempoolTxs(chain, network, 29); const shouldBeGoneTx = await transaction_1.TransactionStorage.collection .find({ chain, network, txid: { $in: [oldMempoolTx.txid] } }) .toArray(); const shouldBeGoneCoins = await coin_1.CoinStorage.collection .find({ chain, network, mintTxid: { $in: [oldMempoolTxOutput.mintTxid, oldMempoolTx2Output.mintTxid] } }) .toArray(); (0, chai_1.expect)(shouldBeGoneTx.length).eq(1); (0, chai_1.expect)(shouldBeGoneCoins.length).eq(2); }); }); }); //# sourceMappingURL=pruning.integration.js.map