UNPKG

ldpos-chain

Version:
1,382 lines (1,286 loc) 136 kB
const crypto = require('crypto'); const path = require('path'); const shuffle = require('lodash.shuffle'); const WritableConsumableStream = require('writable-consumable-stream'); const genesisBlock = require('./genesis/testnet/genesis.json'); const pkg = require('./package.json'); const { validateBlockSchema } = require('./schemas/block-schema'); const { validateTransactionSchema } = require('./schemas/transaction-schema'); const { validateBlockSignatureSchema } = require('./schemas/block-signature-schema'); const { validateMultisigTransactionSchema } = require('./schemas/multisig-transaction-schema'); const { validateSigTransactionSchema } = require('./schemas/sig-transaction-schema'); const { validateWalletAddress, validateBlockId, validateBlockHeight, validateTransactionId, validateTimestamp, validateOffset, validateLimit, validateSortOrder } = require('./schemas/primitives'); const { LDPOS_PASSWORD, LDPOS_FORGING_KEY_INDEX } = process.env; // Cipher configuration for encrypting forging passphrases. // A fixed salt is used for key derivation because: // 1. This is not password hashing for authentication (no rainbow table attack risk) // 2. Each encrypted passphrase is stored in config and decrypted at runtime // 3. Security comes from the strength of LDPOS_PASSWORD, not salt randomness // 4. Salt doesn't need to be secret or unique per installation // // IMPORTANT: Users must choose a strong LDPOS_PASSWORD to protect their encrypted // forging passphrases. The security of the encryption depends entirely on password strength. const CIPHER_ALGORITHM = 'aes-192-cbc'; const CIPHER_KEY = LDPOS_PASSWORD ? crypto.scryptSync(LDPOS_PASSWORD, 'salt', 24) : undefined; const CIPHER_IV = Buffer.alloc(16, 0); const DEFAULT_MODULE_ALIAS = 'ldpos_chain'; const DEFAULT_GENESIS_PATH = path.resolve(__dirname, './genesis/mainnet/genesis.json'); const DEFAULT_NETWORK_SYMBOL = 'ldpos'; const DEFAULT_CRYPTO_CLIENT_LIB_PATH = 'ldpos-client'; const DEFAULT_FORGER_COUNT = 15; const DEFAULT_MIN_FORGER_BLOCK_SIGNATURE_RATIO = .6; const DEFAULT_FORGING_INTERVAL = 30000; const DEFAULT_FETCH_BLOCK_LIMIT = 10; const DEFAULT_FETCH_BLOCK_PAUSE = 500; const DEFAULT_BLOCK_PROCESSING_FAILURE_PAUSE = 20000; const DEFAULT_FETCH_BLOCK_END_CONFIRMATIONS = 10; const DEFAULT_FORGING_BLOCK_BROADCAST_DELAY = 2000; const DEFAULT_FORGING_SIGNATURE_BROADCAST_DELAY = 8000; const DEFAULT_AUTO_SYNC_FORGING_KEY_INDEX = true; const DEFAULT_PROPAGATION_TIMEOUT = 7000; const DEFAULT_PROPAGATION_RANDOMNESS = 5000; const DEFAULT_TIME_POLL_INTERVAL = 200; const DEFAULT_MIN_TRANSACTIONS_PER_BLOCK = 1; const DEFAULT_MAX_TRANSACTIONS_PER_BLOCK = 300; const DEFAULT_MIN_MULTISIG_MEMBERS = 1; const DEFAULT_MAX_MULTISIG_MEMBERS = 100; const DEFAULT_PENDING_TRANSACTION_SETTLEMENT_DELAY = 10000; const DEFAULT_PENDING_TRANSACTION_EXPIRY = 86400000; // 24 hours const DEFAULT_PENDING_TRANSACTION_EXPIRY_CHECK_INTERVAL = 3600000; // 1 hour const DEFAULT_MAX_SPENDABLE_DIGITS = 25; const DEFAULT_MAX_TRANSACTION_MESSAGE_LENGTH = 256; const DEFAULT_MAX_VOTES_PER_ACCOUNT = 5; const DEFAULT_MAX_TRANSACTION_BACKPRESSURE_PER_ACCOUNT = 32; const DEFAULT_MAX_PENDING_TRANSACTIONS_PER_ACCOUNT = 64; const DEFAULT_MAX_CONSECUTIVE_BLOCK_FETCH_FAILURES = 5; const DEFAULT_MAX_CONSECUTIVE_TRANSACTION_FETCH_FAILURES = 3; const DEFAULT_CATCH_UP_CONSENSUS_POLL_COUNT = 6; const DEFAULT_CATCH_UP_CONSENSUS_MIN_RATIO = .5; const DEFAULT_GENESES = { 0: 9 }; const DEFAULT_BLOCK_FORGER_SAMPLING_FACTOR = 4; const DEFAULT_API_LIMIT = 100; const DEFAULT_MAX_PUBLIC_API_LIMIT = 100; const DEFAULT_MAX_PRIVATE_API_LIMIT = 10000; const DEFAULT_MAX_PUBLIC_API_OFFSET = 10000; const DEFAULT_MAX_PRIVATE_API_OFFSET = 10000000; const DEFAULT_PUBLIC_GENESIS = true; const DEFAULT_KEY_INDEX_DIR_PATH = null; const DEFAULT_KEY_INDEX_FILE_EXTENSION = ''; const DEFAULT_KEY_INDEX_FILE_LOCK_OPTIONS = {}; const PROPAGATION_MODE_DELAYED = 'delayed'; const PROPAGATION_MODE_IMMEDIATE = 'immediate'; const PROPAGATION_MODE_NONE = 'none'; const GENESIS_INDICATOR = 'gen'; // Forgers can shift their keys when they forge a block. // Because there are 64 OTS keys in each Merkle signature tree, having more than // 64 forgers will not guarantee that every forger will have an opportunity // to shift their keys when their turn to forge arrives. const MAX_FORGER_COUNT = 64; const DEFAULT_MIN_TRANSACTION_FEES = { transfer: '10000000', vote: '20000000', unvote: '20000000', registerSigDetails: '500000000', registerMultisigDetails: '500000000', registerForgingDetails: '100000000', registerMultisigWallet: '50000000' }; const DEFAULT_MIN_MULTISIG_REGISTRATION_FEE_PER_MEMBER = '100000000'; const DEFAULT_MIN_MULTISIG_TRANSACTION_FEE_PER_MEMBER = '500000'; const NO_PEER_LIMIT = -1; const ACCOUNT_TYPE_SIG = 'sig'; const ACCOUNT_TYPE_MULTISIG = 'multisig'; module.exports = class LDPoSChainModule { constructor(options) { this.alias = options.alias || DEFAULT_MODULE_ALIAS; this.logger = options.logger || console; let { config } = options; let components = config.components || {}; this.dalConfig = components.dal || {}; if (!this.dalConfig) { throw new Error( `The ${this.alias} module config needs to have a components.dal property` ); } if (!this.dalConfig.libPath) { throw new Error( `The ${this.alias} module config needs to have a components.dal.libPath property` ); } const DAL = require(path.resolve(this.dalConfig.libPath)); this.dal = new DAL({ ...this.dalConfig, logger: this.logger }); this.pendingTransactionStreams = {}; this.pendingTransactionMap = new Map(); this.pendingSignerMultisigTransactions = {}; this.pendingBlocks = []; this.topActiveDelegates = []; this.topActiveDelegateAddressSet = new Set(); this.lastProcessedBlock = null; this.lastHandledBlock = null; this.lastReceivedBlock = this.lastProcessedBlock; this.lastReceivedSignerAddressSet = new Set(); this.ldposForgingClients = {}; this.supplantedAddressSigPublicKeySet = new Set(); this.supplantedAddressMultisigPublicKeySet = new Set(); this.verifiedBlockInfoStream = new WritableConsumableStream(); this.isCatchingUp = false; this.isActive = false; this.moduleState = { isOnTip: false }; this.publicGenesis = false; } get dependencies() { return ['app', 'network']; } get info() { return { author: 'Jonathan Gros-Dubois', version: pkg.version, name: DEFAULT_MODULE_ALIAS }; } get events() { return [ 'bootstrap', 'chainChanges', 'transaction' ]; } get actions() { return { getNetworkSymbol: { handler: async action => { return this.networkSymbol; }, isPublic: true }, getAccount: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let { walletAddress } = action.params; return this.dal.getAccount(walletAddress); }, isPublic: true }, getAccountsByBalance: { handler: async action => { let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order); return this.dal.getAccountsByBalance(offset, limit, order); }, isPublic: true }, getMultisigWalletMembers: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let { walletAddress } = action.params; return this.dal.getMultisigWalletMembers(walletAddress); }, isPublic: true }, getMinMultisigRequiredSignatures: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let { walletAddress } = action.params; let account = await this.getSanitizedAccount(walletAddress); if (account.type !== 'multisig') { let error = new Error( `Account ${walletAddress} was not a multisig wallet` ); error.name = 'AccountWasNotMultisigError'; error.type = 'InvalidActionError'; throw error; } return account.requiredSignatureCount; }, isPublic: true }, getSignedPendingTransaction: { handler: async action => { validateTransactionId('transactionId', action.params); let { transactionId } = action.params; let transaction = this.pendingTransactionMap.get(transactionId); if (!transaction) { let error = new Error( `No pending transaction existed with ID ${transactionId}` ); error.name = 'PendingTransactionDidNotExistError'; error.type = 'InvalidActionError'; throw error; } return transaction; }, isPublic: true }, getOutboundPendingTransactions: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); let { walletAddress, offset, limit } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); let senderTxnStream = this.pendingTransactionStreams[walletAddress]; if (!senderTxnStream) { return []; } let transactionInfoList = [...senderTxnStream.transactionInfoMap.values()]; return transactionInfoList .slice(offset, offset + limit) .map(txnInfo => this.simplifyTransaction(txnInfo.transaction, false)); }, isPublic: true }, getPendingTransactionCount: { handler: async action => { return this.pendingTransactionMap.size; }, isPublic: true }, postTransaction: { handler: async action => { let { transaction } = action.params; return this.postTransaction(transaction); }, isPublic: true }, getTransaction: { handler: async action => { validateTransactionId('transactionId', action.params); let { transactionId } = action.params; return this.dal.getTransaction(transactionId); }, isPublic: true }, getTransactionsByTimestamp: { handler: async action => { let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order); return this.dal.getTransactionsByTimestamp(offset, limit, order); }, isPublic: true }, getAccountTransactions: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); if (action.params.fromTimestamp != null) { validateTimestamp('fromTimestamp', action.params); } let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { walletAddress, fromTimestamp, offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order, 'asc'); return this.dal.getAccountTransactions(walletAddress, fromTimestamp, offset, limit, order); }, isPublic: true }, getInboundTransactions: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); if (action.params.fromTimestamp != null) { validateTimestamp('fromTimestamp', action.params); } let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { walletAddress, fromTimestamp, offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order, 'asc'); return this.dal.getInboundTransactions(walletAddress, fromTimestamp, offset, limit, order); }, isPublic: true }, getOutboundTransactions: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); if (action.params.fromTimestamp != null) { validateTimestamp('fromTimestamp', action.params); } let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { walletAddress, fromTimestamp, offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order, 'asc'); return this.dal.getOutboundTransactions(walletAddress, fromTimestamp, offset, limit, order); }, isPublic: true }, getTransactionsFromBlock: { handler: async action => { validateBlockId('blockId', action.params); let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); let { blockId, offset, limit } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); return this.dal.getTransactionsFromBlock(blockId, offset, limit); }, isPublic: true }, getInboundTransactionsFromBlock: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); validateBlockId('blockId', action.params); let { walletAddress, blockId } = action.params; return this.dal.getInboundTransactionsFromBlock(walletAddress, blockId); }, isPublic: true }, getOutboundTransactionsFromBlock: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); validateBlockId('blockId', action.params); let { walletAddress, blockId } = action.params; return this.dal.getOutboundTransactionsFromBlock(walletAddress, blockId); }, isPublic: true }, getLastBlockAtTimestamp: { handler: async action => { validateTimestamp('timestamp', action.params); let { timestamp } = action.params; return this.dal.getLastBlockAtTimestamp(timestamp); }, isPublic: true }, getMaxBlockHeight: { handler: async action => { let maxHeight = await this.dal.getMaxBlockHeight(); if ( action.params.includeSkipped && this.lastHandledBlock && this.lastHandledBlock.height === maxHeight + 1 ) { return this.lastHandledBlock.height; } return maxHeight; }, isPublic: true }, getBlocksFromHeight: { handler: async action => { let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateBlockHeight('height', action.params); validateLimit('limit', action.params, maxLimit); let { height, limit } = action.params; limit = this.sanitizeLimit(limit); return this.dal.getBlocksFromHeight(height, limit); }, isPublic: true }, getSignedBlock: { handler: async action => { validateBlockId('blockId', action.params); let { blockId } = action.params; return this.dal.getSignedBlock(blockId); }, isPublic: true }, getSignedBlocksFromHeight: { handler: async action => { let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateBlockHeight('height', action.params); validateLimit('limit', action.params, maxLimit); validateLimit('signatureLimit', action.params, Number.MAX_SAFE_INTEGER); let { height, limit, signatureLimit } = action.params; limit = this.sanitizeLimit(limit); let signedBlockList = await this.dal.getSignedBlocksFromHeight(height, limit); if (signatureLimit == null) { return signedBlockList; } return signedBlockList.map((signedBlock) => { return { ...signedBlock, signatures: signedBlock.signatures.slice(0, signatureLimit) }; }); }, isPublic: true }, getBlocksBetweenHeights: { handler: async action => { let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateBlockHeight('fromHeight', action.params); validateBlockHeight('toHeight', action.params); validateLimit('limit', action.params, maxLimit); let { fromHeight, toHeight, limit } = action.params; limit = this.sanitizeLimit(limit); let blocks = await this.dal.getBlocksBetweenHeights(fromHeight, toHeight, limit); let appendHeight = blocks.length ? blocks[blocks.length - 1].height : fromHeight; if ( action.params.includeSkipped && this.lastHandledBlock && this.lastHandledBlock.height === appendHeight + 1 && this.lastHandledBlock.height <= toHeight ) { return [ ...blocks, { ...this.simplifyBlock(this.lastHandledBlock), isSkipped: true } ]; } return blocks; }, isPublic: true }, getBlockAtHeight: { handler: async action => { validateBlockHeight('height', action.params); let { height } = action.params; return this.dal.getBlockAtHeight(height); }, isPublic: true }, getBlock: { handler: async action => { validateBlockId('blockId', action.params); let { blockId } = action.params; return this.dal.getBlock(blockId); }, isPublic: true }, hasBlock: { handler: async action => { validateBlockId('blockId', action.params); let { blockId } = action.params; return this.dal.hasBlock(blockId); }, isPublic: true }, getBlocksByTimestamp: { handler: async action => { let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order); return this.dal.getBlocksByTimestamp(offset, limit, order); }, isPublic: true }, getDelegate: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let { walletAddress } = action.params; return this.dal.getDelegate(walletAddress); }, isPublic: true }, getDelegateVoters: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { walletAddress, offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order); return this.dal.getDelegateVoters(walletAddress, offset, limit, order); }, isPublic: true }, getDelegatesByVoteWeight: { handler: async action => { let maxOffset = action.isPublic ? this.maxPublicAPIOffset : this.maxPrivateAPIOffset; validateOffset('offset', action.params, maxOffset); let maxLimit = action.isPublic ? this.maxPublicAPILimit : this.maxPrivateAPILimit; validateLimit('limit', action.params, maxLimit); validateSortOrder('order', action.params); let { offset, limit, order } = action.params; offset = this.sanitizeOffset(offset); limit = this.sanitizeLimit(limit); order = this.sanitizeOrder(order); return this.dal.getDelegatesByVoteWeight(offset, limit, order); }, isPublic: true }, getForgingDelegates: { handler: async action => { return this.getForgingDelegates(); }, isPublic: true }, getAccountVotes: { handler: async action => { validateWalletAddress('walletAddress', action.params, this.networkSymbol); let { walletAddress } = action.params; return this.dal.getAccountVotes(walletAddress); }, isPublic: true }, getMinFees: { handler: async action => { let minTransactionFees = {}; let minTransactionFeeEntries = Object.entries(this.minTransactionFees || {}); for (let [ txnType, fee ] of minTransactionFeeEntries) { minTransactionFees[txnType] = fee.toString(); } return { minTransactionFees, minMultisigRegistrationFeePerMember: this.minMultisigRegistrationFeePerMember.toString(), minMultisigTransactionFeePerMember: this.minMultisigTransactionFeePerMember.toString() }; }, isPublic: true }, getChainInfo: { handler: async action => this.chainInfo, isPublic: true }, getAPIInfo: { handler: async action => this.apiInfo, isPublic: true }, getGenesis: { handler: async action => this.genesis, isPublic: this.publicGenesis }, getModuleOptions: { handler: async action => this.options } }; } simplifyBlock(signedBlock) { let { transactions, forgerSignature, signatures, ...simpleBlock } = signedBlock; return simpleBlock; } sanitizeOffset(offset) { if (offset == null) { return 0; } return offset; } sanitizeLimit(limit) { if (limit == null) { return this.apiLimit; } return limit; } sanitizeOrder(order, defaultOrder) { if (order == null) { return defaultOrder || 'desc'; } return order; } async catchUpWithNetwork(options) { let { forgingInterval, fetchBlockEndConfirmations, fetchBlockLimit, fetchBlockPause, blockProcessingFailurePause, blockSignerMajorityCount, maxConsecutiveBlockFetchFailures } = options; let addedBlockCount = 0; let now = Date.now(); if ( Math.floor(this.lastHandledBlock.timestamp / forgingInterval) >= Math.floor(now / forgingInterval) ) { return { lastHeight: this.lastProcessedBlock.height, addedBlockCount }; } this.logger.info('Attempting to catch up with the network'); this.isCatchingUp = true; if (this.moduleState.isOnTip) { try { await this.updateModuleState({ ...this.moduleState, isOnTip: false }); this.moduleState.isOnTip = false; } catch (error) { this.logger.warn(error); } } let consecutiveFailureCount = 0; while (true) { if (!this.isActive) { break; } let nextBlockHeight = this.lastProcessedBlock.height + 1; this.logger.info( `Fetching new blocks from network starting at height ${nextBlockHeight}` ); let newBlocks; let response; let endBlockHeight = nextBlockHeight + fetchBlockLimit; let genesisHeightAtStart = this.getTargetGenesisHeight(this.genesesHeights, nextBlockHeight); let genesisHeightAtEnd = this.getTargetGenesisHeight(this.genesesHeights, endBlockHeight); let requiredSignatureCountAtStart = this.geneses[genesisHeightAtStart]; let requiredSignatureCountAtEnd = this.geneses[genesisHeightAtEnd]; let signatureLimit = Math.max(requiredSignatureCountAtStart, requiredSignatureCountAtEnd); let actionRouteString; if (genesisHeightAtStart === genesisHeightAtEnd) { actionRouteString = `${this.alias}?${GENESIS_INDICATOR}${ genesisHeightAtStart }-${ requiredSignatureCountAtStart }=1`; } else { actionRouteString = `${ this.alias }?${ GENESIS_INDICATOR }${ genesisHeightAtStart }-${ requiredSignatureCountAtStart }=1&${ GENESIS_INDICATOR }${ genesisHeightAtEnd }-${ requiredSignatureCountAtEnd }=1`; } try { response = await this.channel.invoke('network:request', { procedure: `${actionRouteString}:getSignedBlocksFromHeight`, data: { height: nextBlockHeight, limit: fetchBlockLimit, signatureLimit } }); newBlocks = response.data; if (!Array.isArray(newBlocks)) { throw new Error('Response data from getSignedBlocksFromHeight action must be an array'); } if (newBlocks.length > fetchBlockLimit) { throw new Error( `Peer getBlocksFromHeight action must not return more than ${ fetchBlockLimit } blocks` ); } consecutiveFailureCount = 0; } catch (error) { this.logger.warn( new Error( `Failed to invoke getSignedBlocksFromHeight action on network because of error: ${ error.message }` ) ); if (++consecutiveFailureCount > maxConsecutiveBlockFetchFailures) { break; } await this.wait(fetchBlockPause); continue; } if (!newBlocks.length) { // If there are no new blocks, assume that we've finished synching. break; } let lastBlock = this.lastProcessedBlock; let allBlockIdsLineUp = true; for (let block of newBlocks) { if (block.previousBlockId !== lastBlock.id) { allBlockIdsLineUp = false; break; } lastBlock = block; } if (!allBlockIdsLineUp) { this.logger.warn( new Error( `Batch of blocks ending with the block ${ lastBlock.id } was discarded because some of the block IDs did not line up` ) ); break; } let results = await Promise.all( [...Array(this.catchUpConsensusPollCount).keys()].map(async () => { try { let response = await this.channel.invoke('network:request', { procedure: `${this.alias}:hasBlock`, data: { blockId: lastBlock.id } }); return response.data || false; } catch (error) { return false; } }) ); let matchingCount = results.reduce((total, peerHasBlock) => total + (peerHasBlock ? 1 : 0), 0); let consensusRatio = matchingCount / this.catchUpConsensusPollCount; if (consensusRatio < this.catchUpConsensusMinRatio) { this.logger.warn( new Error( `Batch of blocks ending with the block ${ lastBlock.id } was discarded because the sampled network consensus of ${ Math.round(consensusRatio * 10000) / 100 }% did not meet the minimum required ratio` ) ); break; } let pause = fetchBlockPause; for (let block of newBlocks) { let blockHeight = this.lastProcessedBlock.height + 1; let requiredBlockSignatureCount = Math.min( blockSignerMajorityCount, this.getRequiredBlockSignatureCountAtHeight(blockHeight) ); try { validateBlockSchema( block, 0, this.maxTransactionsPerBlock, requiredBlockSignatureCount, this.forgerCount, this.networkSymbol ); let { senderAccountDetails, } = await this.verifyFullySignedBlock(block, this.lastProcessedBlock); let blockSignificant = await this.isBlockSignificant(block); if (!blockSignificant) { throw new Error(`Block ${block.id} was not significant; it should not be part of the chain`); } try { await this.processBlock(block, senderAccountDetails, true); } catch (error) { pause = blockProcessingFailurePause; throw error; } this.lastHandledBlock = this.lastProcessedBlock; addedBlockCount++; } catch (error) { this.logger.warn( `Failed to process block ${ block.id } while catching up with the network because of error: ${ error.message }` ); break; } } await this.wait(pause); } this.isCatchingUp = false; this.logger.info('Stopped catching up with the network'); return { lastHeight: this.lastProcessedBlock.height, addedBlockCount }; } async receiveLastBlockInfo(timeout) { this.verifiedBlockInfoStream.kill(); try { return await this.verifiedBlockInfoStream.once(timeout); } catch (error) { throw new Error( `Timed out while waiting to receive the latest block from the network` ); } } getCurrentBlockTimeSlot(forgingInterval) { return Math.floor(Date.now() / forgingInterval) * forgingInterval; } getForgingDelegateAddressAtTimestamp(timestamp) { let activeDelegates = this.topActiveDelegates; let slotIndex = Math.floor(timestamp / this.forgingInterval); let targetIndex = slotIndex % activeDelegates.length; if (!activeDelegates.length) { throw new Error('Could not find any active delegates'); } return activeDelegates[targetIndex].address; } sha256(message, encoding) { return crypto.createHash('sha256').update(message, 'utf8').digest(encoding || 'base64'); } async forgeBlock(forgerAddress, height, timestamp, transactions) { let blockData = { height, timestamp, previousBlockId: this.lastProcessedBlock ? this.lastProcessedBlock.id : null, numberOfTransactions: transactions.length, transactions }; return this.ldposForgingClients[forgerAddress].prepareBlock(blockData); } async getSanitizedAccount(walletAddress) { let account = await this.dal.getAccount(walletAddress); return { ...account, balance: BigInt(account.balance) }; } async getSanitizedTransaction(transactionId) { let transaction = await this.dal.getTransaction(transactionId); return { ...transaction, amount: BigInt(transaction.amount), fee: BigInt(transaction.fee) }; } simplifyTransaction(transaction, withSignatureHashes) { let { senderSignature, signatures, ...txnWithoutSignatures} = transaction; if (!withSignatureHashes) { return txnWithoutSignatures; } if (signatures) { // If multisig transaction return { ...txnWithoutSignatures, signatures: signatures.map(signaturePacket => { let { signature, ...signaturePacketWithoutSignature } = signaturePacket; return { ...signaturePacketWithoutSignature, signatureHash: this.sha256(signature) }; }) }; } // If regular sig transaction return { ...txnWithoutSignatures, senderSignatureHash: this.sha256(senderSignature) }; } async getForgingDelegates() { return this.topActiveDelegates; } async fetchTopActiveDelegates() { let delegateList = await this.dal.getDelegatesByVoteWeight(0, this.forgerCount, 'desc'); this.topActiveDelegates = delegateList.filter(delegate => delegate.voteWeight !== '0'); this.topActiveDelegateAddressSet = new Set(this.topActiveDelegates.map(delegate => delegate.address)); } async processBlock(block, senderAccountDetails, synched) { this.logger.info( `Started processing ${synched ? 'synched' : 'received'} block ${block.id}` ); let { transactions, height, signatures: blockSignatureList } = block; let senderAddressSet = new Set(); let recipientAddressSet = new Set(); let multisigMemberAddressSet = new Set(); for (let txn of transactions) { senderAddressSet.add(txn.senderAddress); if (txn.recipientAddress) { recipientAddressSet.add(txn.recipientAddress); } // For multisig transaction, add all signer accounts. if (txn.signatures) { for (let signaturePacket of txn.signatures) { multisigMemberAddressSet.add(signaturePacket.signerAddress); } } } let affectedAddressSet = new Set([ ...senderAddressSet, ...recipientAddressSet, ...multisigMemberAddressSet, block.forgerAddress ]); let affectedAddressList = [...affectedAddressSet]; let affectedAccountList = await Promise.all( affectedAddressList.map(async (address) => { if (senderAccountDetails[address]) { return senderAccountDetails[address].senderAccount; } let account; try { account = await this.getSanitizedAccount(address); } catch (error) { if (error.name === 'AccountDidNotExistError') { return { address, type: ACCOUNT_TYPE_SIG, balance: 0n }; } else { throw new Error( `Failed to fetch account during block processing because of error: ${ error.message }` ); } } return account; }) ); let affectedAccountDetails = {}; for (let account of affectedAccountList) { affectedAccountDetails[account.address] = { account, changes: { balance: account.balance }, balanceDelta: 0n }; } let forgerAccountChanges = affectedAccountDetails[block.forgerAddress].changes; forgerAccountChanges.forgingPublicKey = block.forgingPublicKey; forgerAccountChanges.nextForgingPublicKey = block.nextForgingPublicKey; forgerAccountChanges.nextForgingKeyIndex = block.nextForgingKeyIndex; let voteChangeList = []; let delegateRegistrationList = []; let multisigRegistrationList = []; let totalBlockFees = 0n; for (let txn of transactions) { let { type, senderAddress, fee, timestamp, signatures, sigPublicKey, nextSigPublicKey, nextSigKeyIndex } = txn; let senderAccountChanges = affectedAccountDetails[senderAddress].changes; let txnFee = BigInt(fee); totalBlockFees += txnFee; if (signatures) { for (let signaturePacket of signatures) { let memberAccountChanges = affectedAccountDetails[signaturePacket.signerAddress].changes; memberAccountChanges.multisigPublicKey = signaturePacket.multisigPublicKey; memberAccountChanges.nextMultisigPublicKey = signaturePacket.nextMultisigPublicKey; memberAccountChanges.nextMultisigKeyIndex = signaturePacket.nextMultisigKeyIndex; } } else { // If regular transaction (not multisig), update the account sig public keys. senderAccountChanges.sigPublicKey = sigPublicKey; senderAccountChanges.nextSigPublicKey = nextSigPublicKey; senderAccountChanges.nextSigKeyIndex = nextSigKeyIndex; } if (type === 'transfer') { let { recipientAddress, amount } = txn; let txnAmount = BigInt(amount); let recipientAccountChanges = affectedAccountDetails[recipientAddress].changes; senderAccountChanges.balance -= txnAmount + txnFee; senderAccountChanges.lastTransactionTimestamp = timestamp; recipientAccountChanges.balance += txnAmount; } else { senderAccountChanges.balance -= txnFee; senderAccountChanges.lastTransactionTimestamp = timestamp; if (type === 'vote' || type === 'unvote') { voteChangeList.push({ id: txn.id, type, voterAddress: senderAddress, delegateAddress: txn.delegateAddress, transaction: txn }); } else if (type === 'registerSigDetails') { let { newSigPublicKey, newNextSigPublicKey, newNextSigKeyIndex } = txn; senderAccountChanges.sigPublicKey = newSigPublicKey; senderAccountChanges.nextSigPublicKey = newNextSigPublicKey; senderAccountChanges.nextSigKeyIndex = newNextSigKeyIndex; } else if (type === 'registerMultisigDetails') { let { newMultisigPublicKey, newNextMultisigPublicKey, newNextMultisigKeyIndex } = txn; senderAccountChanges.multisigPublicKey = newMultisigPublicKey; senderAccountChanges.nextMultisigPublicKey = newNextMultisigPublicKey; senderAccountChanges.nextMultisigKeyIndex = newNextMultisigKeyIndex; } else if (type === 'registerForgingDetails') { let { newForgingPublicKey, newNextForgingPublicKey, newNextForgingKeyIndex } = txn; senderAccountChanges.forgingPublicKey = newForgingPublicKey; senderAccountChanges.nextForgingPublicKey = newNextForgingPublicKey; senderAccountChanges.nextForgingKeyIndex = newNextForgingKeyIndex; delegateRegistrationList.push({ delegateAddress: senderAddress }); } else if (type === 'registerMultisigWallet') { multisigRegistrationList.push({ multisigAddress: senderAddress, memberAddresses: txn.memberAddresses, requiredSignatureCount: txn.requiredSignatureCount, transaction: txn }); } } this.logger.info(`Processed transaction ${txn.id}`); } forgerAccountChanges.balance += totalBlockFees; await Promise.all( affectedAddressList.map(async (affectedAddress) => { let accountInfo = affectedAccountDetails[affectedAddress]; let { account } = accountInfo; let accountChanges = accountInfo.changes; accountInfo.balanceDelta = accountChanges.balance - account.balance; let accountUpdatePacket = { ...accountChanges, balance: accountChanges.balance.toString(), updateHeight: height }; if (account.updateHeight == null) { await this.dal.upsertAccount({ ...account, ...accountUpdatePacket }); } else if (account.updateHeight < height) { await this.dal.upsertAccount({ address: account.address, type: account.type, ...accountUpdatePacket }); } }) ); await Promise.all( delegateRegistrationList.map(async (delegateRegistration) => { let { delegateAddress } = delegateRegistration; let hasDelegate = await this.dal.hasDelegate(delegateAddress); if (!hasDelegate) { await this.dal.upsertDelegate({ address: delegateAddress, voteWeight: '0', forgingRewards: '0' }); } }) ); let accountVotes = {}; let delegateVoters = {}; await Promise.all( affectedAddressList.map(async (voterAddress) => { let delegateAddressList = await this.dal.getAccountVotes(voterAddress); accountVotes[voterAddress] = new Set(delegateAddressList); for (let delegateAddress of delegateAddressList) { if (!delegateVoters[delegateAddress]) { delegateVoters[delegateAddress] = new Set(); } delegateVoters[delegateAddress].add(voterAddress); } }) ); let affectedDelegateDetails = {}; let voteChangeDelegateAddressList = [...new Set(voteChangeList.map(voteChange => voteChange.delegateAddress))]; let affectedDelegateAddressSet = new Set([ block.forgerAddress, ...Object.keys(delegateVoters), ...voteChangeDelegateAddressList ]); let affectedDelegateAddressList = [...affectedDelegateAddressSet]; await Promise.all( affectedDelegateAddressList.map(async (delegateAddress) => { let delegate; try { delegate = await this.dal.getDelegate(delegateAddress); } catch (error) { throw new Error( `Failed to fetch delegate during block processing because of error: ${ error.message }` ); } let voteWeightDelta = 0n; let currentDelegateVoters = delegateVoters[delegateAddress]; if (currentDelegateVoters) { for (let voterAddress of currentDelegateVoters) { let accountInfo = affectedAccountDetails[voterAddress]; voteWeightDelta += accountInfo.balanceDelta; } } affectedDelegateDetails[delegateAddress] = { delegate, voteWeightDelta }; }) ); affectedDelegateDetails[block.forgerAddress].forgingRewardsDelta = totalBlockFees; let voterVoteChanges = {}; for (let voteChange of voteChangeList) { if (!voterVoteChanges[voteChange.voterAddress]) { voterVoteChanges[voteChange.voterAddress] = []; } voterVoteChanges[voteChange.voterAddress].push(voteChange); } await Promise.all( Object.keys(voterVoteChanges).map(async (voterAddress) => { let currentVoteChangeList = voterVoteChanges[voterAddress]; for (let voteChange of currentVoteChangeList) { let voterInfo = affectedAccountDetails[voterAddress]; let { changes: voterChanges } = voterInfo; let delegateInfo = affectedDelegateDetails[voteChange.delegateAddress]; try { if (voteChange.type === 'vote') { await this.dal.vote({ id: voteChange.id, voterAddress, delegateAddress: voteChange.delegateAddress }); delegateInfo.voteWeightDelta += voterChanges.balance; } else if (voteChange.type === 'unvote') { await this.dal.unvote({ id: voteChange.id, voterAddress, delegateAddress: voteChange.delegateAddress }); delegateInfo.voteWeightDelta -= voterChanges.balance; } } catch (error) { if (error.type === 'InvalidActionError') { voteChange.transaction.error = error; this.logger.debug(error.message); } else { throw error; } } } }) ); await Promise.all( affectedDelegateAddressList.map(async (delegateAddress) => { let delegateInfo = affectedDelegateDetails[delegateAddress]; let { delegate } = delegateInfo; let delegateUpdatePacket = {}; if (delegateInfo.voteWeightDelta) { delegateUpdatePacket.voteWeight = (BigInt(delegate.voteWeight) + delegateInfo.voteWeightDelta).toString(); } else { delegateUpdatePacket.voteWeight = delegate.voteWeight; } if (delegateInfo.forgingRewardsDelta) { delegateUpdatePacket.forgingRewards = (BigInt(delegate.forgingRewards) + delegateInfo.forgingRewardsDelta).toString(); } else { delegateUpdatePacket.forgingRewards = delegate.forgingRewards; } delegateUpdatePacket.updateHeight = height; if (delegate.updateHeight == null || delegate.updateHeight < height) { await this.dal.upsertDelegate({ address: delegate.address, ...delegateUpdatePacket }); } }) ); for (let multisigRegistration of multisigRegistrationList) { let { multisigAddress, memberAddresses, requiredSignatureCount } = multisigRegistration; try { await this.dal.registerMultisigWallet(multisigAddress, memberAddresses, requiredSignatureCount); let senderAccountChanges = affectedAccountDetails[multisigAddress].changes; senderAccountChanges.type = ACCOUNT_TYPE_MULTISIG; senderAccountChanges.requiredSignatureCount = requiredSignatureCount; } catch (error) { if (error.type === 'InvalidActionError') { multisigRegistration.transaction.error = error; this.logger.debug(error.message); } else { throw error; } } } let numberOfSignaturesToStore = this.getRequiredBlockSignatureCountAtHeight(height); let blockSignaturesToStore = shuffle(blockSignatureList).slice(0, numberOfSignaturesToStore); await this.dal.upsertBlock({ ...block, signatures: blockSignaturesToStore }, synched); this.logger.debug(`Upserted block ${block.id} into data store at height ${height}`); // Remove transactions which have been processed as part of the current block from pending transaction maps. for (let txn of transactions) { this.untrackPendingTransaction(txn); } // Update in-memory accounts in transaction streams to ensure that they have the latest sig and multisig public keys. await Promise.all( Object.keys(this.pendingTransactionStreams).map(async (senderAddress) => { let accountStream = this.pendingTransactionStreams[senderAddress]; let pendingSenderAccount; let pendingMultisigMemberAccounts; try { let senderInfo = await accountStream.senderInfoPromise; pendingSenderAccount = senderInfo.senderAccount; pendingMultisigMemberAccounts = senderInfo.multisigMemberAccounts || {}; } catch (error) { this.logger.debug( `Failed to update public keys of account ${ senderAddress } in pending queue because of error: ${ error.message }` ); return; } let pendingAccountList = [pendingSenderAccount, ...Object.values(pendingMultisigMemberAccounts)]; for (let pendingAccount of pendingAccountList) { let accountInfo = affectedAccountDetails[pendingAccount.address]; if (!accountInfo) { continue; } let accountChanges = accountInfo.changes; if (accountChanges.sigPublicKey) { pendingAccount.sigPublicKey = accountChanges.sigPublicKey; } if (accountChanges.nextSigPublicKey) { pendingAccount.nextSigPublicKey = accountChanges.nextSigPublicKey; } if (accountChanges.nextSigKeyIndex) {