hivesmartcontracts/libs/Block.js

292 lines
12 KiB
JavaScript

const SHA256 = require('crypto-js/sha256');
const enchex = require('crypto-js/enc-hex');
const log = require('loglevel');
const { CONSTANTS } = require('../libs/Constants');
const { SmartContracts } = require('./SmartContracts');
const { Transaction } = require('../libs/Transaction');
const { setupContractPayload } = require('../libs/util/contractUtil');
const revertCommentsContractPayload = setupContractPayload('comments', './contracts/revert/comments_minify_20211027.js');
class Block {
constructor(timestamp, refHiveBlockNumber, refHiveBlockId, prevRefHiveBlockId, transactions, previousBlockNumber, previousHash = '', previousDatabaseHash = '') {
this.blockNumber = previousBlockNumber + 1;
this.refHiveBlockNumber = refHiveBlockNumber;
this.refHiveBlockId = refHiveBlockId;
this.prevRefHiveBlockId = prevRefHiveBlockId;
this.previousHash = previousHash;
this.previousDatabaseHash = previousDatabaseHash;
this.timestamp = timestamp;
this.transactions = transactions;
this.virtualTransactions = [];
this.hash = this.calculateHash();
this.databaseHash = '';
this.merkleRoot = '';
this.round = null;
this.roundHash = '';
this.witness = '';
this.signingKey = '';
this.roundSignature = '';
}
// calculate the hash of the block
calculateHash() {
return SHA256(
this.previousHash
+ this.previousDatabaseHash
+ this.blockNumber.toString()
+ this.refHiveBlockNumber.toString()
+ this.refHiveBlockId
+ this.prevRefHiveBlockId
+ this.timestamp
+ this.merkleRoot
+ JSON.stringify(this.transactions) // eslint-disable-line
)
.toString(enchex);
}
// calculate the Merkle root of the block ((#TA + #TB) + (#TC + #TD) )
calculateMerkleRoot(transactions) {
if (transactions.length <= 0) return '';
const tmpTransactions = transactions.slice(0, transactions.length);
const newTransactions = [];
const nbTransactions = tmpTransactions.length;
for (let index = 0; index < nbTransactions; index += 2) {
const left = tmpTransactions[index].hash;
const right = index + 1 < nbTransactions ? tmpTransactions[index + 1].hash : left;
const leftDbHash = tmpTransactions[index].databaseHash;
const rightDbHash = index + 1 < nbTransactions
? tmpTransactions[index + 1].databaseHash
: leftDbHash;
newTransactions.push({
hash: SHA256(left + right).toString(enchex),
databaseHash: SHA256(leftDbHash + rightDbHash).toString(enchex),
});
}
if (newTransactions.length === 1) {
return {
hash: newTransactions[0].hash,
databaseHash: newTransactions[0].databaseHash,
};
}
return this.calculateMerkleRoot(newTransactions);
}
async blockAdjustments(database) {
if (this.refHiveBlockNumber === 43447729 || this.refHiveBlockNumber === 44870101) {
// main node skipped this due to space issue
this.transactions = [];
}
// To keep in sync with primary node history after hack
if (this.refHiveBlockNumber === 50352631) {
const tokenBalances = database.database.collection('tokens_balances');
await tokenBalances.updateOne({ _id: 8416 }, { $set: { account: 'nightowl1', balance: '1.05000000' } });
await tokenBalances.updateOne({ _id: 21725 }, { $set: { account: 'nightowl1', balance: '1010.00000000' } });
} else if (this.refHiveBlockNumber === 50354478) {
const tokenBalances = database.database.collection('tokens_balances');
await tokenBalances.updateOne({ _id: 8416 }, { $set: { account: 'nightowl1', balance: '0.50000000' } });
} else if (this.refHiveBlockNumber === 50354625) {
const tokenBalances = database.database.collection('tokens_balances');
await tokenBalances.updateOne({ _id: 21725 }, { $set: { account: 'nightowl1', balance: '500000.00000000' } });
}
// Comments contract causing issues and needs to be reverted. put at beginning
if (this.refHiveBlockNumber === 58637536) {
this.transactions.unshift(new Transaction(this.blockNumber, 'FIXTX_COMMENTS_REVERT', CONSTANTS.HIVE_ENGINE_ACCOUNT, 'contract', 'update', JSON.stringify(revertCommentsContractPayload)));
}
}
/**
* Try cleaning up blocks after every 100 blocks if node is a light node.
* @param database
* @returns {Promise<void>}
*/
async cleanupLightNode(database) {
if (this.blockNumber % 100 === 0) {
await database.cleanupLightNode();
}
}
// produce the block (deploy a smart contract or execute a smart contract)
async produceBlock(database, jsVMTimeout, mainBlock) {
await this.cleanupLightNode(database);
await this.blockAdjustments(database);
const nbTransactions = this.transactions.length;
let currentDatabaseHash = this.previousDatabaseHash;
let relIndex = 0;
const allowCommentContract = this.refHiveBlockNumber > 54560500;
for (let i = 0; i < nbTransactions; i += 1) {
const transaction = this.transactions[i];
log.info('Processing tx ', transaction);
await this.processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash); // eslint-disable-line
currentDatabaseHash = transaction.databaseHash;
if ((transaction.contract !== 'comments' || allowCommentContract) || transaction.logs === '{}') {
if (mainBlock && currentDatabaseHash !== mainBlock.transactions[relIndex].databaseHash) {
log.warn(mainBlock.transactions[relIndex]); // eslint-disable-line no-console
log.warn(transaction); // eslint-disable-line no-console
throw new Error('tx hash mismatch with api');
}
relIndex += 1;
}
}
// remove comment, comment_options and votes if not relevant
this.transactions = this.transactions.filter(value => (value.contract !== 'comments' || allowCommentContract) || value.logs === '{}');
// handle virtual transactions
const virtualTransactions = [];
// reset in case block processing loops
this.virtualTransactions = [];
// use contracts_config contractTicks to trigger ticking contract actions.
const contractsConfig = await database.getContractsConfig();
for (let i = 0; i < contractsConfig.contractTicks.length; i += 1) {
const contractTick = contractsConfig.contractTicks[i];
if (this.refHiveBlockNumber >= contractTick.startRefBlock) {
virtualTransactions.push(new Transaction(0, '', 'null', contractTick.contract, contractTick.action, ''));
}
}
if (this.refHiveBlockNumber % 1200 === 0) {
virtualTransactions.push(new Transaction(0, '', 'null', 'inflation', 'issueNewTokens', '{ "isSignedWithActiveKey": true }'));
}
relIndex = 0;
const nbVirtualTransactions = virtualTransactions.length;
for (let i = 0; i < nbVirtualTransactions; i += 1) {
const transaction = virtualTransactions[i];
transaction.refHiveBlockNumber = this.refHiveBlockNumber;
transaction.transactionId = `${this.refHiveBlockNumber}-${i}`;
await this.processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash); // eslint-disable-line
currentDatabaseHash = transaction.databaseHash;
// if there are outputs in the virtual transaction we save the transaction into the block
// the "unknown error" errors are removed as they are related to a non existing action
if (transaction.logs !== '{}'
&& transaction.logs !== '{"errors":["unknown error"]}') {
let tickingAction = false;
for (let j = 0; j < contractsConfig.contractTicks.length; j += 1) {
const contractTick = contractsConfig.contractTicks[j];
if (transaction.contract === contractTick.contract
&& transaction.action === contractTick.action
&& transaction.logs === '{"errors":["contract doesn\'t exist"]}') {
tickingAction = true;
}
}
if (transaction.contract === 'inflation'
&& transaction.action === 'issueNewTokens'
&& transaction.logs === '{"errors":["contract doesn\'t exist"]}') {
// don't save logs
} else if (tickingAction) {
// don't save logs
} else {
this.virtualTransactions.push(transaction);
if (mainBlock && currentDatabaseHash
!== mainBlock.virtualTransactions[relIndex].databaseHash) {
log.warn(mainBlock.virtualTransactions[relIndex]); // eslint-disable-line no-console
log.warn(transaction); // eslint-disable-line no-console
throw new Error('tx hash mismatch with api');
}
relIndex += 1;
}
}
}
// add odd blocks, consensus appended double virtual transactions
if (this.refHiveBlockNumber === 59376574) {
this.virtualTransactions = this.virtualTransactions.concat(this.virtualTransactions);
}
if (this.transactions.length > 0 || this.virtualTransactions.length > 0) {
// calculate the merkle root of the transactions' hashes and the transactions' database hashes
const finalTransactions = this.transactions.concat(this.virtualTransactions);
const merkleRoots = this.calculateMerkleRoot(finalTransactions);
this.merkleRoot = merkleRoots.hash;
this.databaseHash = merkleRoots.databaseHash;
this.hash = this.calculateHash();
} else if (currentDatabaseHash !== this.previousDatabaseHash) {
await database.noteHashChange(this.refHiveBlockNumber);
}
}
async processTransaction(database, jsVMTimeout, transaction, currentDatabaseHash) {
const {
sender,
contract,
action,
payload,
} = transaction;
let results = null;
let newCurrentDatabaseHash = currentDatabaseHash;
// init the database hash for that transactions
database.initDatabaseHash(newCurrentDatabaseHash);
if (sender && contract && action) {
if (contract === 'contract' && (action === 'deploy' || action === 'update') && payload) {
const authorizedAccountContractDeployment = ['null', CONSTANTS.HIVE_ENGINE_ACCOUNT, CONSTANTS.HIVE_PEGGED_ACCOUNT];
if (authorizedAccountContractDeployment.includes(sender)) {
results = await SmartContracts.deploySmartContract( // eslint-disable-line
database, transaction, this.blockNumber, this.timestamp,
this.refHiveBlockId, this.prevRefHiveBlockId, jsVMTimeout,
);
} else {
results = { logs: { errors: ['the contract deployment is currently unavailable'] } };
}
} else if (contract === 'contract' && action === 'registerTick' && payload) {
const authorizedAccountTickRegister = [
CONSTANTS.HIVE_ENGINE_ACCOUNT, CONSTANTS.HIVE_PEGGED_ACCOUNT];
if (authorizedAccountTickRegister.includes(sender)) {
results = await SmartContracts.registerTick(database, transaction);
} else {
results = { logs: { errors: ['registerTick unauthorized'] } };
}
} else {
results = await SmartContracts.executeSmartContract(// eslint-disable-line
database, transaction, this.blockNumber, this.timestamp,
this.refHiveBlockId, this.prevRefHiveBlockId, jsVMTimeout,
);
}
} else {
results = { logs: { errors: ['the parameters sender, contract and action are required'] } };
}
if (results.logs && results.logs.errors && results.logs.errors.find(m => m.includes('MongoError'))) {
throw new Error(`Mongo tx error, transaction: ${JSON.stringify(transaction)}, result: ${JSON.stringify(results)}`);
}
await database.flushCache();
await database.flushContractCache();
// get the database hash
newCurrentDatabaseHash = database.getDatabaseHash();
log.info('Tx results: ', results);
transaction.addLogs(results.logs);
transaction.executedCodeHash = results.executedCodeHash || ''; // eslint-disable-line
transaction.databaseHash = newCurrentDatabaseHash; // eslint-disable-line
transaction.calculateHash();
}
}
module.exports.Block = Block;