ldpos-chain
Version:
LDPoS chain module
1,382 lines (1,286 loc) • 136 kB
JavaScript
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) {