From 01b96a21b97efb80ce90756e0bb0d17eb815c05e Mon Sep 17 00:00:00 2001 From: Jason Dreyzehner Date: Wed, 19 Sep 2018 16:37:04 -0400 Subject: [PATCH] refactor(coin): identify, document SpentHeightIndicators --- packages/bitcore-node/src/models/block.ts | 4 +- packages/bitcore-node/src/models/coin.ts | 19 +++ .../bitcore-node/src/models/transaction.ts | 31 +++-- .../chain-state/internal/internal.ts | 8 +- packages/bitcore-node/src/services/p2p.ts | 3 +- .../integration/models/block.integration.ts | 131 ++++++++++-------- .../test/unit/models/coin.unit.ts | 10 +- 7 files changed, 123 insertions(+), 83 deletions(-) diff --git a/packages/bitcore-node/src/models/block.ts b/packages/bitcore-node/src/models/block.ts index 9c651ff3796..a599936749a 100644 --- a/packages/bitcore-node/src/models/block.ts +++ b/packages/bitcore-node/src/models/block.ts @@ -1,4 +1,4 @@ -import { CoinModel } from './coin'; +import { CoinModel, SpentHeightIndicators } from './coin'; import { TransactionModel } from './transaction'; import { TransformOptions } from '../types/TransformOptions'; import { LoggifyClass } from '../decorators/Loggify'; @@ -149,7 +149,7 @@ export class Block extends BaseModel { await CoinModel.collection.deleteMany({ chain, network, mintHeight: { $gte: localTip.height } }); await CoinModel.collection.updateMany( { chain, network, spentHeight: { $gte: localTip.height } }, - { $set: { spentTxid: null, spentHeight: -1 } } + { $set: { spentTxid: null, spentHeight: SpentHeightIndicators.pending } } ); logger.debug('Removed data from above blockHeight: ', localTip.height); diff --git a/packages/bitcore-node/src/models/coin.ts b/packages/bitcore-node/src/models/coin.ts index eed74a115cc..288e0831dc7 100644 --- a/packages/bitcore-node/src/models/coin.ts +++ b/packages/bitcore-node/src/models/coin.ts @@ -17,6 +17,25 @@ export type ICoin = { spentHeight: number; }; +/** + * Number values less than 0 which indicate the spent state of a coin. + */ +export enum SpentHeightIndicators { + /** + * The value below which numbers are simply used as indicators. + */ + minimum = 0, + /** + * The coin is spent by a transaction currently in the mempool but not yet + * included in a block. + */ + pending = -1, + /** + * The coin is unspent, and no transactions spending it have been seen. + */ + unspent = -2 +} + @LoggifyClass class Coin extends BaseModel { constructor() { diff --git a/packages/bitcore-node/src/models/transaction.ts b/packages/bitcore-node/src/models/transaction.ts index 84736443a6c..a67856801e6 100644 --- a/packages/bitcore-node/src/models/transaction.ts +++ b/packages/bitcore-node/src/models/transaction.ts @@ -1,4 +1,4 @@ -import { CoinModel, ICoin } from './coin'; +import { CoinModel, ICoin, SpentHeightIndicators } from './coin'; import { WalletAddressModel } from './walletAddress'; import { partition } from '../utils/partition'; import { ObjectID } from 'bson'; @@ -81,7 +81,9 @@ export class Transaction extends BaseModel { let txOps = await this.addTransactions(params); logger.debug('Writing Transactions', txOps.length); const txBatches = partition(txOps, txOps.length / config.maxPoolSize); - txs = txBatches.map((txBatch: Array) => this.collection.bulkWrite(txBatch, { ordered: false, j: false, w: 0 })); + txs = txBatches.map((txBatch: Array) => + this.collection.bulkWrite(txBatch, { ordered: false, j: false, w: 0 }) + ); } await Promise.all(txs); @@ -107,7 +109,7 @@ export class Transaction extends BaseModel { let mintWallets; let spentWallets; - if (initialSyncComplete){ + if (initialSyncComplete) { mintWallets = await CoinModel.collection .aggregate([ { $match: { mintTxid: { $in: txids }, chain, network } }, @@ -118,17 +120,16 @@ export class Transaction extends BaseModel { spentWallets = await CoinModel.collection .aggregate([ - { $match: { spentTxid: { $in: txids }, chain, network } }, + { $match: { spentTxid: { $in: txids }, chain, network } }, { $unwind: '$wallets' }, { $group: { _id: '$spentTxid', wallets: { $addToSet: '$wallets' } } } ]) .toArray(); } - let txOps = txs.map((tx, index) => { let wallets = new Array(); - if (initialSyncComplete){ + if (initialSyncComplete) { for (let wallet of mintWallets.concat(spentWallets).filter(wallet => wallet._id === txids[index])) { for (let walletMatch of wallet.wallets) { if (!wallets.find(wallet => wallet.toHexString() === walletMatch.toHexString())) { @@ -182,7 +183,7 @@ export class Transaction extends BaseModel { chain: parentChain, network, mintHeight: height, - spentHeight: { $gt: -2, $lt: forkHeight } + spentHeight: { $gt: SpentHeightIndicators.unspent, $lt: forkHeight } }) .toArray(); } @@ -209,7 +210,13 @@ export class Transaction extends BaseModel { mintOps.push({ updateOne: { - filter: { mintTxid: txid, mintIndex: index, spentHeight: { $lt: 0 }, chain, network }, + filter: { + mintTxid: txid, + mintIndex: index, + spentHeight: { $lt: SpentHeightIndicators.minimum }, + chain, + network + }, update: { $set: { chain, @@ -219,7 +226,7 @@ export class Transaction extends BaseModel { value: output.satoshis, address, script: scriptBuffer, - spentHeight: -2, + spentHeight: SpentHeightIndicators.unspent, wallets: [] } }, @@ -258,7 +265,7 @@ export class Transaction extends BaseModel { network: string; mintOps?: Array; }): Array { - let { chain, network, height, txs, parentChain, forkHeight, mintOps=[] } = params; + let { chain, network, height, txs, parentChain, forkHeight, mintOps = [] } = params; let spendOps: any[] = []; if (parentChain && forkHeight && height < forkHeight) { return spendOps; @@ -276,7 +283,7 @@ export class Transaction extends BaseModel { for (let input of tx.inputs) { let inputObj = input.toObject(); let sameBlockSpend = mintMap[inputObj.prevTxId] && mintMap[inputObj.prevTxId][inputObj.outputIndex]; - if (sameBlockSpend){ + if (sameBlockSpend) { sameBlockSpend.updateOne.update.$set.spentHeight = height; sameBlockSpend.updateOne.update.$set.spentTxid = txid; if (config.pruneSpentScripts && height > 0) { @@ -289,7 +296,7 @@ export class Transaction extends BaseModel { filter: { mintTxid: inputObj.prevTxId, mintIndex: inputObj.outputIndex, - spentHeight: { $lt: 0 }, + spentHeight: { $lt: SpentHeightIndicators.minimum }, chain, network }, diff --git a/packages/bitcore-node/src/providers/chain-state/internal/internal.ts b/packages/bitcore-node/src/providers/chain-state/internal/internal.ts index 316ee78cb3d..50a77cef7c8 100644 --- a/packages/bitcore-node/src/providers/chain-state/internal/internal.ts +++ b/packages/bitcore-node/src/providers/chain-state/internal/internal.ts @@ -3,7 +3,7 @@ import through2 from 'through2'; import { MongoBound } from '../../../models/base'; import { ObjectId } from 'mongodb'; -import { CoinModel, ICoin } from '../../../models/coin'; +import { CoinModel, ICoin, SpentHeightIndicators } from '../../../models/coin'; import { BlockModel, IBlock } from '../../../models/block'; import { WalletModel, IWallet } from '../../../models/wallet'; import { WalletAddressModel } from '../../../models/walletAddress'; @@ -40,7 +40,7 @@ export class InternalStateProvider implements CSP.IChainStateService { } const query = { chain: chain, network: network.toLowerCase(), address } as any; if (args.unspent) { - query.spentHeight = { $lt: 0 }; + query.spentHeight = { $lt: SpentHeightIndicators.minimum }; } return query; } @@ -230,7 +230,7 @@ export class InternalStateProvider implements CSP.IChainStateService { const { chain, network, pubKey, stream } = params; const wallet = await WalletModel.collection.findOne({ pubKey }); const walletId = wallet!._id; - const query = { chain, network, wallets: walletId, spentHeight: { $gte: 0 } }; + const query = { chain, network, wallets: walletId, spentHeight: { $gte: SpentHeightIndicators.minimum } }; const cursor = CoinModel.collection.find(query); const seen = {}; const stringifyWallets = (wallets: Array) => wallets.map(w => w.toHexString()); @@ -311,7 +311,7 @@ export class InternalStateProvider implements CSP.IChainStateService { const { wallet, limit, args = {}, stream } = params; let query: any = { wallets: wallet._id }; if (args.includeSpent !== 'true') { - query.spentHeight = { $lt: -1 }; + query.spentHeight = { $lt: SpentHeightIndicators.pending }; } const tip = await this.getLocalTip(params); const tipHeight = tip ? tip.height : 0; diff --git a/packages/bitcore-node/src/services/p2p.ts b/packages/bitcore-node/src/services/p2p.ts index 4374b411ffc..f24fd250159 100644 --- a/packages/bitcore-node/src/services/p2p.ts +++ b/packages/bitcore-node/src/services/p2p.ts @@ -6,6 +6,7 @@ import { ChainStateProvider } from '../providers/chain-state'; import { TransactionModel } from '../models/transaction'; import { Bitcoin } from '../types/namespaces/Bitcoin'; import { StateModel } from '../models/state'; +import { SpentHeightIndicators } from '../models/coin'; const Chain = require('../chain'); const LRU = require('lru-cache'); @@ -235,7 +236,7 @@ export class P2pService { chain: this.chain, network: this.network, txs: [tx], - height: -1, + height: SpentHeightIndicators.pending, mempoolTime: now, blockTime: now, blockTimeNormalized: now, diff --git a/packages/bitcore-node/test/integration/models/block.integration.ts b/packages/bitcore-node/test/integration/models/block.integration.ts index 197b36aae0f..36a74eb57f3 100644 --- a/packages/bitcore-node/test/integration/models/block.integration.ts +++ b/packages/bitcore-node/test/integration/models/block.integration.ts @@ -2,11 +2,11 @@ import { expect } from 'chai'; import { resetDatabase } from '../../helpers'; import { BlockModel } from '../../../src/models/block'; import { TransactionModel } from '../../../src/models/transaction'; -import { CoinModel } from '../../../src/models/coin'; +import { CoinModel, SpentHeightIndicators } from '../../../src/models/coin'; import { TEST_BLOCK } from '../../data/test-block'; import logger from '../../../src/logger'; -describe('Block Model', function () { +describe('Block Model', function() { beforeEach(async () => { await resetDatabase(); }); @@ -53,7 +53,7 @@ describe('Block Model', function () { previousBlockHash: '2a883ff89c7d6e9302bb4a4634cd580319a4fd59d69e979b344972b0ba042b86', size: 264, bits: parseInt('207fffff', 16).toString(), - processed: true, + processed: true }); await BlockModel.collection.insertOne({ chain: 'BTC', @@ -68,12 +68,15 @@ describe('Block Model', function () { previousBlockHash: '3279069d22ce5af68ef38332d5b40e79e1964b154d466e7fa233015a34c27312', size: 264, bits: parseInt('207fffff', 16).toString(), - processed: true, + processed: true }); await BlockModel.addBlock({ block: TEST_BLOCK, chain: 'BTC', network: 'regtest', initialSyncComplete: false }); - const blocks = await BlockModel.collection.find({ chain: 'BTC', network: 'regtest' }).sort({ height: 1 }).toArray(); + const blocks = await BlockModel.collection + .find({ chain: 'BTC', network: 'regtest' }) + .sort({ height: 1 }) + .toArray(); expect(blocks.length).to.equal(5); const ownBlock = blocks[4]; expect(ownBlock.chain).to.equal('BTC'); @@ -93,11 +96,13 @@ describe('Block Model', function () { logger.info(`new block was successfully added with hash`, ownBlock.hash); - const transaction = await TransactionModel.collection.find({ - chain: 'BTC', - network: 'regtest', - blockHash: '64bfb3eda276ae4ae5b64d9e36c9c0b629bc767fb7ae66f9d55d2c5c8103a929' - }).toArray(); + const transaction = await TransactionModel.collection + .find({ + chain: 'BTC', + network: 'regtest', + blockHash: '64bfb3eda276ae4ae5b64d9e36c9c0b629bc767fb7ae66f9d55d2c5c8103a929' + }) + .toArray(); expect(transaction.length).to.equal(1); expect(transaction[0].chain).to.equal('BTC'); expect(transaction[0].network).to.equal('regtest'); @@ -111,13 +116,11 @@ describe('Block Model', function () { expect(transaction[0].wallets.length).to.equal(0); logger.info(`tx: ${transaction[0].txid} was successfully stored in the TX model`); - }); }); describe('handleReorg', () => { - it('should not reorg if the incoming block\'s prevHash matches the block hash of the current highest block', async () => { - + it("should not reorg if the incoming block's prevHash matches the block hash of the current highest block", async () => { await BlockModel.collection.insertOne({ chain: 'BTC', network: 'regtest', @@ -158,7 +161,7 @@ describe('Block Model', function () { previousBlockHash: '2a883ff89c7d6e9302bb4a4634cd580319a4fd59d69e979b344972b0ba042b86', size: 264, bits: parseInt('207fffff', 16).toString(), - processed: true, + processed: true }); await BlockModel.handleReorg({ @@ -177,10 +180,8 @@ describe('Block Model', function () { const result = await BlockModel.collection.find({ chain: 'BTC', network: 'regtest' }).toArray(); expect(result.length).to.equal(3); - }); it('should not reorg if localTip height is zero', async () => { - await BlockModel.handleReorg({ header: { prevHash: '12c719927ce18f9a61d7c5a7af08d3110cacfa43671aa700956c3c05ed38bdaa', @@ -197,7 +198,6 @@ describe('Block Model', function () { const result = await BlockModel.collection.find({ chain: 'BTC', network: 'regtest' }).toArray(); expect(result.length).to.equal(0); - }); it('should successfully handle reorg', async () => { // setting the Block model @@ -241,7 +241,7 @@ describe('Block Model', function () { previousBlockHash: '2a883ff89c7d6e9302bb4a4634cd580319a4fd59d69e979b344972b0ba042b86', size: 264, bits: parseInt('207fffff', 16).toString(), - processed: true, + processed: true }); // setting TX model @@ -328,60 +328,74 @@ describe('Block Model', function () { }); // check for removed block after Reorg in db - const blocks = await BlockModel.collection.find({ - chain: 'BTC', - network: 'regtest' - }).toArray(); + const blocks = await BlockModel.collection + .find({ + chain: 'BTC', + network: 'regtest' + }) + .toArray(); expect(blocks.length).to.equal(2); - const removedBlock = await BlockModel.collection.find({ - chain: 'BTC', - network: 'regtest', - height: { - $gte: 7 - } - }).toArray(); + const removedBlock = await BlockModel.collection + .find({ + chain: 'BTC', + network: 'regtest', + height: { + $gte: 7 + } + }) + .toArray(); expect(removedBlock.length).to.equal(0); // check for removed tx after Reorg in db - const transaction = await TransactionModel.collection.find({ - chain: 'BTC', - network: 'regtest' - }).toArray(); + const transaction = await TransactionModel.collection + .find({ + chain: 'BTC', + network: 'regtest' + }) + .toArray(); expect(transaction.length).to.equal(1); - const removedTransaction = await TransactionModel.collection.find({ - chain: 'BTC', - network: 'regtest', - blockHeight: { - $gte: 7 - } - }).toArray(); + const removedTransaction = await TransactionModel.collection + .find({ + chain: 'BTC', + network: 'regtest', + blockHeight: { + $gte: 7 + } + }) + .toArray(); expect(removedTransaction.length).to.equal(0); // check for removed coin after Reorg in db - const coinModel = await CoinModel.collection.find({ - chain: 'BTC', - network: 'regtest', - }).toArray(); + const coinModel = await CoinModel.collection + .find({ + chain: 'BTC', + network: 'regtest' + }) + .toArray(); expect(coinModel.length).to.equal(1); - const removedCoin = await CoinModel.collection.find({ - chain: 'BTC', - network: 'regtest', - mintHeight: { - $gte: 7 - } - }).toArray(); + const removedCoin = await CoinModel.collection + .find({ + chain: 'BTC', + network: 'regtest', + mintHeight: { + $gte: 7 + } + }) + .toArray(); expect(removedCoin.length).to.equal(0); // check for unspent coins in the db - const unspentCoins = await CoinModel.collection.find({ - chain: 'BTC', - network: 'regtest', - spentTxid: null, - spentHeight: -1 - }).toArray(); + const unspentCoins = await CoinModel.collection + .find({ + chain: 'BTC', + network: 'regtest', + spentTxid: null, + spentHeight: SpentHeightIndicators.pending + }) + .toArray(); expect(unspentCoins.length).equal(1); expect(unspentCoins[0].chain).to.equal('BTC'); expect(unspentCoins[0].network).to.equal('regtest'); @@ -392,8 +406,7 @@ describe('Block Model', function () { expect(unspentCoins[0].value).to.equal(500.0); expect(unspentCoins[0].address).to.equal('mkjB6LmjiNfJWgH4aP4v1GkFjRcQTfDSfj'); expect(unspentCoins[0].spentTxid).to.equal(null); - expect(unspentCoins[0].spentHeight).to.equal(-1); - + expect(unspentCoins[0].spentHeight).to.equal(SpentHeightIndicators.pending); }); }); }); diff --git a/packages/bitcore-node/test/unit/models/coin.unit.ts b/packages/bitcore-node/test/unit/models/coin.unit.ts index 94b26244d64..b2fdebec49a 100644 --- a/packages/bitcore-node/test/unit/models/coin.unit.ts +++ b/packages/bitcore-node/test/unit/models/coin.unit.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { CoinModel, ICoin } from '../../../src/models/coin'; +import { CoinModel, ICoin, SpentHeightIndicators } from '../../../src/models/coin'; import { ObjectId } from 'mongodb'; describe('Coin Model', function() { @@ -19,7 +19,7 @@ describe('Coin Model', function() { script: Buffer.from(''), wallets: [], spentTxid: '', - spentHeight: -2 + spentHeight: SpentHeightIndicators.unspent } as ICoin; const result = CoinModel._apiTransform(coin, { object: false }); @@ -35,7 +35,7 @@ describe('Coin Model', function() { address: 'n1ojJtS98D2VRLcTkaHH4YXLG4ytCyS7AL', coinbase: true, script: coin.script.toJSON(), - spentHeight: -2, + spentHeight: SpentHeightIndicators.unspent, value: 5000000000.0 }); }); @@ -54,7 +54,7 @@ describe('Coin Model', function() { script: Buffer.from(''), wallets: [], spentTxid: '', - spentHeight: -2 + spentHeight: SpentHeightIndicators.unspent } as ICoin; const result = CoinModel._apiTransform(coin, { object: true }); @@ -65,7 +65,7 @@ describe('Coin Model', function() { vout: 0, spentTxid: '', mintHeight: 1, - spentHeight: -2, + spentHeight: SpentHeightIndicators.unspent, address: 'n1ojJtS98D2VRLcTkaHH4YXLG4ytCyS7AL', coinbase: true, script: coin.script,