bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
330 lines • 17.8 kB
JavaScript
"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