bitcore-node
Version:
A blockchain indexing node with extended capabilities using bitcore
235 lines (209 loc) • 7.93 kB
text/typescript
import { expect } from 'chai';
import * as crypto from 'crypto';
import { BitcoreLib } from 'crypto-wallet-core';
import { CoinStorage, ICoin } from '../../../src/models/coin';
import { IBtcTransaction, SpendOp, TransactionStorage } from '../../../src/models/transaction';
import { SpentHeightIndicators } from '../../../src/types/Coin';
import { resetDatabase } from '../../helpers';
import { intAfterHelper, intBeforeHelper } from '../../helpers/integration';
import { BitcoinTransaction } from '../../../src/types/namespaces/Bitcoin';
function createNewTxid() {
const seed = (Math.random() * 10000).toString();
return crypto
.createHash('sha256')
.update(seed + 1)
.digest()
.toString('hex');
}
async function addTx(tx: IBtcTransaction, outputs: ICoin[]) {
await TransactionStorage.collection.insertOne(tx as IBtcTransaction);
await CoinStorage.collection.insertMany(outputs as ICoin[]);
}
async function makeMempoolTxChain(chain: string, network: string, startingTxid: string, chainLength = 1) {
let txid = startingTxid;
let nextTxid = createNewTxid();
let allTxids = new Array<string>();
for (let i = 1; i <= chainLength; i++) {
const badMempoolTx = {
chain,
network,
blockHeight: -1,
txid
};
const badMempoolOutputs = [
{
chain,
network,
mintHeight: -1,
mintTxid: txid,
spentTxid: i != chainLength ? nextTxid : '',
mintIndex: 0,
spentHeight: -1
}
];
await addTx(badMempoolTx as IBtcTransaction, badMempoolOutputs as ICoin[]);
allTxids.push(txid);
txid = nextTxid;
nextTxid = createNewTxid();
}
return allTxids;
}
describe('Coin Model', function() {
const suite = this;
this.timeout(30000);
before(intBeforeHelper);
after(async () => intAfterHelper(suite));
beforeEach(async () => {
await resetDatabase();
});
const chain = 'BTC';
const network = 'integration';
const blockTx = {
chain,
network,
blockHeight: 1,
txid: '01234'
};
const blockTxOutputs = {
chain,
network,
mintHeight: 1,
mintTxid: '01234',
mintIndex: 0,
spentHeight: -1,
spentTxid: '12345'
};
const block2TxOutputs = {
chain,
network,
mintHeight: 2,
mintTxid: '123456',
mintIndex: 0,
spentHeight: -1
};
it('should appropriately mark coins related to transactions that are in mempool, but no longer valid', async () => {
// insert a valid tx, with a valid output
await TransactionStorage.collection.insertOne(blockTx as IBtcTransaction);
await CoinStorage.collection.insertOne(blockTxOutputs as ICoin);
const chainLength = 5;
const txids = await makeMempoolTxChain(chain, network, blockTxOutputs.spentTxid, chainLength);
const allRelatedCoins = await TransactionStorage.findAllRelatedOutputs(blockTxOutputs.spentTxid);
expect(allRelatedCoins.length).to.eq(chainLength);
const spentOps = new Array<SpendOp>();
spentOps.push({
updateOne: {
filter: {
chain,
network,
mintIndex: blockTxOutputs.mintIndex,
mintTxid: blockTxOutputs.mintTxid,
spentHeight: { $lt: 0 }
},
update: { $set: { spentHeight: block2TxOutputs.mintHeight, spentTxid: block2TxOutputs.mintTxid } }
}
});
await TransactionStorage.pruneMempool({
chain,
network,
initialSyncComplete: true,
spendOps: spentOps
});
const badTxs = await TransactionStorage.collection.find({ chain, network, txid: { $in: txids } }).toArray();
expect(badTxs.length).to.eq(chainLength);
// the replaced tx is marked as conflicting, all the rest still pending to be cleaned up by pruning service
expect(badTxs[0].blockHeight).to.eq(SpentHeightIndicators.conflicting);
expect(badTxs[0].replacedByTxid).to.exist;
expect(badTxs.slice(1).every(tx => tx.blockHeight === SpentHeightIndicators.pending)).to.equal(true);
const goodTxs = await TransactionStorage.collection.find({ chain, network, txid: blockTx.txid }).toArray();
expect(goodTxs.length).to.eq(1);
expect(goodTxs[0].txid).to.eq(blockTx.txid);
expect(goodTxs[0].blockHeight).to.eq(blockTx.blockHeight);
// Coins
const badNewCoins = await CoinStorage.collection.find({ chain, network, mintTxid: { $in: txids } }).toArray();
expect(badNewCoins.length).to.equal(badNewCoins.filter(c => c.spentHeight == SpentHeightIndicators.pending).length);
const goodNewCoins = await CoinStorage.collection.find({ chain, network, mintTxid: blockTx.txid }).toArray();
expect(goodNewCoins.length).to.equal(
goodNewCoins.filter(c => c.spentHeight == SpentHeightIndicators.unspent).length
);
});
it('should appropriately mark coins related to transactions that are RBFed', async () => {
const privateKey = new BitcoreLib.PrivateKey('L1uyy5qTuGrVXrmrsvHWHgVzW9kKdrp27wBC7Vs6nZDTF2BRUVwy');
const utxo1 = {
txId: createNewTxid(),
outputIndex: 0,
address: '17XBj6iFEsf8kzDMGQk5ghZipxX49VXuaV',
script: '76a91447862fe165e6121af80d5dde1ecb478ed170565b88ac',
satoshis: 50000
};
// create tx with mutliple outputs
const tx1 = new BitcoreLib.Transaction()
.from(utxo1)
.to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 15000)
.to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 13000)
.to('1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK', 11000)
.sign(privateKey) as BitcoinTransaction;
// import transaction in block 1
await TransactionStorage.batchImport({
txs: [tx1],
height: 1,
initialSyncComplete: true,
chain: 'BTC',
network: 'integration'
});
// insert mempool tx using all outputs from last tx
const mempoolTxid = createNewTxid();
const mempoolTx = {
chain,
network,
blockHeight: -1, // pending
txid: mempoolTxid
} as IBtcTransaction;
const mempoolOutputs = Array.from({ length: 3 }, (_v, i) => i).map(i => {
return {
chain,
network,
mintHeight: -1,
mintTxid: mempoolTxid,
mintIndex: i,
spentHeight: SpentHeightIndicators.unspent
} as ICoin;
});
await addTx(mempoolTx, mempoolOutputs);
// update existing outputs to be spent by mempool tx
await CoinStorage.collection.updateMany(
{ chain, network, mintTxid: tx1.hash },
{ $set: { spentTxid: mempoolTxid, spentHeight: SpentHeightIndicators.pending } }
);
// create new tx that uses one of the inputs
const utxo2 = [
{
txId: tx1.hash,
outputIndex: 0,
address: '1Gokm82v6DmtwKEB8AiVhm82hyFSsEvBDK',
script: '76a91447862fe165e6121af80d5dde1ecb478ed170565b88ac',
satoshis: 15000
}
];
const tx2 = new BitcoreLib.Transaction()
.from(utxo2)
.to('bc1qm0jxvjvj6pzcc64lu4k7vccsg2x22pj60zke6c', 15000)
.sign(privateKey) as BitcoinTransaction;
// import transaction in block 2
await TransactionStorage.batchImport({
txs: [tx2],
height: 2,
initialSyncComplete: true,
chain: 'BTC',
network: 'integration'
});
const tx1Outputs = await CoinStorage.collection.find({ chain, network, mintTxid: tx1.hash }).toArray();
const spentCoin = tx1Outputs.find(c => c.spentTxid === tx2.hash && c.spentHeight === 2);
expect(spentCoin).to.exist;
const unspentCoins = tx1Outputs.filter(c => c.spentHeight < SpentHeightIndicators.minimum);
expect(unspentCoins.length).to.equal(2);
expect(unspentCoins.filter(c => c.spentHeight === SpentHeightIndicators.unspent && !c.spentTxid).length).to.equal(2);
const mempoolCoins = await CoinStorage.collection.find({ chain, network, mintTxid: mempoolTxid }).toArray();
expect(mempoolCoins.length).to.equal(3);
expect(mempoolCoins.filter(c => c.mintHeight === SpentHeightIndicators.conflicting).length).to.equal(3);
});
});