UNPKG

bitcore-node

Version:

A blockchain indexing node with extended capabilities using bitcore

250 lines (221 loc) 8.91 kB
#!/usr/bin/env node import { expect } from 'chai'; import config from '../../src/config'; import logger from '../../src/logger'; import { BitcoinBlockStorage, IBtcBlock } from '../../src/models/block'; import { CoinStorage } from '../../src/models/coin'; import { IBtcTransaction, TransactionStorage } from '../../src/models/transaction'; import { WalletAddressStorage } from '../../src/models/walletAddress'; import { ChainStateProvider } from '../../src/providers/chain-state'; import { AsyncRPC } from '../../src/rpc'; import { Storage } from '../../src/services/storage'; import { ChainNetwork } from '../../src/types/ChainNetwork'; import { IUtxoNetworkConfig } from '../../src/types/Config'; const SATOSHI = 100000000.0; export async function blocks( info: ChainNetwork, creds: { username: string; password: string; host: string; port: number | string; } ) { const rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); const tip = await ChainStateProvider.getLocalTip({ chain: info.chain, network: info.network }); const heights = new Array(tip!.height).fill(false); const times = new Array(tip!.height).fill(0); const normalizedTimes = new Array(tip!.height).fill(0); // check each block const cursor = BitcoinBlockStorage.collection.find({ chain: info.chain, network: info.network }); while (await cursor.hasNext()) { const block: IBtcBlock | null = await cursor.next(); if (!block) break; if (!block.processed) continue; logger.info(`verifying block ${block.hash}: ${block.height}`); // Check there's all unique heights expect(block.height, 'block height').to.be.gte(1); expect(block.height, 'block height').to.be.lte(tip!.height); expect(heights[block.height - 1], 'height already used').to.be.false; heights[block.height - 1] = true; // Check times are increasing times[block.height - 1] = block.time.getTime(); normalizedTimes[block.height - 1] = block.timeNormalized.getTime(); const truth = await rpc.verbose_block(block.hash); expect(block.height, 'block height').to.equal(truth.height); expect(block.hash, 'block hash').to.equal(truth.hash); expect(block.version, 'block version').to.equal(truth.version); expect(block.merkleRoot, 'block merkle root').to.equal(truth.merkleroot); expect(block.nonce, 'block nonce').to.equal(truth.nonce); expect(block.previousBlockHash, 'block prev hash').to.equal(truth.previousblockhash); expect(block.transactionCount, 'block tx count').to.equal(truth.tx.length); if (info.network !== 'regtest') { expect(block.size, 'block size').to.equal(truth.size); } expect(block.bits.toString(16), 'block bits').to.equal(truth.bits); expect(block.processed, 'block processed').to.equal(true); expect(block.time.getTime(), 'block time').to.equal(truth.time * 1000); if (block.height < tip!.height) { expect(block.nextBlockHash, 'block next hash').to.equal(truth.nextblockhash); } // Transaction Specifics { const coinbase = truth.tx[0]; // Check reward const reward = coinbase.vout.reduce((a, b) => a + b.value, 0); expect(block.reward, 'block reward').to.equal(Math.round(reward * SATOSHI)); // Check block only has all `truth`'s transactions const ours = await TransactionStorage.collection .find({ chain: info.chain, network: info.network, txid: { $in: truth.tx.map(tx => tx.txid) } }) .project({ txid: true, coinbase: true, blockHash: true, blockHeight: true, blockTime: true, blockTimeNormalized: true }) .toArray(); // Check coinbase flag const ourCoinbase = ours.filter(tx => tx.coinbase); expect(ourCoinbase.length, 'number of coinbases').to.equal(1); expect(ourCoinbase[0].txid, 'coinbase txid to match truth').to.equal(coinbase.txid); // Check both sets of txs are the same size and contain no duplicates const txidset = new Set(ours.map(tx => tx.txid)); expect(ours.length, 'number of txs').to.equal(truth.tx.length); expect(txidset.size, 'number of unique txs').to.equal(truth.tx.length); for (const our of ours) { // Check every one of our txs is contained in `truth` const tx = truth.tx.find(tx => tx.txid === our.txid); expect(tx, 'tx to be in the block').to.not.be.undefined; // Check our txs' block hash matches the mongo block expect(our.blockHash, 'tx block hash').to.equal(block.hash); expect(our.blockHeight, 'tx block height').to.equal(block.height); const time = our.blockTime && our.blockTime.getTime(); expect(time, 'tx block time').to.equal(block.time.getTime()); const ntime = our.blockTimeNormalized && our.blockTimeNormalized.getTime(); expect(ntime, 'tx block time normalized').to.equal(block.timeNormalized.getTime()); } // Check no other tx points to our block hash const extra = await TransactionStorage.collection.countDocuments({ chain: info.chain, network: info.network, blockHash: block.hash, txid: { $nin: truth.tx.map(tx => tx.txid) } }); expect(extra, 'number of extra transactions').to.equal(0); } } // Check the heights are all unique expect(heights.filter(h => !h).length, 'no duplicate heights').to.equal(0); // Check increasing times const increases = l => !!l.reduce((prev, curr) => (prev < curr ? curr : undefined)); expect(increases(normalizedTimes), 'normalized block times only increase').to.be.true; } export async function transactions( info: ChainNetwork, creds: { username: string; password: string; host: string; port: number | string; } ) { const rpc = new AsyncRPC(creds.username, creds.password, creds.host, creds.port); const txcursor = TransactionStorage.collection.find({ chain: info.chain, network: info.network }); while (await txcursor.hasNext()) { const tx: IBtcTransaction | null = await txcursor.next(); if (!tx) { break; } logger.info(`verifying tx ${tx.txid}: ${tx.blockHeight}`); const truth = await rpc.transaction(tx.txid, tx.blockHash); if (info.network !== 'regtest') { expect(tx.size, 'tx size').to.equal(truth.size); } expect(tx.locktime, 'tx locktime').to.equal(truth.locktime); { // Minted by this transaction const ours = await CoinStorage.collection .find({ network: info.network, chain: info.chain, mintTxid: tx.txid }) .toArray(); expect(ours.length, 'number mint txids').to.equal(truth.vout.length); for (const our of ours) { // coins expect(our.mintHeight, 'tx mint height').to.equal(tx.blockHeight); expect(our.value, 'tx mint value').to.equal(Math.round(truth.vout[our.mintIndex].value * SATOSHI)); // TODO: why? if (our.address && our.address !== 'false') { expect(truth.vout[our.mintIndex].scriptPubKey.addresses, 'tx mint address').to.include(our.address); } expect(our.coinbase).to.equal(tx.coinbase); // wallets expect(tx.wallets).to.include.members(Array.from(our.wallets)); if (our.wallets.length > 0) { const wallets = await WalletAddressStorage.collection .find({ wallet: { $in: our.wallets }, address: our.address, chain: info.chain, network: info.network }) .toArray(); expect(wallets.length, 'wallet exists').to.be.greaterThan(0); } } } { // Spent by this transaction const ours = await CoinStorage.collection .find({ network: info.network, chain: info.chain, spentTxid: tx.txid }) .toArray(); const nspent = truth.vin.length + (tx.coinbase ? -1 : 0); expect(ours.length, 'number spent txids').to.equal(nspent); for (const our of ours) { expect(our.spentHeight, 'tx spent height').to.equal(tx.blockHeight); expect(tx.wallets).to.include.members(Array.from(our.wallets)); } } } } if (require.main === module) (async () => { const info = { chain: process.env.CHAIN || 'BTC', network: process.env.NETWORK || 'testnet' }; const creds = (config.chains[info.chain][info.network] as IUtxoNetworkConfig).rpc; await Storage.start({}); logger.info('verifying blocks'); await blocks(info, creds); logger.info('verifying transactions'); await transactions(info, creds); process.exit(); })().catch(err => { logger.error('%o', err); process.exit(1); });