diff --git a/.gitignore b/.gitignore index 0e6068efa..dab78c775 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # XXX: Project Related -chains +ain_blockchain_data logs log1.txt log2.txt diff --git a/blockchain/block-file-util.js b/blockchain/block-file-util.js deleted file mode 100644 index 72c7632d3..000000000 --- a/blockchain/block-file-util.js +++ /dev/null @@ -1,109 +0,0 @@ -const fs = require('fs'); -const glob = require('glob'); -const path = require('path'); -const {compare} = require('natural-orderby'); -const zlib = require('zlib'); -const {CHAINS_N2B_DIR_NAME, CHAINS_H2N_DIR_NAME} = require('../common/constants'); -const ChainUtil = require('../common/chain-util'); -const FILE_NAME_SUFFIX = 'json.gz'; -const logger = require('../logger')('BLOCK-FILE-UTIL'); - -class BlockFileUtil { - static getBlockPath(chainPath, blockNumber) { - return path.join(chainPath, CHAINS_N2B_DIR_NAME, this.getBlockFilenameByNumber(blockNumber)); - } - - static getHashToNumberPath(chainPath, blockHash) { - return path.join(chainPath, CHAINS_H2N_DIR_NAME, blockHash); - } - - static getBlockFilenameByNumber(blockNumber) { - return `${blockNumber}.${FILE_NAME_SUFFIX}`; - } - - static getBlockFilename(block) { - return this.getBlockFilenameByNumber(block.number); - } - - // TODO(cshcomcom): Don't use glob? - static getAllBlockPaths(chainPath) { - const allBlockFilesPattern = `${chainPath}/${CHAINS_N2B_DIR_NAME}/*.${FILE_NAME_SUFFIX}`; - return glob.sync(allBlockFilesPattern).sort(compare()); - } - - static getBlockPaths(chainPath, from, to) { - const blockPaths = []; - for (let number = from; number < to; number++) { - const blockFile = this.getBlockPath(chainPath, number); - if (fs.existsSync(blockFile)) { - blockPaths.push(blockFile); - } - } - return blockPaths; - } - - static createBlockchainDir(chainPath) { - const n2bPath = path.join(chainPath, CHAINS_N2B_DIR_NAME); - const h2nPath = path.join(chainPath, CHAINS_H2N_DIR_NAME); - let isBlockEmpty = true; - - if (!fs.existsSync(chainPath)) { - fs.mkdirSync(chainPath, {recursive: true}); - } - - if (!fs.existsSync(n2bPath)) { - fs.mkdirSync(n2bPath); - } - - if (!fs.existsSync(h2nPath)) { - fs.mkdirSync(h2nPath); - } - - if (fs.readdirSync(n2bPath).length > 0) { - isBlockEmpty = false; - } - return isBlockEmpty; - } - - // TODO(cshcomcom): Change to asynchronous. - static readBlock(blockPath) { - const zippedFs = fs.readFileSync(blockPath); - return JSON.parse(zlib.gunzipSync(zippedFs).toString()); - } - - static readBlockByNumber(chainPath, blockNumber) { - const blockPath = this.getBlockPath(chainPath, blockNumber); - return this.readBlock(blockPath); - } - - // TODO(cshcomcom): Change to asynchronous. - static writeBlock(chainPath, block) { - const blockPath = this.getBlockPath(chainPath, block.number); - if (!fs.existsSync(blockPath)) { - const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(block))); - fs.writeFileSync(blockPath, compressed); - } else { - logger.debug(`${blockPath} file already exists!`); - } - } - - static writeHashToNumber(chainPath, blockHash, blockNumber) { - if (!blockHash || !ChainUtil.isNumber(blockNumber) || blockNumber < 0) { - logger.error(`Invalid writeHashToNumber parameters (${blockHash}, ${blockNumber})`); - return; - } - const hashToNumberPath = this.getHashToNumberPath(chainPath, blockHash); - if (!fs.existsSync(hashToNumberPath)) { - fs.writeFileSync(hashToNumberPath, blockNumber); - } else { - logger.debug(`${hashToNumberPath} file already exists!`); - } - } - - static readHashToNumber(chainPath, blockHash) { - const hashToNumberPath = this.getHashToNumberPath(chainPath, blockHash); - return Number(fs.readFileSync(hashToNumberPath).toString()); - } -} - -module.exports = BlockFileUtil; diff --git a/blockchain/block.js b/blockchain/block.js index 0ef476d41..da6bac52a 100644 --- a/blockchain/block.js +++ b/blockchain/block.js @@ -1,6 +1,7 @@ const stringify = require('fast-json-stable-stringify'); const sizeof = require('object-sizeof'); const moment = require('moment'); +const _ = require('lodash'); const logger = require('../logger')('BLOCK'); const ChainUtil = require('../common/chain-util'); const Transaction = require('../tx-pool/transaction'); @@ -259,7 +260,6 @@ class Block { } static buildGenesisStakingTxs(timestamp) { - const _ = require('lodash'); const txs = []; Object.entries(GENESIS_VALIDATORS).forEach(([address, amount], index) => { const privateKey = _.get(GenesisAccounts, diff --git a/blockchain/index.js b/blockchain/index.js index 32d9e3127..2292f5aac 100644 --- a/blockchain/index.js +++ b/blockchain/index.js @@ -3,8 +3,11 @@ const path = require('path'); const fs = require('fs'); const logger = require('../logger')('BLOCKCHAIN'); const { Block } = require('./block'); -const BlockFileUtil = require('./block-file-util'); -const { CHAINS_DIR } = require('../common/constants'); +const FileUtil = require('../common/file-util'); +const { + CHAINS_DIR, + CHAINS_N2B_DIR_NAME, +} = require('../common/constants'); const CHAIN_SEGMENT_LENGTH = 20; const ON_MEM_CHAIN_LENGTH = 20; @@ -13,11 +16,13 @@ class Blockchain { // Finalized chain this.chain = []; this.blockchainPath = path.resolve(CHAINS_DIR, basePath); + this.initSnapshotBlockNumber = -1; } - init(isFirstNode) { + init(isFirstNode, latestSnapshotBlockNumber) { let lastBlockWithoutProposal; - if (BlockFileUtil.createBlockchainDir(this.blockchainPath)) { + this.initSnapshotBlockNumber = latestSnapshotBlockNumber; + if (FileUtil.createBlockchainDir(this.blockchainPath)) { if (isFirstNode) { logger.info('\n'); logger.info('############################################################'); @@ -48,18 +53,23 @@ class Blockchain { logger.info('################################################################'); logger.info('\n'); } - const newChain = this.loadChain(); + const newChain = this.loadChain(latestSnapshotBlockNumber); if (newChain) { // NOTE(minsulee2): Deal with the case the only genesis block was generated. if (newChain.length > 1) { lastBlockWithoutProposal = newChain.pop(); - const lastBlockPath = BlockFileUtil.getBlockPath( + const lastBlockPath = FileUtil.getBlockPath( this.blockchainPath, lastBlockWithoutProposal.number); fs.unlinkSync(lastBlockPath); } this.chain = newChain; } } + if (!this.getBlockByNumber(0)) { + const genesisBlock = Block.genesis(); + FileUtil.writeBlock(this.blockchainPath, genesisBlock); + FileUtil.writeHashToNumber(this.blockchainPath, genesisBlock.hash, genesisBlock.number); + } return lastBlockWithoutProposal; } @@ -72,13 +82,12 @@ class Blockchain { */ getBlockByHash(hash) { if (!hash) return null; - const blockPath = BlockFileUtil.getBlockPath(this.blockchainPath, - BlockFileUtil.readHashToNumber(this.blockchainPath, hash)); - if (blockPath === undefined) { - const found = this.chain.filter((block) => block.hash === hash); - return found.length ? found[0] : null; + const blockPath = FileUtil.getBlockPath(this.blockchainPath, + FileUtil.readHashToNumber(this.blockchainPath, hash)); + if (!blockPath) { + return this.chain.find((block) => block.hash === hash); } else { - return Block.parse(BlockFileUtil.readBlock(blockPath)); + return Block.parse(FileUtil.readCompressedJson(blockPath)); } } @@ -90,12 +99,11 @@ class Blockchain { */ getBlockByNumber(number) { if (number === undefined || number === null) return null; - const blockPath = BlockFileUtil.getBlockPath(this.blockchainPath, number); - if (blockPath === undefined || number > this.lastBlockNumber() - ON_MEM_CHAIN_LENGTH) { - const found = this.chain.filter((block) => block.number === number); - return found.length ? found[0] : null; + const blockPath = FileUtil.getBlockPath(this.blockchainPath, number); + if (!blockPath || number > this.lastBlockNumber() - ON_MEM_CHAIN_LENGTH) { + return this.chain.find((block) => block.number === number); } else { - return Block.parse(BlockFileUtil.readBlock(blockPath)); + return Block.parse(FileUtil.readCompressedJson(blockPath)); } } @@ -109,6 +117,9 @@ class Blockchain { lastBlockNumber() { const lastBlock = this.lastBlock(); if (!lastBlock) { + if (this.initSnapshotBlockNumber) { + return this.initSnapshotBlockNumber; + } return -1; } return lastBlock.number; @@ -153,21 +164,31 @@ class Blockchain { return true; } - static isValidChain(chain) { + static isValidChain(chain, latestSnapshotBlockNumber) { + if (!chain.length) { + return true; + } const firstBlock = Block.parse(chain[0]); - if (!firstBlock || firstBlock.hash !== Block.genesis().hash) { - logger.error(`First block is not the Genesis block: ${firstBlock}\n${Block.genesis()}`); + if (!firstBlock) { + return false; + } + if (latestSnapshotBlockNumber > 0 && latestSnapshotBlockNumber + 1 !== firstBlock.number) { + logger.error(`Missing blocks between ${latestSnapshotBlockNumber + 1} and ${firstBlock.number}`); return false; } - if (!Block.validateHashes(firstBlock)) { - logger.error(`Genesis block is corrupted`); + if (firstBlock.number === 0 && firstBlock.hash !== Block.genesis().hash) { + logger.error(`Invalid genesis block: ${firstBlock}\n${Block.genesis()}`); return false; } - // TODO(liayoo): Check if the tx nonces are correct. return Blockchain.isValidChainSegment(chain); } static isValidChainSegment(chainSegment) { + if (chainSegment.length) { + if (!Block.validateHashes(chainSegment[0])) { + return false; + } + } for (let i = 1; i < chainSegment.length; i++) { const block = chainSegment[i]; const lastBlock = Block.parse(chainSegment[i - 1]); @@ -181,52 +202,16 @@ class Blockchain { writeChain() { for (let i = 0; i < this.chain.length; i++) { const block = this.chain[i]; - BlockFileUtil.writeBlock(this.blockchainPath, block); - BlockFileUtil.writeHashToNumber(this.blockchainPath, block.hash, block.number); - } - } - - /** - * Returns a section of the chain up to a maximuim of length CHAIN_SEGMENT_LENGTH, starting from - * the block number of the reference block. - * - * @param {Block} refBlock - The current highest block tin the querying nodes blockchain - * @return {list} A list of Block instances with refBlock at index 0, up to a maximuim length - * CHAIN_SEGMENT_LENGTH - */ - requestBlockchainSection(refBlock) { - const refBlockNumber = refBlock ? refBlock.number : -1; - const nextBlockNumber = refBlockNumber + 1; - - logger.info(`Current last block number: ${this.lastBlockNumber()}, ` + - `Requester's last block number: ${refBlockNumber}`); - - const blockPaths = BlockFileUtil.getBlockPaths(this.blockchainPath, nextBlockNumber, nextBlockNumber + CHAIN_SEGMENT_LENGTH); - - if (blockPaths.length > 0 && - (!!(refBlock) && Block.parse(BlockFileUtil.readBlock(blockPaths[0])).last_hash !== refBlock.hash)) { - logger.error('Invalid blockchain request. Requesters last block does not belong to ' + - 'this blockchain'); - return; + FileUtil.writeBlock(this.blockchainPath, block); + FileUtil.writeHashToNumber(this.blockchainPath, block.hash, block.number); } - - const refBlockHash = refBlock ? refBlock.hash : null; - if (refBlockHash === this.lastBlock().hash) { - logger.info(`Requesters blockchain is up to date with this blockchain`); - return [this.lastBlock()]; - } - - const chainSegment = []; - blockPaths.forEach((blockFile) => { - chainSegment.push(Block.parse(BlockFileUtil.readBlock(blockFile))); - }); - return chainSegment.length > 0 ? chainSegment : []; } - getValidBlocks(chainSegment) { + getValidBlocksInChainSegment(chainSegment) { logger.info(`Last block number before merge: ${this.lastBlockNumber()}`); const firstBlock = Block.parse(chainSegment[0]); - const lastBlockHash = this.lastBlockNumber() >= 0 ? this.lastBlock().hash : null; + const lastBlock = this.lastBlock(); + const lastBlockHash = this.lastBlockNumber() >= 0 && lastBlock ? lastBlock.hash : null; const overlap = lastBlockHash ? chainSegment.filter((block) => block.number === this.lastBlockNumber()) : null; const overlappingBlock = overlap ? overlap[0] : null; @@ -234,13 +219,13 @@ class Blockchain { if (lastBlockHash) { // Case 1: Not a cold start. if (overlappingBlock && overlappingBlock.hash !== lastBlockHash) { - logger.info(`The last block's hash ${this.lastBlock().hash.substring(0, 5)} ` + + logger.info(`The last block's hash ${lastBlock.hash.substring(0, 5)} ` + `does not match with the first block's hash ${firstBlock.hash.substring(0, 5)}`); return validBlocks; } } else { // Case 2: A cold start. - if (firstBlock.last_hash !== '') { + if (firstBlock.number === 0 && firstBlock.last_hash !== '') { logger.info(`First block of hash ${firstBlock.hash.substring(0, 5)} ` + `and last hash ${firstBlock.last_hash.substring(0, 5)} is not a genesis block`); return validBlocks; @@ -259,17 +244,24 @@ class Blockchain { return validBlocks; } - loadChain() { + /** + * Reads the block files at the chains n2b directory and returns a list of blocks starting from + * the latestSnapshotBlockNumber + 1. + * @param {Number} latestSnapshotBlockNumber + * @returns {list} A list of Blocks + */ + loadChain(latestSnapshotBlockNumber) { const chainPath = this.blockchainPath; const newChain = []; - const blockPaths = BlockFileUtil.getAllBlockPaths(chainPath); + const numBlockFiles = fs.readdirSync(path.join(chainPath, CHAINS_N2B_DIR_NAME)).length; + const blockPaths = FileUtil.getBlockPaths(chainPath, latestSnapshotBlockNumber + 1, numBlockFiles); blockPaths.forEach((blockPath) => { - const block = Block.parse(BlockFileUtil.readBlock(blockPath)); + const block = Block.parse(FileUtil.readCompressedJson(blockPath)); newChain.push(block); }); - if (Blockchain.isValidChain(newChain)) { + if (Blockchain.isValidChain(newChain, latestSnapshotBlockNumber)) { logger.info(`Valid chain of size ${newChain.length}`); return newChain; } @@ -278,7 +270,25 @@ class Blockchain { return null; } - getChainSection(from, to) { + /** + * Returns a section of the chain up to a maximuim of length CHAIN_SEGMENT_LENGTH, starting from + * the `from` block number up till `to` block number. + * + * @param {Number} from - The lowest block number to get + * @param {Number} to - The highest block number to geet + * @return {list} A list of Blocks, up to a maximuim length of CHAIN_SEGMENT_LENGTH + */ + getBlockList(from, to) { + const blockList = []; + const lastBlock = this.lastBlock(); + if (!lastBlock) { + return blockList; + } + if (from === lastBlock.number + 1) { + logger.info(`Requesters blockchain is up to date with this blockchain`); + blockList.push(lastBlock); + return blockList; + } if (!Number.isInteger(from) || from < 0) { from = 0; } @@ -288,13 +298,11 @@ class Blockchain { if (to - from > CHAIN_SEGMENT_LENGTH) { // NOTE: To prevent large query. to = from + CHAIN_SEGMENT_LENGTH; } - const chain = []; - const blockPaths = BlockFileUtil.getBlockPaths(this.blockchainPath, from, to); + const blockPaths = FileUtil.getBlockPaths(this.blockchainPath, from, to - from); blockPaths.forEach((blockPath) => { - const block = Block.parse(BlockFileUtil.readBlock(blockPath)); - chain.push(block); + blockList.push(Block.parse(FileUtil.readCompressedJson(blockPath))); }); - return chain; + return blockList; } } diff --git a/client/index.js b/client/index.js index 9228d6eef..aafcad09f 100755 --- a/client/index.js +++ b/client/index.js @@ -298,7 +298,7 @@ app.get('/connection_status', (req, res) => { app.get('/blocks', (req, res, next) => { const blockEnd = node.bc.lastBlockNumber() + 1; const blockBegin = Math.max(blockEnd - MAX_BLOCKS, 0); - const result = node.bc.getChainSection(blockBegin, blockEnd); + const result = node.bc.getBlockList(blockBegin, blockEnd); res.status(200) .set('Content-Type', 'application/json') .send({code: 0, result}) @@ -384,7 +384,9 @@ app.get('/get_transaction', (req, res, next) => { if (transactionInfo.status === TransactionStatus.BLOCK_STATUS) { const block = node.bc.getBlockByNumber(transactionInfo.number); const index = transactionInfo.index; - if (index >= 0) { + if (!block) { + logger.debug(`No block found for the tx: ${req.query.hash}`); + } else if (index >= 0) { transactionInfo.transaction = block.transactions[index]; } else { transactionInfo.transaction = _.find(block.last_votes, (tx) => tx.hash === req.query.hash); @@ -478,6 +480,9 @@ function createSingleSetTxBody(input, opType) { if (input.gas_price !== undefined) { txBody.gas_price = input.gas_price; } + if (input.billing !== undefined) { + txBody.billing = input.billing; + } return txBody; } @@ -500,6 +505,9 @@ function createMultiSetTxBody(input) { if (input.gas_price !== undefined) { txBody.gas_price = input.gas_price; } + if (input.billing !== undefined) { + txBody.billing = input.billing; + } return txBody; } diff --git a/client/protocol_versions.json b/client/protocol_versions.json index 76a8660de..5942ecf05 100644 --- a/client/protocol_versions.json +++ b/client/protocol_versions.json @@ -43,5 +43,8 @@ }, "0.7.6": { "min": "0.7.0" + }, + "0.7.7": { + "min": "0.7.0" } } \ No newline at end of file diff --git a/common/chain-util.js b/common/chain-util.js index 9757d7725..8f1feef8f 100644 --- a/common/chain-util.js +++ b/common/chain-util.js @@ -1,5 +1,3 @@ -const EC = require('elliptic').ec; -const ec = new EC('secp256k1'); const stringify = require('fast-json-stable-stringify'); const ainUtil = require('@ainblockchain/ain-util'); const _ = require('lodash'); @@ -72,18 +70,6 @@ class ChainUtil { return address; } - // TODO(liayoo): Remove this function. - static genKeyPair() { - let keyPair; - if (PRIVATE_KEY) { - keyPair = ec.keyFromPrivate(PRIVATE_KEY, 'hex'); - keyPair.getPublic(); - } else { - keyPair = ec.genKeyPair(); - } - return keyPair; - } - static isBool(value) { return ruleUtil.isBool(value); } @@ -164,6 +150,12 @@ class ChainUtil { return ruleUtil.toServiceAccountName(serviceType, serviceName, key); } + // NOTE(liayoo): billing is in the form <app name>|<billing id> + static toBillingAccountName(billing) { + const { PredefinedDbPaths } = require('../common/constants'); + return `${PredefinedDbPaths.BILLING}|${billing}`; + } + static toEscrowAccountName(source, target, escrowKey) { return ruleUtil.toEscrowAccountName(source, target, escrowKey); } @@ -353,13 +345,43 @@ class ChainUtil { static isAppPath(parsedPath) { const { PredefinedDbPaths } = require('../common/constants'); + return _.get(parsedPath, 0) === PredefinedDbPaths.APPS; } // TODO(liayoo): Fix testing paths (writing at the root) and update isServicePath(). static isServicePath(parsedPath) { - const { NATIVE_SERVICE_TYPES } = require('../common/constants'); - return NATIVE_SERVICE_TYPES.includes(_.get(parsedPath, 0)); + const { isServiceType } = require('../common/constants'); + + return isServiceType(_.get(parsedPath, 0)); + } + + static getDependentAppNameFromRef(ref) { + const { isAppDependentServiceType } = require('../common/constants'); + const parsedPath = ChainUtil.parsePath(ref); + const type = _.get(parsedPath, 0); + if (!type || !isAppDependentServiceType(type)) { + return null; + } + return _.get(parsedPath, 1, null); + } + + static getServiceDependentAppNameList(op) { + if (!op) { + return []; + } + if (op.op_list) { + const appNames = new Set(); + for (const innerOp of op.op_list) { + const name = ChainUtil.getDependentAppNameFromRef(innerOp.ref); + if (name) { + appNames.add(name); + } + } + return [...appNames]; + } + const name = ChainUtil.getDependentAppNameFromRef(op.ref); + return name ? [name] : []; } static getSingleOpGasAmount(parsedPath, value) { diff --git a/common/constants.js b/common/constants.js index 8fb2e6b24..39e18073d 100644 --- a/common/constants.js +++ b/common/constants.js @@ -1,10 +1,9 @@ -const os = require('os'); const fs = require('fs'); const path = require('path'); const semver = require('semver'); const ChainUtil = require('./chain-util'); -// Genesis configs. +// ** Genesis configs ** const DEFAULT_GENESIS_CONFIGS_DIR = 'genesis-configs/base'; const CUSTOM_GENESIS_CONFIGS_DIR = process.env.GENESIS_CONFIGS_DIR ? process.env.GENESIS_CONFIGS_DIR : null; @@ -12,7 +11,7 @@ const GenesisParams = getGenesisConfig('genesis_params.json'); const GenesisToken = getGenesisConfig('genesis_token.json'); const GenesisAccounts = getGenesisConfig('genesis_accounts.json'); -// Feature flags. +// ** Feature flags ** // NOTE(platfowner): If there is a corresponding env variable (e.g. force... flags), // the flag value will be OR-ed to the value. const FeatureFlags = { @@ -30,7 +29,7 @@ const FeatureFlags = { enableRichTxSelectionLogging: false, }; -// Environment variables. +// ** Environment variables ** const DEBUG = ChainUtil.convertEnvVarInputToBool(process.env.DEBUG); const CONSOLE_LOG = ChainUtil.convertEnvVarInputToBool(process.env.CONSOLE_LOG); const ENABLE_DEV_SET_CLIENT_API = ChainUtil.convertEnvVarInputToBool(process.env.ENABLE_DEV_SET_CLIENT_API); @@ -44,8 +43,9 @@ const ACCOUNT_INDEX = process.env.ACCOUNT_INDEX || null; const PORT = process.env.PORT || getPortNumber(8080, 8080); const P2P_PORT = process.env.P2P_PORT || getPortNumber(5000, 5000); const LIGHTWEIGHT = ChainUtil.convertEnvVarInputToBool(process.env.LIGHTWEIGHT); +const SYNC_MODE = process.env.SYNC_MODE || 'full'; -// Constants +// ** Constants ** const CURRENT_PROTOCOL_VERSION = require('../package.json').version; if (!semver.valid(CURRENT_PROTOCOL_VERSION)) { throw Error('Wrong version format is specified in package.json'); @@ -71,27 +71,17 @@ if (!fs.existsSync(BLOCKCHAIN_DATA_DIR)) { const CHAINS_DIR = path.resolve(BLOCKCHAIN_DATA_DIR, 'chains'); const CHAINS_N2B_DIR_NAME = 'n2b'; // NOTE: Block number to block. const CHAINS_H2N_DIR_NAME = 'h2n'; // NOTE: Block hash to block number. +const SNAPSHOTS_ROOT_DIR = path.resolve(BLOCKCHAIN_DATA_DIR, 'snapshots'); +const SNAPSHOTS_N2S_DIR_NAME = 'n2s'; // NOTE: Block number to snapshot. +const SNAPSHOTS_INTERVAL_BLOCK_NUMBER = 1000; // NOTE: How often the snapshot is made +const MAX_NUM_SNAPSHOTS = 10; // NOTE: max number of snapshots to keep const HASH_DELIMITER = '#'; const TX_NONCE_ERROR_CODE = 900; const TX_TIMESTAMP_ERROR_CODE = 901; const MILLI_AIN = 10**-3; // 1,000 milliain = 1 ain const MICRO_AIN = 10**-6; // 1,000,000 microain = 1 ain -const NATIVE_SERVICE_TYPES = [ - 'accounts', - 'checkin', - 'consensus', - 'escrow', - 'gas_fee', - 'manage_app', - 'payments', - 'service_accounts', - 'sharding', - 'staking', - 'test', // NOTE(platfowner): A temporary solution for tests. - 'transfer', -]; -// Enums +// ** Enums ** /** * Message types for communication between nodes. * @@ -143,6 +133,7 @@ const PredefinedDbPaths = { // Gas fee GAS_FEE: 'gas_fee', COLLECT: 'collect', + BILLING: 'billing', // Token TOKEN: 'token', TOKEN_NAME: 'name', @@ -325,6 +316,11 @@ const NativeFunctionIds = { UPDATE_LATEST_SHARD_REPORT: '_updateLatestShardReport', }; +function isNativeFunctionId(fid) { + const fidList = Object.values(NativeFunctionIds); + return fidList.includes(fid); +} + /** * Properties of sharding configs. * @@ -410,6 +406,8 @@ const FunctionResultCode = { IN_LOCKUP_PERIOD: 'IN_LOCKUP_PERIOD', INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE', INTERNAL_ERROR: 'INTERNAL_ERROR', // Something went wrong but don't know why + INVALID_ACCOUNT_NAME: 'INVALID_ACCOUNT_NAME', + INVALID_SERVICE_NAME: 'INVALID_SERVICE_NAME', SUCCESS: 'SUCCESS', }; @@ -455,6 +453,67 @@ const GasFeeConstants = { REST_FUNCTION_CALL_GAS_AMOUNT: 10, }; +// ** Lists ** + +/** + * Root labels of service paths. + */ +const SERVICE_TYPES = [ + PredefinedDbPaths.ACCOUNTS, + PredefinedDbPaths.CHECKIN, + PredefinedDbPaths.ESCROW, + PredefinedDbPaths.GAS_FEE, + PredefinedDbPaths.MANAGE_APP, + PredefinedDbPaths.PAYMENTS, + PredefinedDbPaths.SERVICE_ACCOUNTS, + PredefinedDbPaths.SHARDING, + PredefinedDbPaths.STAKING, + PredefinedDbPaths.TRANSFER, + 'test', // NOTE(platfowner): A temporary solution for tests. +]; + +function isServiceType(type) { + return SERVICE_TYPES.includes(type); +} + +/** + * Service types allowed to create service accounts. + */ +const SERVICE_ACCOUNT_SERVICE_TYPES = [ + PredefinedDbPaths.BILLING, + PredefinedDbPaths.ESCROW, + PredefinedDbPaths.GAS_FEE, + PredefinedDbPaths.PAYMENTS, + PredefinedDbPaths.STAKING, +]; + +function isServiceAccountServiceType(type) { + return SERVICE_ACCOUNT_SERVICE_TYPES.includes(type); +} + +/** + * Service types that are app-dependent. + */ +const APP_DEPENDENT_SERVICE_TYPES = [ + PredefinedDbPaths.MANAGE_APP, + PredefinedDbPaths.PAYMENTS, + PredefinedDbPaths.STAKING, +]; + +function isAppDependentServiceType(type) { + return APP_DEPENDENT_SERVICE_TYPES.includes(type); +} + +/** + * Sync mode options. + * + * @enum {string} + */ +const SyncModeOptions = { + FULL: 'full', + FAST: 'fast', +} + /** * Overwriting environment variables. * These parameters are defined in genesis_params.json, but if specified as environment variables, @@ -666,6 +725,10 @@ module.exports = { CHAINS_DIR, CHAINS_N2B_DIR_NAME, CHAINS_H2N_DIR_NAME, + SNAPSHOTS_ROOT_DIR, + SNAPSHOTS_N2S_DIR_NAME, + SNAPSHOTS_INTERVAL_BLOCK_NUMBER, + MAX_NUM_SNAPSHOTS, DEBUG, CONSOLE_LOG, ENABLE_DEV_SET_CLIENT_API, @@ -676,12 +739,12 @@ module.exports = { PORT, P2P_PORT, LIGHTWEIGHT, + SYNC_MODE, HASH_DELIMITER, TX_NONCE_ERROR_CODE, TX_TIMESTAMP_ERROR_CODE, MICRO_AIN, MILLI_AIN, - NATIVE_SERVICE_TYPES, MessageTypes, BlockchainNodeStates, PredefinedDbPaths, @@ -695,6 +758,7 @@ module.exports = { ProofProperties, StateInfoProperties, NativeFunctionIds, + isNativeFunctionId, ShardingProperties, ShardingProtocols, TokenExchangeSchemes, @@ -710,6 +774,10 @@ module.exports = { GenesisRules, GenesisOwners, GasFeeConstants, + SyncModeOptions, + isServiceType, + isServiceAccountServiceType, + isAppDependentServiceType, buildOwnerPermissions, buildRulePermission, ...GenesisParams.blockchain, diff --git a/common/file-util.js b/common/file-util.js new file mode 100644 index 000000000..20b85b9f3 --- /dev/null +++ b/common/file-util.js @@ -0,0 +1,169 @@ +const fs = require('fs'); +const path = require('path'); +const zlib = require('zlib'); +const _ = require('lodash'); +const { + CHAINS_N2B_DIR_NAME, + CHAINS_H2N_DIR_NAME, + SNAPSHOTS_N2S_DIR_NAME, +} = require('./constants'); +const ChainUtil = require('./chain-util'); +const JSON_GZIP_FILE_EXTENSION = 'json.gz'; +const logger = require('../logger')('FILE-UTIL'); + +class FileUtil { + static getBlockPath(chainPath, blockNumber) { + if (blockNumber < 0) return null; + return path.join(chainPath, CHAINS_N2B_DIR_NAME, this.getBlockFilenameByNumber(blockNumber)); + } + + static getSnapshotPathByBlockNumber(snapshotPath, blockNumber) { + return path.join(snapshotPath, SNAPSHOTS_N2S_DIR_NAME, this.getBlockFilenameByNumber(blockNumber)); + } + + static getHashToNumberPath(chainPath, blockHash) { + return path.join(chainPath, CHAINS_H2N_DIR_NAME, blockHash); + } + + static getBlockFilenameByNumber(blockNumber) { + return `${blockNumber}.${JSON_GZIP_FILE_EXTENSION}`; + } + + static getBlockFilename(block) { + return this.getBlockFilenameByNumber(block.number); + } + + static getLatestSnapshotInfo(snapshotPath) { + const snapshotPathPrefix = path.join(snapshotPath, SNAPSHOTS_N2S_DIR_NAME); + let latestSnapshotPath = null; + let latestSnapshotBlockNumber = -1; + let files = []; + try { + files = fs.readdirSync(snapshotPathPrefix); + } catch (err) { + logger.debug(`Failed to read snapshots: ${err.stack}`); + return { latestSnapshotPath, latestSnapshotBlockNumber }; + } + for (const file of files) { + const blockNumber = _.get(file.split(`.${JSON_GZIP_FILE_EXTENSION}`), 0); + if (blockNumber !== undefined && blockNumber > latestSnapshotBlockNumber) { + latestSnapshotPath = path.join(snapshotPathPrefix, file); + latestSnapshotBlockNumber = Number(blockNumber); + } + } + return { latestSnapshotPath, latestSnapshotBlockNumber }; + } + + static getBlockPaths(chainPath, from, size) { + const blockPaths = []; + if (size <= 0) return blockPaths; + for (let number = from; number < from + size; number++) { + const blockFile = this.getBlockPath(chainPath, number); + if (fs.existsSync(blockFile)) { + blockPaths.push(blockFile); + } else { + logger.debug(`blockFile (${blockFile}) does not exist`); + return blockPaths; + } + } + return blockPaths; + } + + static createBlockchainDir(chainPath) { + const n2bPath = path.join(chainPath, CHAINS_N2B_DIR_NAME); + const h2nPath = path.join(chainPath, CHAINS_H2N_DIR_NAME); + let isBlockEmpty = true; + + if (!fs.existsSync(chainPath)) { + fs.mkdirSync(chainPath, {recursive: true}); + } + + if (!fs.existsSync(n2bPath)) { + fs.mkdirSync(n2bPath); + } + + if (!fs.existsSync(h2nPath)) { + fs.mkdirSync(h2nPath); + } + + if (fs.readdirSync(n2bPath).length > 0) { + isBlockEmpty = false; + } + return isBlockEmpty; + } + + static createSnapshotDir(snapshotPath) { + if (!fs.existsSync(snapshotPath)) { + fs.mkdirSync(snapshotPath, { recursive: true }); + } + if (!fs.existsSync(path.join(snapshotPath, SNAPSHOTS_N2S_DIR_NAME))) { + fs.mkdirSync(path.join(snapshotPath, SNAPSHOTS_N2S_DIR_NAME)); + } + } + + // TODO(cshcomcom): Change to asynchronous. + static readCompressedJson(blockPath) { + try { + const zippedFs = fs.readFileSync(blockPath); + return JSON.parse(zlib.gunzipSync(zippedFs).toString()); + } catch (err) { + return null; + } + } + + static readBlockByNumber(chainPath, blockNumber) { + const blockPath = this.getBlockPath(chainPath, blockNumber); + return this.readCompressedJson(blockPath); + } + + // TODO(cshcomcom): Change to asynchronous. + static writeBlock(chainPath, block) { + const blockPath = this.getBlockPath(chainPath, block.number); + if (!fs.existsSync(blockPath)) { + const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(block))); + fs.writeFileSync(blockPath, compressed); + } else { + logger.debug(`${blockPath} file already exists!`); + } + } + + static writeHashToNumber(chainPath, blockHash, blockNumber) { + if (!blockHash || !ChainUtil.isNumber(blockNumber) || blockNumber < 0) { + logger.error(`Invalid writeHashToNumber parameters (${blockHash}, ${blockNumber})`); + return; + } + const hashToNumberPath = this.getHashToNumberPath(chainPath, blockHash); + if (!fs.existsSync(hashToNumberPath)) { + fs.writeFileSync(hashToNumberPath, blockNumber); + } else { + logger.debug(`${hashToNumberPath} file already exists!`); + } + } + + static readHashToNumber(chainPath, blockHash) { + try { + const hashToNumberPath = this.getHashToNumberPath(chainPath, blockHash); + return Number(fs.readFileSync(hashToNumberPath).toString()); + } catch (err) { + return -1; + } + } + + static writeSnapshot(snapshotPath, blockNumber, snapshot) { + const filePath = this.getSnapshotPathByBlockNumber(snapshotPath, blockNumber); + if (snapshot === null) { // Delete + if (fs.existsSync(filePath)) { + try { + fs.unlinkSync(filePath); + } catch (err) { + logger.debug(`Failed to delete ${filePath}: ${err.stack}`); + } + } + } else { + // TODO(liayoo): Change this operation to be asynchronous + fs.writeFileSync(filePath, zlib.gzipSync(Buffer.from(JSON.stringify(snapshot)))); + } + } +} + +module.exports = FileUtil; diff --git a/db/functions.js b/db/functions.js index 3461f3fe8..daf14abc1 100644 --- a/db/functions.js +++ b/db/functions.js @@ -84,14 +84,6 @@ class Functions { this.callStack = []; } - static isNativeFunctionId(fid) { - if (!fid) { - return false; - } - const fidList = Object.values(NativeFunctionIds); - return fidList.find((elem) => elem === fid) !== undefined; - } - /** * Runs functions of function paths matched with given database path. * @@ -545,6 +537,8 @@ class Functions { } _createApp(value, context) { + const { isValidServiceName } = require('./state-util'); + const appName = context.params.app_name; const recordId = context.params.record_id; const resultPath = PathUtil.getCreateAppResultPath(appName, recordId); @@ -552,6 +546,10 @@ class Functions { const adminConfig = value[PredefinedDbPaths.MANAGE_APP_CONFIG_ADMIN]; const billingConfig = _.get(value, PredefinedDbPaths.MANAGE_APP_CONFIG_BILLING); const serviceConfig = _.get(value, PredefinedDbPaths.MANAGE_APP_CONFIG_SERVICE); + if (!isValidServiceName(appName)) { + return this.saveAndReturnFuncResult( + context, resultPath, FunctionResultCode.INVALID_SERVICE_NAME); + } if (!ChainUtil.isDict(adminConfig)) { return this.saveAndReturnFuncResult(context, resultPath, FunctionResultCode.FAILURE); } diff --git a/db/index.js b/db/index.js index c556f80aa..5a886007e 100644 --- a/db/index.js +++ b/db/index.js @@ -36,6 +36,7 @@ const { isValidFunctionTree, isValidOwnerTree, applyFunctionChange, + applyOwnerChange, setProofHashForStateTree, updateProofHashForAllRootPaths, } = require('./state-util'); @@ -62,19 +63,26 @@ class DB { GenesisAccounts, [AccountProperties.OWNER, AccountProperties.ADDRESS]); } - initDbStates() { - // Initialize DB owners. - this.writeDatabase([PredefinedDbPaths.OWNERS_ROOT], { - [OwnerProperties.OWNER]: { - [OwnerProperties.OWNERS]: { - [OwnerProperties.ANYONE]: buildOwnerPermissions(true, true, true, true), + initDbStates(snapshot) { + if (snapshot) { + this.writeDatabase([PredefinedDbPaths.OWNERS_ROOT], JSON.parse(JSON.stringify(snapshot[PredefinedDbPaths.OWNERS_ROOT]))); + this.writeDatabase([PredefinedDbPaths.RULES_ROOT], JSON.parse(JSON.stringify(snapshot[PredefinedDbPaths.RULES_ROOT]))); + this.writeDatabase([PredefinedDbPaths.VALUES_ROOT], JSON.parse(JSON.stringify(snapshot[PredefinedDbPaths.VALUES_ROOT]))); + this.writeDatabase([PredefinedDbPaths.FUNCTIONS_ROOT], JSON.parse(JSON.stringify(snapshot[PredefinedDbPaths.FUNCTIONS_ROOT]))); + } else { + // Initialize DB owners. + this.writeDatabase([PredefinedDbPaths.OWNERS_ROOT], { + [OwnerProperties.OWNER]: { + [OwnerProperties.OWNERS]: { + [OwnerProperties.ANYONE]: buildOwnerPermissions(true, true, true, true), + } } - } - }); - // Initialize DB rules. - this.writeDatabase([PredefinedDbPaths.RULES_ROOT], { - [RuleProperties.WRITE]: true - }); + }); + // Initialize DB rules. + this.writeDatabase([PredefinedDbPaths.RULES_ROOT], { + [RuleProperties.WRITE]: true + }); + } } /** @@ -691,12 +699,12 @@ class DB { return this.setValue(valuePath, valueAfter, auth, timestamp, transaction, isGlobal); } - setFunction(functionPath, functionChange, auth, isGlobal) { - const isValidObj = isValidJsObjectForStates(functionChange); + setFunction(functionPath, func, auth, isGlobal) { + const isValidObj = isValidJsObjectForStates(func); if (!isValidObj.isValid) { return ChainUtil.returnTxResult(401, `Invalid object for states: ${isValidObj.invalidPath}`); } - const isValidFunction = isValidFunctionTree(functionChange); + const isValidFunction = isValidFunctionTree(func); if (!isValidFunction.isValid) { return ChainUtil.returnTxResult(405, `Invalid function tree: ${isValidFunction.invalidPath}`); } @@ -706,7 +714,7 @@ class DB { return ChainUtil.returnTxResult(402, `Invalid path: ${isValidPath.invalidPath}`); } if (!auth || auth.addr !== this.ownerAddress) { - const ownerOnlyFid = this.func.hasOwnerOnlyFunction(functionChange); + const ownerOnlyFid = this.func.hasOwnerOnlyFunction(func); if (ownerOnlyFid !== null) { return ChainUtil.returnTxResult( 403, `Trying to write owner-only function: ${ownerOnlyFid}`); @@ -722,7 +730,7 @@ class DB { return ChainUtil.returnTxResult(404, `No write_function permission on: ${functionPath}`); } const curFunction = this.getFunction(functionPath, isGlobal); - const newFunction = applyFunctionChange(curFunction, functionChange); + const newFunction = applyFunctionChange(curFunction, func); const fullPath = DB.getFullPath(localPath, PredefinedDbPaths.FUNCTIONS_ROOT); this.writeDatabase(fullPath, newFunction); return ChainUtil.returnTxResult(0, null, 1); @@ -781,9 +789,10 @@ class DB { return ChainUtil.returnTxResult( 603, `No write_owner or branch_owner permission on: ${ownerPath}`); } + const curOwner = this.getOwner(ownerPath, isGlobal); + const newOwner = applyOwnerChange(curOwner, owner); const fullPath = DB.getFullPath(localPath, PredefinedDbPaths.OWNERS_ROOT); - const ownerCopy = ChainUtil.isDict(owner) ? JSON.parse(JSON.stringify(owner)) : owner; - this.writeDatabase(fullPath, ownerCopy); + this.writeDatabase(fullPath, newOwner); return ChainUtil.returnTxResult(0, null, 1); } @@ -901,14 +910,9 @@ class DB { if (blockNumber > 0) { // Use only the service gas amount total result.gas_cost_total = ChainUtil.getTotalGasCost(gasPrice, gasAmountTotal.service); - if (result.gas_cost_total > 0) { - const gasFeeCollectPath = PathUtil.getGasFeeCollectPath(auth.addr, blockNumber, tx.hash); - const gasFeeCollectRes = this.setValue( - gasFeeCollectPath, { amount: result.gas_cost_total }, auth, timestamp, tx, false); - if (ChainUtil.isFailedTx(gasFeeCollectRes)) { - return ChainUtil.returnTxResult( - 15, `Failed to collect gas fee: ${JSON.stringify(gasFeeCollectRes, null, 2)}`, 0); - } + const collectFeeRes = this.checkBillingAndCollectFee(op, auth, timestamp, tx, blockNumber, result); + if (collectFeeRes !== true) { + return collectFeeRes; } } if (tx && auth && auth.addr && !auth.fid) { @@ -918,6 +922,48 @@ class DB { return result; } + checkBillingAndCollectFee(op, auth, timestamp, tx, blockNumber, result) { + if (result.gas_cost_total <= 0) { // No fees to collect + return true; + } + + const billing = tx.tx_body.billing; + if (!billing) { // Charge the individual account + return this.collectFee(auth.addr, result.gas_cost_total, auth, timestamp, tx, blockNumber); + } + const billingParsed = billing.split('|'); + if (billingParsed.length !== 2) { + const reason = 'Invalid billing param'; + return ChainUtil.returnTxResult(15, `Failed to collect gas fee: ${reason}`, 0); + } + const billingAppName = billingParsed[0]; + const billingServiceAcntName = ChainUtil.toBillingAccountName(billing); + const appNameList = ChainUtil.getServiceDependentAppNameList(op); + if (appNameList.length > 1) { + // More than 1 apps are involved. Cannot charge an app-related billing account. + const reason = 'Multiple app-dependent service operations for a billing account'; + return ChainUtil.returnTxResult(16, `Failed to collect gas fee: ${reason}`, 0); + } else if (appNameList.length === 1 && appNameList[0] !== billingAppName) { + // Tx app name doesn't match the billing account. + const reason = 'Invalid billing account'; + return ChainUtil.returnTxResult(17, `Failed to collect gas fee: ${reason}`, 0); + } + // Either app-independent or app name matches the billing account. + return this.collectFee( + billingServiceAcntName, result.gas_cost_total, auth, timestamp, tx, blockNumber); + } + + collectFee(billedTo, gasCost, auth, timestamp, tx, blockNumber) { + const gasFeeCollectPath = PathUtil.getGasFeeCollectPath(billedTo, blockNumber, tx.hash); + const gasFeeCollectRes = this.setValue( + gasFeeCollectPath, { amount: gasCost }, auth, timestamp, tx, false); + if (ChainUtil.isFailedTx(gasFeeCollectRes)) { + return ChainUtil.returnTxResult( + 18, `Failed to collect gas fee: ${JSON.stringify(gasFeeCollectRes, null, 2)}`, 0); + } + return true; + } + executeTransaction(tx, blockNumber = 0) { const LOG_HEADER = 'executeTransaction'; // NOTE(platfowner): A transaction needs to be converted to an executable form diff --git a/db/rule-util.js b/db/rule-util.js index 1314f45ec..86eeedfc5 100644 --- a/db/rule-util.js +++ b/db/rule-util.js @@ -67,11 +67,22 @@ class RuleUtil { } isServAcntName(name) { - return this.isString(name) && name.split('|').length >= 3; + const { isServiceAccountServiceType } = require('../common/constants'); + const { isValidServiceName } = require('./state-util'); + + if (!this.isString(name)) { + return false; + } + const parsed = name.split('|'); + if (parsed.length < 3) { + return false; + } + return isServiceAccountServiceType(parsed[0]) && isValidServiceName(parsed[1]); } isValShardProto(value) { - const {ShardingProtocols} = require('../common/constants'); + const { ShardingProtocols } = require('../common/constants'); + return value === ShardingProtocols.NONE || value === ShardingProtocols.POA; } @@ -135,6 +146,16 @@ class RuleUtil { } } + getBillingUserPath(billingServAcntName, userAddr) { + const { PredefinedDbPaths } = require('../common/constants'); + const parsed = this.parseServAcntName(billingServAcntName); + const appName = parsed[1]; + const billingId = parsed[2]; + return `/${PredefinedDbPaths.MANAGE_APP}/${appName}/${PredefinedDbPaths.MANAGE_APP_CONFIG}/` + + `${PredefinedDbPaths.MANAGE_APP_CONFIG_BILLING}/${billingId}/` + + `${PredefinedDbPaths.MANAGE_APP_CONFIG_BILLING_USERS}/${userAddr}`; + } + getOwnerAddr() { const { GenesisAccounts, AccountProperties } = require('../common/constants'); return _.get(GenesisAccounts, `${AccountProperties.OWNER}.${AccountProperties.ADDRESS}`, null); diff --git a/db/state-util.js b/db/state-util.js index f9bc16181..13d18728c 100644 --- a/db/state-util.js +++ b/db/state-util.js @@ -7,9 +7,11 @@ const ChainUtil = require('../common/chain-util'); const { FunctionProperties, FunctionTypes, + isNativeFunctionId, RuleProperties, OwnerProperties, ShardingProperties, + STATE_LABEL_LENGTH_LIMIT, } = require('../common/constants'); const Functions = require('./functions'); @@ -88,6 +90,11 @@ function isWritablePathWithSharding(fullPath, root) { return {isValid, invalidPath: isValid ? '' : ChainUtil.formatPath(path)}; } +function hasVarNamePattern(name) { + const varNameRegex = /^[A-Za-z_]+[A-Za-z0-9_]*$/gm; + return ChainUtil.isString(name) ? varNameRegex.test(name) : false; +} + function hasReservedChar(label) { const reservedCharRegex = /[\/\.\$\*#\{\}\[\]<>'"` \x00-\x1F\x7F]/gm; return ChainUtil.isString(label) ? reservedCharRegex.test(label) : false; @@ -100,9 +107,14 @@ function hasAllowedPattern(label) { (wildCardPatternRegex.test(label) || configPatternRegex.test(label)) : false; } +function isValidServiceName(name) { + return hasVarNamePattern(name); +} + function isValidStateLabel(label) { if (!ChainUtil.isString(label) || label === '' || + label.length > STATE_LABEL_LENGTH_LIMIT || (hasReservedChar(label) && !hasAllowedPattern(label))) { return false; } @@ -156,51 +168,51 @@ function isValidJsObjectForStates(obj) { /** * Checks the validity of the given rule configuration. */ - function isValidRuleConfig(ruleConfig) { - if (!ChainUtil.isBool(ruleConfig) && !ChainUtil.isString(ruleConfig)) { + function isValidRuleConfig(ruleConfigObj) { + if (!ChainUtil.isBool(ruleConfigObj) && !ChainUtil.isString(ruleConfigObj)) { return { isValid: false, invalidPath: ChainUtil.formatPath([]) }; } return { isValid: true, invalidPath: '' }; } -function sanitizeFunctionInfo(functionInfo) { - if (!functionInfo) { +function sanitizeFunctionInfo(functionInfoObj) { + if (!functionInfoObj) { return null; } - const functionType = functionInfo[FunctionProperties.FUNCTION_TYPE]; + const functionType = functionInfoObj[FunctionProperties.FUNCTION_TYPE]; const sanitized = {}; if (functionType === FunctionTypes.NATIVE) { sanitized[FunctionProperties.FUNCTION_TYPE] = functionType; sanitized[FunctionProperties.FUNCTION_ID] = - ChainUtil.stringOrEmpty(functionInfo[FunctionProperties.FUNCTION_ID]); + ChainUtil.stringOrEmpty(functionInfoObj[FunctionProperties.FUNCTION_ID]); } else if (functionType === FunctionTypes.REST) { sanitized[FunctionProperties.FUNCTION_TYPE] = functionType; sanitized[FunctionProperties.FUNCTION_ID] = - ChainUtil.stringOrEmpty(functionInfo[FunctionProperties.FUNCTION_ID]); + ChainUtil.stringOrEmpty(functionInfoObj[FunctionProperties.FUNCTION_ID]); sanitized[FunctionProperties.EVENT_LISTENER] = - ChainUtil.stringOrEmpty(functionInfo[FunctionProperties.EVENT_LISTENER]); + ChainUtil.stringOrEmpty(functionInfoObj[FunctionProperties.EVENT_LISTENER]); sanitized[FunctionProperties.SERVICE_NAME] = - ChainUtil.stringOrEmpty(functionInfo[FunctionProperties.SERVICE_NAME]); + ChainUtil.stringOrEmpty(functionInfoObj[FunctionProperties.SERVICE_NAME]); } return sanitized; } -function isValidFunctionInfo(functionInfo) { - if (ChainUtil.isEmpty(functionInfo)) { +function isValidFunctionInfo(functionInfoObj) { + if (ChainUtil.isEmpty(functionInfoObj)) { return false; } - const sanitized = sanitizeFunctionInfo(functionInfo); + const sanitized = sanitizeFunctionInfo(functionInfoObj); const isIdentical = - _.isEqual(JSON.parse(JSON.stringify(sanitized)), functionInfo, { strict: true }); + _.isEqual(JSON.parse(JSON.stringify(sanitized)), functionInfoObj, { strict: true }); if (!isIdentical) { return false; } - const eventListener = functionInfo[FunctionProperties.EVENT_LISTENER]; + const eventListener = functionInfoObj[FunctionProperties.EVENT_LISTENER]; if (eventListener !== undefined && - !validUrl.isUri(functionInfo[FunctionProperties.EVENT_LISTENER])) { + !validUrl.isUri(functionInfoObj[FunctionProperties.EVENT_LISTENER])) { return false; } return true; @@ -209,17 +221,17 @@ function isValidFunctionInfo(functionInfo) { /** * Checks the validity of the given function configuration. */ -function isValidFunctionConfig(functionConfig) { - if (!ChainUtil.isDict(functionConfig)) { +function isValidFunctionConfig(functionConfigObj) { + if (!ChainUtil.isDict(functionConfigObj)) { return { isValid: false, invalidPath: ChainUtil.formatPath([]) }; } - const fidList = Object.keys(functionConfig); + const fidList = Object.keys(functionConfigObj); if (ChainUtil.isEmpty(fidList)) { return { isValid: false, invalidPath: ChainUtil.formatPath([]) }; } for (const fid of fidList) { const invalidPath = ChainUtil.formatPath([fid]); - const functionInfo = functionConfig[fid]; + const functionInfo = functionConfigObj[fid]; if (functionInfo === null) { // Function deletion. continue; @@ -238,41 +250,41 @@ function isValidFunctionConfig(functionConfig) { return { isValid: true, invalidPath: '' }; } -function sanitizeOwnerPermissions(ownerPermissions) { - if (!ownerPermissions) { +function sanitizeOwnerPermissions(ownerPermissionsObj) { + if (!ownerPermissionsObj) { return null; } return { [OwnerProperties.BRANCH_OWNER]: - ChainUtil.boolOrFalse(ownerPermissions[OwnerProperties.BRANCH_OWNER]), + ChainUtil.boolOrFalse(ownerPermissionsObj[OwnerProperties.BRANCH_OWNER]), [OwnerProperties.WRITE_FUNCTION]: - ChainUtil.boolOrFalse(ownerPermissions[OwnerProperties.WRITE_FUNCTION]), + ChainUtil.boolOrFalse(ownerPermissionsObj[OwnerProperties.WRITE_FUNCTION]), [OwnerProperties.WRITE_OWNER]: - ChainUtil.boolOrFalse(ownerPermissions[OwnerProperties.WRITE_OWNER]), + ChainUtil.boolOrFalse(ownerPermissionsObj[OwnerProperties.WRITE_OWNER]), [OwnerProperties.WRITE_RULE]: - ChainUtil.boolOrFalse(ownerPermissions[OwnerProperties.WRITE_RULE]), + ChainUtil.boolOrFalse(ownerPermissionsObj[OwnerProperties.WRITE_RULE]), }; } -function isValidOwnerPermissions(ownerPermissions) { - if (ChainUtil.isEmpty(ownerPermissions)) { +function isValidOwnerPermissions(ownerPermissionsObj) { + if (ChainUtil.isEmpty(ownerPermissionsObj)) { return false; } - const sanitized = sanitizeOwnerPermissions(ownerPermissions); + const sanitized = sanitizeOwnerPermissions(ownerPermissionsObj); const isIdentical = - _.isEqual(JSON.parse(JSON.stringify(sanitized)), ownerPermissions, { strict: true }); + _.isEqual(JSON.parse(JSON.stringify(sanitized)), ownerPermissionsObj, { strict: true }); return isIdentical; } /** * Checks the validity of the given owner configuration. */ -function isValidOwnerConfig(ownerConfig) { - if (!ChainUtil.isDict(ownerConfig)) { +function isValidOwnerConfig(ownerConfigObj) { + if (!ChainUtil.isDict(ownerConfigObj)) { return { isValid: false, invalidPath: ChainUtil.formatPath([]) }; } const path = []; - const ownersProp = ownerConfig[OwnerProperties.OWNERS]; + const ownersProp = ownerConfigObj[OwnerProperties.OWNERS]; if (ownersProp === undefined) { return { isValid: false, invalidPath: ChainUtil.formatPath(path) }; } @@ -291,11 +303,15 @@ function isValidOwnerConfig(ownerConfig) { return { isValid: false, invalidPath }; } const fid = owner.substring(OwnerProperties.FID_PREFIX.length); - if (!Functions.isNativeFunctionId(fid)) { + if (!isNativeFunctionId(fid)) { return { isValid: false, invalidPath }; } } - const ownerPermissions = ChainUtil.getJsObject(ownerConfig, [...path, owner]); + const ownerPermissions = ChainUtil.getJsObject(ownerConfigObj, [...path, owner]); + if (ownerPermissions === null) { + // Owner deletion. + continue; + } if (!isValidOwnerPermissions(ownerPermissions)) { return { isValid: false, invalidPath }; } @@ -304,14 +320,14 @@ function isValidOwnerConfig(ownerConfig) { return { isValid: true, invalidPath: '' }; } -function isValidConfigTreeRecursive(stateTree, path, configLabel, stateConfigValidator) { - if (!ChainUtil.isDict(stateTree) || ChainUtil.isEmpty(stateTree)) { +function isValidConfigTreeRecursive(stateTreeObj, path, configLabel, stateConfigValidator) { + if (!ChainUtil.isDict(stateTreeObj) || ChainUtil.isEmpty(stateTreeObj)) { return { isValid: false, invalidPath: ChainUtil.formatPath(path) }; } - for (const label in stateTree) { + for (const label in stateTreeObj) { path.push(label); - const subtree = stateTree[label]; + const subtree = stateTreeObj[label]; if (label === configLabel) { const isValidConfig = stateConfigValidator(subtree); if (!isValidConfig.isValid) { @@ -336,73 +352,140 @@ function isValidConfigTreeRecursive(stateTree, path, configLabel, stateConfigVal /** * Checks the validity of the given rule tree. */ -function isValidRuleTree(ruleTree) { - if (ruleTree === null) { +function isValidRuleTree(ruleTreeObj) { + if (ruleTreeObj === null) { return { isValid: true, invalidPath: '' }; } - return isValidConfigTreeRecursive(ruleTree, [], RuleProperties.WRITE, isValidRuleConfig); + return isValidConfigTreeRecursive(ruleTreeObj, [], RuleProperties.WRITE, isValidRuleConfig); } /** * Checks the validity of the given function tree. */ -function isValidFunctionTree(functionTree) { - if (functionTree === null) { +function isValidFunctionTree(functionTreeObj) { + if (functionTreeObj === null) { return { isValid: true, invalidPath: '' }; } return isValidConfigTreeRecursive( - functionTree, [], FunctionProperties.FUNCTION, isValidFunctionConfig); + functionTreeObj, [], FunctionProperties.FUNCTION, isValidFunctionConfig); } /** * Checks the validity of the given owner tree. */ -function isValidOwnerTree(ownerTree) { - if (ownerTree === null) { +function isValidOwnerTree(ownerTreeObj) { + if (ownerTreeObj === null) { return { isValid: true, invalidPath: '' }; } - return isValidConfigTreeRecursive(ownerTree, [], OwnerProperties.OWNER, isValidOwnerConfig); + return isValidConfigTreeRecursive(ownerTreeObj, [], OwnerProperties.OWNER, isValidOwnerConfig); } /** - * Returns a new function created by applying the function change to the current function. - * @param {Object} curFunction current function (to be modified and returned by this function) - * @param {Object} functionChange function change + * Returns whether the given state tree object has the given config label as a property. */ -function applyFunctionChange(curFunction, functionChange) { - if (curFunction === null) { - // Just write the function change. - return functionChange; +function hasConfigLabel(stateTreeObj, configLabel) { + if (!ChainUtil.isDict(stateTreeObj)) { + return false; } - if (functionChange === null) { - // Just delete the existing value. - return null; + if (ChainUtil.getJsObject(stateTreeObj, [configLabel]) === null) { + return false; + } + + return true; +} + +/** + * Returns whether the given state tree object has the given config label as the only property. + */ +function hasConfigLabelOnly(stateTreeObj, configLabel) { + if (!hasConfigLabel(stateTreeObj, configLabel)) { + return false; + } + if (Object.keys(stateTreeObj).length !== 1) { + return false; + } + + return true; +} + +/** + * Returns a new function tree created by applying the function change to + * the current function tree. + * @param {Object} curFuncTree current function tree (to be modified by this function) + * @param {Object} functionChange function change + */ +function applyFunctionChange(curFuncTree, functionChange) { + // NOTE(platfowner): Partial set is applied only when the current function tree has + // .function property and the function change has .function property as the only property. + if (!hasConfigLabel(curFuncTree, FunctionProperties.FUNCTION) || + !hasConfigLabelOnly(functionChange, FunctionProperties.FUNCTION)) { + return ChainUtil.isDict(functionChange) ? + JSON.parse(JSON.stringify(functionChange)) : functionChange; } const funcChangeMap = ChainUtil.getJsObject(functionChange, [FunctionProperties.FUNCTION]); if (!funcChangeMap || Object.keys(funcChangeMap).length === 0) { - return curFunction; + return curFuncTree; } - const newFunction = - ChainUtil.isDict(curFunction) ? JSON.parse(JSON.stringify(curFunction)) : {}; - let newFuncMap = ChainUtil.getJsObject(newFunction, [FunctionProperties.FUNCTION]); - if (!newFuncMap || !ChainUtil.isDict(newFunction)) { + const newFuncConfig = + ChainUtil.isDict(curFuncTree) ? JSON.parse(JSON.stringify(curFuncTree)) : {}; + let newFuncMap = ChainUtil.getJsObject(newFuncConfig, [FunctionProperties.FUNCTION]); + if (!ChainUtil.isDict(newFuncMap)) { // Add a place holder. - ChainUtil.setJsObject(newFunction, [FunctionProperties.FUNCTION], {}); - newFuncMap = ChainUtil.getJsObject(newFunction, [FunctionProperties.FUNCTION]); + ChainUtil.setJsObject(newFuncConfig, [FunctionProperties.FUNCTION], {}); + newFuncMap = ChainUtil.getJsObject(newFuncConfig, [FunctionProperties.FUNCTION]); } for (const functionKey in funcChangeMap) { - const functionValue = funcChangeMap[functionKey]; - if (functionValue === null) { + const functionInfo = funcChangeMap[functionKey]; + if (functionInfo === null) { delete newFuncMap[functionKey]; } else { - newFuncMap[functionKey] = functionValue; + newFuncMap[functionKey] = functionInfo; + } + } + + return newFuncConfig; +} + +/** + * Returns a new owner tree created by applying the owner change to + * the current owner tree. + * @param {Object} curOwnerTree current owner tree (to be modified by this function) + * @param {Object} ownerChange owner change + */ +function applyOwnerChange(curOwnerTree, ownerChange) { + // NOTE(platfowner): Partial set is applied only when the current owner tree has + // .owner property and the owner change has .owner property as the only property. + if (!hasConfigLabel(curOwnerTree, OwnerProperties.OWNER) || + !hasConfigLabelOnly(ownerChange, OwnerProperties.OWNER)) { + return ChainUtil.isDict(ownerChange) ? + JSON.parse(JSON.stringify(ownerChange)) : ownerChange; + } + const ownerMapPath = [OwnerProperties.OWNER, OwnerProperties.OWNERS]; + const ownerChangeMap = ChainUtil.getJsObject(ownerChange, ownerMapPath); + if (!ownerChangeMap || Object.keys(ownerChangeMap).length === 0) { + return curOwnerTree; + } + const newOwnerConfig = + ChainUtil.isDict(curOwnerTree) ? JSON.parse(JSON.stringify(curOwnerTree)) : {}; + let newOwnerMap = ChainUtil.getJsObject(newOwnerConfig, ownerMapPath); + if (!ChainUtil.isDict(newOwnerMap)) { + // Add a place holder. + ChainUtil.setJsObject(newOwnerConfig, ownerMapPath, {}); + newOwnerMap = ChainUtil.getJsObject(newOwnerConfig, ownerMapPath); + } + for (const ownerKey in ownerChangeMap) { + const ownerPermissions = ownerChangeMap[ownerKey]; + if (ownerPermissions === null) { + delete newOwnerMap[ownerKey]; + } else { + newOwnerMap[ownerKey] = ownerPermissions; } } - return newFunction; + return newOwnerConfig; } /** @@ -592,6 +675,7 @@ module.exports = { hasReservedChar, hasAllowedPattern, isWritablePathWithSharding, + isValidServiceName, isValidStateLabel, isValidPathForStates, isValidJsObjectForStates, @@ -602,6 +686,7 @@ module.exports = { isValidOwnerConfig, isValidOwnerTree, applyFunctionChange, + applyOwnerChange, setStateTreeVersion, renameStateTreeVersion, deleteStateTree, diff --git a/genesis-configs/afan-shard/genesis_params.json b/genesis-configs/afan-shard/genesis_params.json index 8316ca6b0..c7e4124ba 100644 --- a/genesis-configs/afan-shard/genesis_params.json +++ b/genesis-configs/afan-shard/genesis_params.json @@ -27,12 +27,13 @@ "DEFAULT_MAX_INBOUND": 3 }, "resource": { - "BATCH_TX_LIST_SIZE_LIMIT": 50, "TREE_HEIGHT_LIMIT": 20, "TREE_SIZE_LIMIT": 1000000, + "STATE_LABEL_LENGTH_LIMIT": 150, "TX_BYTES_LIMIT": 10000, + "BATCH_TX_LIST_SIZE_LIMIT": 50, "TX_POOL_SIZE_LIMIT": 1000, - "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 50, + "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 100, "BANDWIDTH_BUDGET_PER_BLOCK": 10000, "SERVICE_BANDWIDTH_BUDGET_PER_BLOCK": 5000 } diff --git a/genesis-configs/base/genesis_params.json b/genesis-configs/base/genesis_params.json index e8a74899a..5e586f926 100644 --- a/genesis-configs/base/genesis_params.json +++ b/genesis-configs/base/genesis_params.json @@ -27,12 +27,13 @@ "DEFAULT_MAX_INBOUND": 3 }, "resource": { - "BATCH_TX_LIST_SIZE_LIMIT": 50, "TREE_HEIGHT_LIMIT": 20, "TREE_SIZE_LIMIT": 1000000, + "STATE_LABEL_LENGTH_LIMIT": 150, "TX_BYTES_LIMIT": 10000, + "BATCH_TX_LIST_SIZE_LIMIT": 50, "TX_POOL_SIZE_LIMIT": 1000, - "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 50, + "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 100, "BANDWIDTH_BUDGET_PER_BLOCK": 10000, "SERVICE_BANDWIDTH_BUDGET_PER_BLOCK": 5000 } diff --git a/genesis-configs/base/genesis_rules.json b/genesis-configs/base/genesis_rules.json index a123330eb..c6d9a0387 100644 --- a/genesis-configs/base/genesis_rules.json +++ b/genesis-configs/base/genesis_rules.json @@ -75,7 +75,7 @@ "$from": { "$block_number": { "$tx_hash": { - ".write": "auth.addr === $from && data === null && util.isDict(newData) && util.isNumber(newData.amount) && newData.amount <= getValue(util.getBalancePath($from))" + ".write": "(auth.addr === $from || (util.isServAcntName($from) && getValue(util.getBillingUserPath($from, auth.addr)) === true)) && data === null && util.isDict(newData) && util.isNumber(newData.amount) && newData.amount <= getValue(util.getBalancePath($from))" } } } diff --git a/genesis-configs/sim-shard/genesis_params.json b/genesis-configs/sim-shard/genesis_params.json index 864496abf..d01e0c7ab 100644 --- a/genesis-configs/sim-shard/genesis_params.json +++ b/genesis-configs/sim-shard/genesis_params.json @@ -27,12 +27,13 @@ "DEFAULT_MAX_INBOUND": 3 }, "resource": { - "BATCH_TX_LIST_SIZE_LIMIT": 50, "TREE_HEIGHT_LIMIT": 20, "TREE_SIZE_LIMIT": 1000000, + "STATE_LABEL_LENGTH_LIMIT": 150, "TX_BYTES_LIMIT": 10000, + "BATCH_TX_LIST_SIZE_LIMIT": 50, "TX_POOL_SIZE_LIMIT": 1000, - "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 50, + "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 100, "BANDWIDTH_BUDGET_PER_BLOCK": 10000, "SERVICE_BANDWIDTH_BUDGET_PER_BLOCK": 5000 } diff --git a/genesis-configs/testnet/genesis_params.json b/genesis-configs/testnet/genesis_params.json index 67c118cfa..3ebd89a5e 100644 --- a/genesis-configs/testnet/genesis_params.json +++ b/genesis-configs/testnet/genesis_params.json @@ -27,12 +27,13 @@ "DEFAULT_MAX_INBOUND": 3 }, "resource": { - "BATCH_TX_LIST_SIZE_LIMIT": 50, "TREE_HEIGHT_LIMIT": 20, "TREE_SIZE_LIMIT": 1000000, + "STATE_LABEL_LENGTH_LIMIT": 150, "TX_BYTES_LIMIT": 10000, + "BATCH_TX_LIST_SIZE_LIMIT": 50, "TX_POOL_SIZE_LIMIT": 1000, - "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 50, + "TX_POOL_SIZE_LIMIT_PER_ACCOUNT": 100, "BANDWIDTH_BUDGET_PER_BLOCK": 10000, "SERVICE_BANDWIDTH_BUDGET_PER_BLOCK": 5000 } diff --git a/integration/node.test.js b/integration/node.test.js index 63d3cc806..0be27b9b5 100644 --- a/integration/node.test.js +++ b/integration/node.test.js @@ -20,6 +20,7 @@ const { TX_BYTES_LIMIT, BATCH_TX_LIST_SIZE_LIMIT, TX_POOL_SIZE_LIMIT_PER_ACCOUNT, + MICRO_AIN, } = require('../common/constants'); const ChainUtil = require('../common/chain-util'); const { waitUntilTxFinalized, parseOrLog } = require('../unittest/test-util'); @@ -2537,41 +2538,47 @@ describe('Blockchain Node', () => { nonce: -1 }; - const txList1 = []; // Not over the limit. - for (let i = 0; i < TX_POOL_SIZE_LIMIT_PER_ACCOUNT; i++) { - const txBody = JSON.parse(JSON.stringify(txBodyTemplate)); - txBody.timestamp = timestamp + i; - const signature = - ainUtil.ecSignTransaction(txBody, Buffer.from(account.private_key, 'hex')); - txList1.push({ - tx_body: txBody, - signature, + let txCount = 0; + while (txCount < TX_POOL_SIZE_LIMIT_PER_ACCOUNT) { + const remainingTxCount = TX_POOL_SIZE_LIMIT_PER_ACCOUNT - txCount; + const batchTxSize = (remainingTxCount >= BATCH_TX_LIST_SIZE_LIMIT) ? + BATCH_TX_LIST_SIZE_LIMIT : remainingTxCount; + const txList1 = []; + for (let i = 0; i < batchTxSize; i++) { + const txBody = JSON.parse(JSON.stringify(txBodyTemplate)); + txBody.timestamp = timestamp + txCount + i; + const signature = + ainUtil.ecSignTransaction(txBody, Buffer.from(account.private_key, 'hex')); + txList1.push({ + tx_body: txBody, + signature, + }); + } + const res1 = await client.request('ain_sendSignedTransactionBatch', { + tx_list: txList1, + protoVer: CURRENT_PROTOCOL_VERSION }); - } - const res1 = await client.request('ain_sendSignedTransactionBatch', { - tx_list: txList1, - protoVer: CURRENT_PROTOCOL_VERSION - }); - const resultList1 = _.get(res1, 'result.result', null); - // Accepts transactions. - expect(ChainUtil.isArray(resultList1)).to.equal(true); - for (let i = 0; i < resultList1.length; i++) { - expect(ChainUtil.isFailedTx(resultList1[i].result)).to.equal(false); + const resultList1 = _.get(res1, 'result.result', null); + // Accepts transactions. + expect(ChainUtil.isArray(resultList1)).to.equal(true); + for (let i = 0; i < resultList1.length; i++) { + expect(ChainUtil.isFailedTx(resultList1[i].result)).to.equal(false); + } + + txCount += batchTxSize; } - const txList2 = []; // Just over the limit. - for (let i = 0; i < 1; i++) { - const txBody = JSON.parse(JSON.stringify(txBodyTemplate)); - txBody.timestamp = timestamp + TX_POOL_SIZE_LIMIT_PER_ACCOUNT + i; - const signature = - ainUtil.ecSignTransaction(txBody, Buffer.from(account.private_key, 'hex')); - txList2.push({ - tx_body: txBody, - signature, - }); - } + const txList2 = []; + const txBody = JSON.parse(JSON.stringify(txBodyTemplate)); + txBody.timestamp = timestamp + TX_POOL_SIZE_LIMIT_PER_ACCOUNT + 1; + const signature = + ainUtil.ecSignTransaction(txBody, Buffer.from(account.private_key, 'hex')); + txList2.push({ + tx_body: txBody, + signature, + }); const res2 = await client.request('ain_sendSignedTransactionBatch', { tx_list: txList2, protoVer: CURRENT_PROTOCOL_VERSION @@ -2585,7 +2592,7 @@ describe('Blockchain Node', () => { { "result": { "code": 4, - "error_message": "[executeTransactionAndAddToPool] Tx pool does NOT have enough room (50) for account: 0x85a620A5A46d01cc1fCF49E73ab00710d4da943E", + "error_message": "[executeTransactionAndAddToPool] Tx pool does NOT have enough room (100) for account: 0x85a620A5A46d01cc1fCF49E73ab00710d4da943E", "gas_amount": 0 }, "tx_hash": "erased" @@ -3713,9 +3720,125 @@ describe('Blockchain Node', () => { }); }); + describe('App creation', () => { + before(async () => { + const appStakingPath = + `/staking/test_service_create_app/${serviceAdmin}/0/stake/${Date.now()}/value`; + const appStakingRes = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { + ref: appStakingPath, + value: 1 + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, appStakingRes.tx_hash))) { + console.error(`Failed to check finalization of tx.`); + } + }); + + it("when successful with valid app name", async () => { + const manageAppPath = '/manage_app/test_service_create_app0/create/1'; + const createAppRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: manageAppPath, + value: { + admin: { [serviceAdmin]: true }, + }, + nonce: -1, + timestamp: 1234567890000, + }}).body.toString('utf-8')).result; + assert.deepEqual(createAppRes, { + "result": { + "code": 0, + "func_results": { + "_createApp": { + "code": "SUCCESS", + "gas_amount": 0, + "op_results": [ + { + "path": "/apps/test_service_create_app0", + "result": { + "code": 0, + "gas_amount": 1 + } + }, + { + "path": "/apps/test_service_create_app0", + "result": { + "code": 0, + "gas_amount": 1 + } + }, + { + "path": "/manage_app/test_service_create_app0/config", + "result": { + "code": 0, + "gas_amount": 1 + } + }, + { + "path": "/manage_app/test_service_create_app0/create/1/result", + "result": { + "code": 0, + "gas_amount": 1 + } + } + ] + } + }, + "gas_amount": 1, + "gas_amount_total": { + "app": { + "test_service_create_app0": 2 + }, + "service": 3 + }, + "gas_cost_total": 0 + }, + "tx_hash": "0x4e2a4bc009347bbaa1a14f1ddecb0f2b06d02d46326d33def7c346c613093079" + }); + }); + + it("when failed with invalid app name", async () => { + const manageAppPath = '/manage_app/0test_service_create_app/create/1'; + const createAppRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: manageAppPath, + value: { + admin: { [serviceAdmin]: true }, + }, + nonce: -1, + timestamp: 1234567890000, + }}).body.toString('utf-8')).result; + assert.deepEqual(createAppRes, { + "result": { + "code": 0, + "func_results": { + "_createApp": { + "code": "INVALID_SERVICE_NAME", + "gas_amount": 0, + "op_results": [ + { + "path": "/manage_app/0test_service_create_app/create/1/result", + "result": { + "code": 0, + "gas_amount": 1 + } + } + ] + } + }, + "gas_amount": 1, + "gas_amount_total": { + "app": {}, + "service": 2 + }, + "gas_cost_total": 0 + }, + "tx_hash": "0x60f6a71fedc8bbe457680ff6cf2e24b5c2097718f226c4f40fb4f9849d52f7fa" + }); + }); + }); + describe('Gas fee', () => { before(async () => { - const appStakingPath = `/staking/test_service_gas_fee/${serviceAdmin}/0/stake/${Date.now()}/value` + const appStakingPath = + `/staking/test_service_gas_fee/${serviceAdmin}/0/stake/${Date.now()}/value`; const appStakingRes = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { ref: appStakingPath, value: 1 @@ -3723,7 +3846,7 @@ describe('Blockchain Node', () => { if (!(await waitUntilTxFinalized(serverList, appStakingRes.tx_hash))) { console.error(`Failed to check finalization of tx.`); } - const manageAppPath = '/manage_app/test_service_gas_fee/create/1' + const manageAppPath = '/manage_app/test_service_gas_fee/create/1'; const createAppRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { ref: manageAppPath, value: { @@ -4136,6 +4259,113 @@ describe('Blockchain Node', () => { }}).body.toString('utf-8')); expect(bodyToUpperCase.code).to.equals(1); }); + + it('transfer: transfer with valid service account service type', async () => { + let fromBeforeBalance = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferFromBalancePath}`).body.toString('utf-8')).result; + const transferToService = `staking|test_service|${transferTo}|0`; + const transferToServiceBalancePath = + `/service_accounts/staking/test_service/${transferTo}|0/balance`; + const toServiceBeforeBalance = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferToServiceBalancePath}`) + .body.toString('utf-8')).result || 0; + const transferServicePath = `/transfer/${transferFrom}/${transferToService}`; + const body = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { + ref: transferServicePath + '/1/value', + value: transferAmount, + nonce: -1, + timestamp: 1234567890000, + }}).body.toString('utf-8')); + assert.deepEqual(body, { + "code": 0, + "result": { + "result": { + "code": 0, + "func_results": { + "_transfer": { + "code": "SUCCESS", + "gas_amount": 1000, + "op_results": [ + { + "path": "/accounts/0x00ADEc28B6a845a085e03591bE7550dd68673C1C/balance", + "result": { + "code": 0, + "gas_amount": 1 + } + }, + { + "path": "/service_accounts/staking/test_service/0x01A0980d2D4e418c7F27e1ef539d01A5b5E93204|0/balance", + "result": { + "code": 0, + "gas_amount": 1 + } + }, + { + "path": "/transfer/0x00ADEc28B6a845a085e03591bE7550dd68673C1C/staking|test_service|0x01A0980d2D4e418c7F27e1ef539d01A5b5E93204|0/1/result", + "result": { + "code": 0, + "gas_amount": 1 + } + } + ] + } + }, + "gas_amount": 1, + "gas_amount_total": { + "app": {}, + "service": 1004 + }, + "gas_cost_total": 0 + }, + "tx_hash": "0x62f01969d903d7a6f184279634249941a2c312e896f045c071afe78ac635fe96" + } + }); + if (!(await waitUntilTxFinalized([server2], _.get(body, 'result.tx_hash')))) { + console.error(`Failed to check finalization of tx.`); + } + const fromAfterBalance = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferFromBalancePath}`).body.toString('utf-8')).result; + const toServiceAfterBalance = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferToServiceBalancePath}`).body.toString('utf-8')).result; + const resultCode = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferServicePath}/1/result/code`) + .body.toString('utf-8')).result + expect(fromAfterBalance).to.equal(fromBeforeBalance - transferAmount); + expect(toServiceAfterBalance).to.equal(toServiceBeforeBalance + transferAmount); + expect(resultCode).to.equal(FunctionResultCode.SUCCESS); + }); + + it('transfer: transfer with invalid service account service type', async () => { + let fromBeforeBalance = parseOrLog(syncRequest('GET', + server2 + `/get_value?ref=${transferFromBalancePath}`).body.toString('utf-8')).result; + const transferToService = `invalid_service_type|test_service|${transferTo}|0`; + const transferServicePath = `/transfer/${transferFrom}/${transferToService}`; + const body = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { + ref: transferServicePath + '/1/value', + value: transferAmount, + nonce: -1, + timestamp: 1234567890000, + }}).body.toString('utf-8')); + assert.deepEqual(body, { + "code": 1, + "result": { + "result": { + "code": 103, + "error_message": "No .write permission on: /transfer/0x00ADEc28B6a845a085e03591bE7550dd68673C1C/invalid_service_type|test_service|0x01A0980d2D4e418c7F27e1ef539d01A5b5E93204|0/1/value", + "gas_amount": 0, + "gas_amount_total": { + "app": {}, + "service": 0 + }, + "gas_cost_total": 0 + }, + "tx_hash": "0x6cce46b284beb254c6b67205f5ba00f04c85028d7457410b4fa4b4d8522c14be" + } + }); + const fromAfterBalance = parseOrLog(syncRequest('GET', + server1 + `/get_value?ref=${transferFromBalancePath}`).body.toString('utf-8')).result; + expect(fromAfterBalance).to.equal(fromBeforeBalance); + }); }) describe('Staking: _stake, _unstake', () => { @@ -5593,4 +5823,368 @@ describe('Blockchain Node', () => { }); }); }); + + describe('Billing', async () => { + let serviceAdmin; // = server1 + let billingUserA; // = server2 + let billingUserB; // = server3 + let userBalancePathA; + let userBalancePathB; + const billingAccountBalancePathA = '/get_value?ref=/service_accounts/billing/test_billing/A/balance'; + before(async () => { + serviceAdmin = + parseOrLog(syncRequest('GET', server1 + '/get_address').body.toString('utf-8')).result; + billingUserA = + parseOrLog(syncRequest('GET', server2 + '/get_address').body.toString('utf-8')).result; + billingUserB = + parseOrLog(syncRequest('GET', server3 + '/get_address').body.toString('utf-8')).result; + userBalancePathA = `/get_value?ref=/accounts/${billingUserA}/balance`; + userBalancePathB = `/get_value?ref=/accounts/${billingUserB}/balance`; + + const appStakingRes = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { + ref: `/staking/test_billing/${serviceAdmin}/0/stake/${Date.now()}/value`, + value: 1 + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, appStakingRes.tx_hash))) { + console.error(`Failed to check finalization of app staking tx.`); + } + + const createAppRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing/create/0', + value: { + admin: { + [serviceAdmin]: true, + [billingUserA]: true, + [billingUserB]: true + }, + billing: { + A: { + users: { + [serviceAdmin]: true, + [billingUserA]: true + } + }, + B: { + users: { + [serviceAdmin]: true, + [billingUserB]: true + } + } + } + }, + nonce: -1, + timestamp: Date.now(), + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, createAppRes.tx_hash))) { + console.error(`Failed to check finalization of create app tx.`); + } + + const server4Addr = + parseOrLog(syncRequest('GET', server4 + '/get_address').body.toString('utf-8')).result; + const transferRes = parseOrLog(syncRequest('POST', server4 + '/set', {json: { + op_list: [ + { + ref: `/transfer/${server4Addr}/billing|test_billing|A/${Date.now()}/value`, + value: 100, + type: 'SET_VALUE' + }, + { + ref: `/transfer/${server4Addr}/billing|test_billing|B/${Date.now()}/value`, + value: 100, + type: 'SET_VALUE' + } + ], + nonce: -1, + timestamp: Date.now(), + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, transferRes.tx_hash))) { + console.error(`Failed to check finalization of transfer tx.`); + } + }); + + it('app txs are not charged by transfer', async () => { + const balanceBefore = parseOrLog(syncRequest('GET', server2 + userBalancePathA).body.toString('utf-8')).result; + const txWithoutBillingRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/apps/test_billing/test', + value: 'testing app tx', + gas_price: 1, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txWithoutBillingRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + const balanceAfter = parseOrLog(syncRequest('GET', server2 + userBalancePathA).body.toString('utf-8')).result; + assert.deepEqual(balanceAfter, balanceBefore); + + const billingAccountBalanceBefore = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + const txWithBillingRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/apps/test_billing/test', + value: 'testing app tx', + gas_price: 1, + billing: 'test_billing|A', + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txWithBillingRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + const billingAccountBalanceAfter = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + assert.deepEqual(billingAccountBalanceAfter, billingAccountBalanceBefore); + }); + + it('app-dependent service tx: individual account', async () => { + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing/config/service/staking/lockup_duration', + value: 1000, + gas_price: gasPrice, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + // NOTE(liayoo): Checking the gas fee was collected instead of account balances, since the + // nodes also participate in consensus & get the collected fees as rewards. + const tx = parseOrLog(syncRequest('GET', server2 + `/get_transaction?hash=${txRes.tx_hash}`).body.toString('utf-8')).result; + const gasFeeCollected = parseOrLog(syncRequest( + 'GET', + `${server2}/get_value?ref=/gas_fee/collect/${billingUserA}/${tx.number}/${txRes.tx_hash}/amount` + ).body.toString('utf-8')).result; + assert.deepEqual(gasFeeCollected, gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service); + }); + + it('app-dependent service tx: invalid billing param', async () => { + const txResBody = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing/config/service/staking/lockup_duration', + value: 1000, + gas_price: 1, + billing: 'A', + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')); + assert.deepEqual(txResBody, {code: 1, result: { tx_hash: null, result: false }}); + }); + + it('app-dependent service tx: not a billing account user', async () => { + const txResBody = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing/config/service/staking/lockup_duration', + value: 1000, + gas_price: 1, + billing: 'test_billing|B', + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')); + expect(txResBody.code).to.equals(1); + expect(txResBody.result.result.code, 18); + expect(txResBody.result.result.error_message.includes('No .write permission on: /gas_fee/collect/billing|test_billing|B'), true); + }); + + it('app-dependent service tx: billing account', async () => { + const billingAccountBalanceBefore = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing/config/service/staking/lockup_duration', + value: 1000, + gas_price: 1, + billing: 'test_billing|A', + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + const billingAccountBalanceAfter = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + assert.deepEqual( + billingAccountBalanceAfter, + billingAccountBalanceBefore - (gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service) + ); + }); + + it('app-independent service tx: individual account', async () => { + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: `/transfer/${billingUserA}/${billingUserB}/${Date.now()}/value`, + value: 1, + gas_price: gasPrice, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + // NOTE(liayoo): Checking the gas fee was collected instead of account balances, since the + // nodes also participate in consensus & get the collected fees as rewards. + const tx = parseOrLog(syncRequest('GET', server2 + `/get_transaction?hash=${txRes.tx_hash}`).body.toString('utf-8')).result; + const gasFeeCollected = parseOrLog(syncRequest( + 'GET', + `${server2}/get_value?ref=/gas_fee/collect/${billingUserA}/${tx.number}/${txRes.tx_hash}/amount` + ).body.toString('utf-8')).result; + assert.deepEqual(gasFeeCollected, gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service); + }); + + it('app-independent service tx: billing account', async () => { + const billingAccountBalanceBefore = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: `/transfer/${billingUserA}/${billingUserB}/${Date.now()}/value`, + value: 1, + gas_price: gasPrice, + billing: 'test_billing|A', + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + const billingAccountBalanceAfter = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + assert.deepEqual( + billingAccountBalanceAfter, + billingAccountBalanceBefore - (gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service) + ); + }); + + it('multi-set service tx: individual account', async () => { + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set', {json: { + op_list: [ + { + ref: `/transfer/${billingUserA}/${billingUserB}/${Date.now()}/value`, + value: 1, + type: 'SET_VALUE' + }, + { + ref: `/manage_app/test_billing/config/service/staking/lockup_duration`, + value: 100, + type: 'SET_VALUE' + } + ], + gas_price: gasPrice, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + // NOTE(liayoo): Checking the gas fee was collected instead of account balances, since the + // nodes also participate in consensus & get the collected fees as rewards. + const tx = parseOrLog(syncRequest('GET', server2 + `/get_transaction?hash=${txRes.tx_hash}`).body.toString('utf-8')).result; + const gasFeeCollected = parseOrLog(syncRequest( + 'GET', + `${server2}/get_value?ref=/gas_fee/collect/${billingUserA}/${tx.number}/${txRes.tx_hash}/amount` + ).body.toString('utf-8')).result; + assert.deepEqual(gasFeeCollected, gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service); + }); + + it('multi-set service tx: billing account', async () => { + const billingAccountBalanceBefore = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + const gasPrice = 1; + const txRes = parseOrLog(syncRequest('POST', server2 + '/set', {json: { + op_list: [ + { + ref: `/transfer/${billingUserA}/${billingUserB}/${Date.now()}/value`, + value: 1, + type: 'SET_VALUE' + }, + { + ref: `/manage_app/test_billing/config/service/staking/lockup_duration`, + value: 100, + type: 'SET_VALUE' + } + ], + billing: 'test_billing|A', + gas_price: gasPrice, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, txRes.tx_hash))) { + console.error(`Failed to check finalization of app tx.`); + } + const billingAccountBalanceAfter = parseOrLog(syncRequest( + 'GET', server2 + billingAccountBalancePathA).body.toString('utf-8')).result; + assert.deepEqual( + billingAccountBalanceAfter, + billingAccountBalanceBefore - (gasPrice * MICRO_AIN * txRes.result.gas_amount_total.service) + ); + }); + + it('multi-set service tx: multiple apps', async () => { + // Set up another app + const appStakingRes = parseOrLog(syncRequest('POST', server1 + '/set_value', {json: { + ref: `/staking/test_billing_2/${serviceAdmin}/0/stake/${Date.now()}/value`, + value: 1 + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, appStakingRes.tx_hash))) { + console.error(`Failed to check finalization of app staking tx.`); + } + + const createAppRes = parseOrLog(syncRequest('POST', server2 + '/set_value', {json: { + ref: '/manage_app/test_billing_2/create/0', + value: { + admin: { + [serviceAdmin]: true, + [billingUserA]: true, + [billingUserB]: true + }, + billing: { + '0': { + users: { + [serviceAdmin]: true, + [billingUserA]: true, + [billingUserB]: true + } + } + } + }, + nonce: -1, + timestamp: Date.now(), + }}).body.toString('utf-8')).result; + if (!(await waitUntilTxFinalized(serverList, createAppRes.tx_hash))) { + console.error(`Failed to check finalization of create app tx.`); + } + + const txResBody = parseOrLog(syncRequest('POST', server1 + '/set', {json: { + op_list: [ + { + ref: `/manage_app/test_billing/config/service/staking/lockup_duration`, + value: 100, + type: 'SET_VALUE' + }, + { + ref: `/manage_app/test_billing_2/config/service/staking/lockup_duration`, + value: 100, + type: 'SET_VALUE' + } + ], + billing: 'test_billing|A', + gas_price: 1, + nonce: -1, + timestamp: Date.now(), + } + }).body.toString('utf-8')); + assert.deepEqual(txResBody.result.result, { + "error_message": "Failed to collect gas fee: Multiple app-dependent service operations for a billing account", + "code": 16, + "gas_amount": 0 + }); + }); + }); }); diff --git a/json_rpc/index.js b/json_rpc/index.js index b86bc96f5..d6a63bd33 100644 --- a/json_rpc/index.js +++ b/json_rpc/index.js @@ -48,7 +48,7 @@ module.exports = function getMethods(node, p2pServer, minProtocolVersion, maxPro // Bloock API ain_getBlockList: function(args, done) { - const blocks = node.bc.getChainSection(args.from, args.to); + const blocks = node.bc.getBlockList(args.from, args.to); done(null, addProtocolVersion({result: blocks})); }, @@ -62,7 +62,7 @@ module.exports = function getMethods(node, p2pServer, minProtocolVersion, maxPro }, ain_getBlockHeadersList: function(args, done) { - const blocks = node.bc.getChainSection(args.from, args.to); + const blocks = node.bc.getBlockList(args.from, args.to); const blockHeaders = []; blocks.forEach((block) => { blockHeaders.push(block.header); @@ -215,7 +215,9 @@ module.exports = function getMethods(node, p2pServer, minProtocolVersion, maxPro if (transactionInfo.status === TransactionStatus.BLOCK_STATUS) { const block = node.bc.getBlockByNumber(transactionInfo.number); const index = transactionInfo.index; - if (index >= 0) { + if (!block) { + // TODO(liayoo): Ask peers for the transaction / block + } else if (index >= 0) { transactionInfo.transaction = block.transactions[index]; } else { transactionInfo.transaction = _.find(block.last_votes, (tx) => tx.hash === args.hash); @@ -233,7 +235,7 @@ module.exports = function getMethods(node, p2pServer, minProtocolVersion, maxPro if (args.block_hash && Number.isInteger(args.index)) { const index = Number(args.index); const block = node.bc.getBlockByHash(args.block_hash); - if (block.transactions.length > index && index >= 0) { + if (block && block.transactions.length > index && index >= 0) { result = { transaction: block.transactions[index], is_finalized: true @@ -248,7 +250,7 @@ module.exports = function getMethods(node, p2pServer, minProtocolVersion, maxPro if (Number.isInteger(args.block_number) && Number.isInteger(args.index)) { const index = Number(args.index); const block = node.bc.getBlockByNumber(args.block_number); - if (block.transactions.length > index && index >= 0) { + if (block && block.transactions.length > index && index >= 0) { result = { transaction: block.transactions[index], is_finalized: true diff --git a/node/index.js b/node/index.js index f99fd2a35..347e544ef 100644 --- a/node/index.js +++ b/node/index.js @@ -1,13 +1,19 @@ /* eslint guard-for-in: "off" */ const ainUtil = require('@ainblockchain/ain-util'); const _ = require('lodash'); +const fs = require('fs'); +const path = require('path'); const logger = require('../logger')('NODE'); const { FeatureFlags, PORT, ACCOUNT_INDEX, + SYNC_MODE, TX_NONCE_ERROR_CODE, TX_TIMESTAMP_ERROR_CODE, + SNAPSHOTS_ROOT_DIR, + SNAPSHOTS_INTERVAL_BLOCK_NUMBER, + MAX_NUM_SNAPSHOTS, BlockchainNodeStates, PredefinedDbPaths, ShardingProperties, @@ -15,8 +21,10 @@ const { GenesisAccounts, GenesisSharding, StateVersions, + SyncModeOptions, LIGHTWEIGHT } = require('../common/constants'); +const FileUtil = require('../common/file-util'); const ChainUtil = require('../common/chain-util'); const Blockchain = require('../blockchain'); const TransactionPool = require('../tx-pool'); @@ -51,6 +59,8 @@ class BlockchainNode { this.db = this.createDb(StateVersions.EMPTY, initialVersion, this.bc, this.tp, false, true); this.nonce = null; // nonce from current final version this.state = BlockchainNodeStates.STARTING; + this.snapshotDir = path.resolve(SNAPSHOTS_ROOT_DIR, `${PORT}`); + FileUtil.createSnapshotDir(this.snapshotDir); } // For testing purpose only. @@ -75,23 +85,51 @@ class BlockchainNode { init(isFirstNode) { const LOG_HEADER = 'init'; - logger.info(`[${LOG_HEADER}] Initializing node..`); - const lastBlockWithoutProposal = this.bc.init(isFirstNode); + let latestSnapshot = null; + let latestSnapshotPath = null; + let latestSnapshotBlockNumber = -1; + + // 1. Get the latest snapshot if in the "fast" sync mode. + if (SYNC_MODE === SyncModeOptions.FAST) { + const latestSnapshotInfo = FileUtil.getLatestSnapshotInfo(this.snapshotDir); + latestSnapshotPath = latestSnapshotInfo.latestSnapshotPath; + latestSnapshotBlockNumber = latestSnapshotInfo.latestSnapshotBlockNumber; + if (latestSnapshotPath) { + try { + latestSnapshot = FileUtil.readCompressedJson(latestSnapshotPath); + } catch (err) { + logger.error(`[${LOG_HEADER}] ${err.stack}`); + } + } + } + + // 2. Initialize the blockchain, starting from `latestSnapshotBlockNumber`. + const lastBlockWithoutProposal = this.bc.init(isFirstNode, latestSnapshotBlockNumber); + + // 3. Initialize DB (with the latest snapshot, if it exists) const startingDb = this.createDb(StateVersions.EMPTY, StateVersions.START, this.bc, this.tp, true); - startingDb.initDbStates(); + startingDb.initDbStates(latestSnapshot); + + // 4. Execute the chain on the DB and finalize it. this.executeChainOnDb(startingDb); - this.nonce = this.getNonceFromChain(); this.cloneAndFinalizeVersion(StateVersions.START, this.bc.lastBlockNumber()); + this.nonce = this.getNonceForAddr(this.account.address, false, true); + + // 5. Execute transactions from the pool. this.db.executeTransactionList( this.tp.getValidTransactions(null, this.stateManager.getFinalVersion()), this.bc.lastBlockNumber() + 1); + + // 6. Node status changed: STARTING -> SYNCING. this.state = BlockchainNodeStates.SYNCING; + return lastBlockWithoutProposal; } createTempDb(baseVersion, versionPrefix, blockNumberSnapshot) { + const LOG_HEADER = 'createTempDb'; const { tempVersion, tempRoot } = this.stateManager.cloneToTempVersion( baseVersion, versionPrefix); if (!tempRoot) { @@ -175,44 +213,30 @@ class BlockchainNode { } const nodeVersion = `${StateVersions.NODE}:${blockNumber}`; this.syncDbAndNonce(nodeVersion); + this.updateSnapshots(blockNumber); } - dumpFinalVersion(withDetails) { - return this.stateManager.getFinalRoot().toJsObject(withDetails); - } - - getNonceFromChain() { - const LOG_HEADER = 'getNonceFromChain'; - - // TODO(cshcomcom): Search through all blocks for any previous nonced transaction with current - // account. - let nonce = 0; - for (let i = this.bc.chain.length - 1; i > -1; i--) { - for (let j = this.bc.chain[i].transactions.length - 1; j > -1; j--) { - if (ChainUtil.areSameAddrs(this.bc.chain[i].transactions[j].address, - this.account.address) && this.bc.chain[i].transactions[j].tx_body.nonce > -1) { - // If blockchain is being restarted, retreive nonce from blockchain - nonce = this.bc.chain[i].transactions[j].tx_body.nonce + 1; - break; - } - } - if (nonce > 0) { - break; - } + updateSnapshots(blockNumber) { + if (blockNumber > 0 && blockNumber % SNAPSHOTS_INTERVAL_BLOCK_NUMBER === 0) { + const snapshot = this.dumpFinalVersion(false); + FileUtil.writeSnapshot(this.snapshotDir, blockNumber, snapshot); + FileUtil.writeSnapshot( + this.snapshotDir, blockNumber - MAX_NUM_SNAPSHOTS * SNAPSHOTS_INTERVAL_BLOCK_NUMBER, null); } + } - logger.info(`[${LOG_HEADER}] Setting nonce to ${nonce}`); - return nonce; + dumpFinalVersion(withDetails) { + return this.stateManager.getFinalRoot().toJsObject(withDetails); } - getNonceForAddr(address, fromPending) { + getNonceForAddr(address, fromPending, fromDb = false) { if (!isValAddr(address)) return -1; const cksumAddr = toCksumAddr(address); if (fromPending) { const { nonce } = this.db.getAccountNonceAndTimestamp(cksumAddr); return nonce; } - if (cksumAddr === this.account.address) { + if (!fromDb && cksumAddr === this.account.address) { return this.nonce; } const stateRoot = this.stateManager.getFinalRoot(); @@ -428,7 +452,7 @@ class BlockchainNode { logger.error(`Failed to create a temp database with state version: ${baseVersion}.`); return null; } - const validBlocks = this.bc.getValidBlocks(chainSegment); + const validBlocks = this.bc.getValidBlocksInChainSegment(chainSegment); if (validBlocks.length > 0) { if (!this.applyBlocksToDb(validBlocks, tempDb)) { logger.error(`[${LOG_HEADER}] Failed to apply valid blocks to database: ` + @@ -461,15 +485,22 @@ class BlockchainNode { executeChainOnDb(db) { const LOG_HEADER = 'executeChainOnDb'; - this.bc.chain.forEach((block) => { + for (const block of this.bc.chain) { if (!db.executeTransactionList(block.last_votes)) { - logger.error(`[${LOG_HEADER}] Failed to execute last_votes`) + logger.error(`[${LOG_HEADER}] Failed to execute last_votes (${block.number})`); + process.exit(1); // NOTE(liayoo): Quick fix for the problem. May be fixed by deleting the block files. } if (!db.executeTransactionList(block.transactions, block.number)) { - logger.error(`[${LOG_HEADER}] Failed to execute transactions`) + logger.error(`[${LOG_HEADER}] Failed to execute transactions (${block.number})`) + process.exit(1); // NOTE(liayoo): Quick fix for the problem. May be fixed by deleting the block files. + } + if (block.state_proof_hash !== db.stateRoot.getProofHash()) { + logger.error(`[${LOG_HEADER}] Invalid state proof hash (${block.number}): ` + + `${db.stateRoot.getProofHash()}, ${block.state_proof_hash}`); + process.exit(1); // NOTE(liayoo): Quick fix for the problem. May be fixed by deleting the block files. } this.tp.cleanUpForNewBlock(block); - }); + } } } diff --git a/p2p/index.js b/p2p/index.js index 409abd2d7..e74e93d84 100644 --- a/p2p/index.js +++ b/p2p/index.js @@ -1,9 +1,7 @@ /* eslint no-mixed-operators: "off" */ const _ = require('lodash'); const P2pServer = require('./server'); -const url = require('url'); const Websocket = require('ws'); -const semver = require('semver'); const logger = require('../logger')('P2P_CLIENT'); const { ConsensusStatus } = require('../consensus/constants'); const VersionUtil = require('../common/version-util'); @@ -16,8 +14,7 @@ const { DEFAULT_MAX_OUTBOUND, DEFAULT_MAX_INBOUND, MAX_OUTBOUND_LIMIT, - MAX_INBOUND_LIMIT, - FeatureFlags + MAX_INBOUND_LIMIT } = require('../common/constants'); const { sleep } = require('../common/chain-util'); const { @@ -46,8 +43,8 @@ class P2pClient { this.startHeartbeat(); } - run() { - this.server.listen(); + async run() { + await this.server.listen(); this.connectToTracker(); } @@ -97,31 +94,26 @@ class P2pClient { } getNetworkStatus() { + const extIp = this.server.getExternalIp(); + const url = new URL(`ws://${extIp}:${P2P_PORT}`); + const p2pUrl = url.toString(); + url.protocol = 'http:'; + url.port = PORT; + const clientApiUrl = url.toString(); + url.pathname = 'json-rpc'; + const jsonRpcUrl = url.toString(); return { - ip: this.server.getExternalIp(), + ip: extIp, p2p: { - url: url.format({ - protocol: 'ws', - hostname: this.server.getExternalIp(), - port: P2P_PORT - }), + url: p2pUrl, port: P2P_PORT, }, clientApi: { - url: url.format({ - protocol: 'http', - hostname: this.server.getExternalIp(), - port: PORT - }), + url: clientApiUrl, port: PORT, }, jsonRpc: { - url: url.format({ - protocol: 'http', - hostname: this.server.getExternalIp(), - port: PORT, - pathname: '/json-rpc', - }), + url: jsonRpcUrl, port: PORT, }, connectionStatus: this.getConnectionStatus() @@ -165,7 +157,6 @@ class P2pClient { `${JSON.stringify(this.server.managedPeersInfo, null, 2)}`); } if (node.state === BlockchainNodeStates.STARTING) { - node.state = BlockchainNodeStates.SYNCING; if (parsedMsg.numLivePeers === 0) { const lastBlockWithoutProposal = node.init(true); await this.server.tryInitializeShard(); @@ -214,9 +205,12 @@ class P2pClient { logger.debug(`SENDING: ${JSON.stringify(consensusMessage)}`); } - requestChainSegment(socket, lastBlock) { - const payload = encapsulateMessage(MessageTypes.CHAIN_SEGMENT_REQUEST, - { lastBlock: lastBlock }); + requestChainSegment(socket, lastBlockNumber) { + if (this.server.node.state !== BlockchainNodeStates.SYNCING && + this.server.node.state !== BlockchainNodeStates.SERVING) { + return; + } + const payload = encapsulateMessage(MessageTypes.CHAIN_SEGMENT_REQUEST, { lastBlockNumber }); if (!payload) { logger.error('The request chainSegment cannot be sent because of msg encapsulation failure.'); return; @@ -256,36 +250,6 @@ class P2pClient { socket.send(JSON.stringify(payload)); } - // TODO(minsulee2): This check will be updated when data compatibility version up. - checkDataProtoVerForAddressResponse(version) { - const majorVersion = VersionUtil.toMajorVersion(version); - const isGreater = semver.gt(this.server.majorDataProtocolVersion, majorVersion); - if (isGreater) { - // TODO(minsulee2): Compatible message. - } - const isLower = semver.lt(this.server.majorDataProtocolVersion, majorVersion); - if (isLower) { - // TODO(minsulee2): Compatible message. - } - } - - checkDataProtoVerForChainSegmentResponse(version) { - const majorVersion = VersionUtil.toMajorVersion(version); - const isGreater = semver.gt(this.server.majorDataProtocolVersion, majorVersion); - if (isGreater) { - // TODO(minsulee2): Compatible message. - } - const isLower = semver.lt(this.server.majorDataProtocolVersion, majorVersion); - if (isLower) { - if (FeatureFlags.enableRichP2pCommunicationLogging) { - logger.error('CANNOT deal with higher data protocol version. Discard the ' + - 'CHAIN_SEGMENT_RESPONSE message.'); - } - return false; - } - return true; - } - setPeerEventHandlers(socket) { const LOG_HEADER = 'setPeerEventHandlers'; socket.on('message', (message) => { @@ -307,8 +271,12 @@ class P2pClient { switch (parsedMessage.type) { case MessageTypes.ADDRESS_RESPONSE: - // TODO(minsulee2): Add compatibility check here after data version up. - // this.checkDataProtoVerForAddressResponse(dataProtoVer); + const dataVersionCheckForAddress = + this.server.checkDataProtoVer(dataProtoVer, MessageTypes.ADDRESS_RESPONSE); + if (dataVersionCheckForAddress < 0) { + // TODO(minsulee2): need to convert message when updating ADDRESS_RESPONSE necessary. + // this.convertAddressMessage(); + } const address = _.get(parsedMessage, 'data.body.address'); if (!address) { logger.error(`[${LOG_HEADER}] Providing an address is compulsary when initiating ` + @@ -342,8 +310,21 @@ class P2pClient { } break; case MessageTypes.CHAIN_SEGMENT_RESPONSE: - if (!this.checkDataProtoVerForChainSegmentResponse(parsedMessage.dataProtoVer)) { + if (this.server.node.state !== BlockchainNodeStates.SYNCING && + this.server.node.state !== BlockchainNodeStates.SERVING) { + logger.error(`[${LOG_HEADER}] Not ready to process chain segment response.\n` + + `Node state: ${this.server.node.state}.`); + return; + } + const dataVersionCheckForChainSegment = + this.server.checkDataProtoVer(dataProtoVer, MessageTypes.CHAIN_SEGMENT_RESPONSE); + if (dataVersionCheckForChainSegment > 0) { + logger.error(`[${LOG_HEADER}] CANNOT deal with higher data protocol ` + + `version(${dataProtoVer}). Discard the CHAIN_SEGMENT_RESPONSE message.`); return; + } else if (dataVersionCheckForChainSegment < 0) { + // TODO(minsulee2): need to convert message when updating CHAIN_SEGMENT_RESPONSE. + // this.convertChainSegmentResponseMessage(); } const chainSegment = _.get(parsedMessage, 'data.chainSegment'); const number = _.get(parsedMessage, 'data.number'); @@ -394,7 +375,7 @@ class P2pClient { // your local blockchain matches the height of the consensus blockchain. if (number > this.server.node.bc.lastBlockNumber()) { setTimeout(() => { - this.requestChainSegment(socket, this.server.node.bc.lastBlock()); + this.requestChainSegment(socket, this.server.node.bc.lastBlockNumber()); }, 1000); } } else { @@ -413,13 +394,13 @@ class P2pClient { logger.info(`[${LOG_HEADER}] I am behind ` + `(${number} < ${this.server.node.bc.lastBlockNumber()}).`); setTimeout(() => { - this.requestChainSegment(socket, this.server.node.bc.lastBlock()); + this.requestChainSegment(socket, this.server.node.bc.lastBlockNumber()); }, 1000); } } break; default: - logger.error(`[${LOG_HEADER}] Wrong message type(${parsedMessage.type}) has been ` + + logger.error(`[${LOG_HEADER}] Unknown message type(${parsedMessage.type}) has been ` + `specified. Igonore the message.`); break; } @@ -464,7 +445,7 @@ class P2pClient { this.setPeerEventHandlers(socket); this.sendAddress(socket); await this.waitForAddress(socket); - this.requestChainSegment(socket, this.server.node.bc.lastBlock()); + this.requestChainSegment(socket, this.server.node.bc.lastBlockNumber()); if (this.server.consensus.stakeTx) { this.broadcastTransaction(this.server.consensus.stakeTx); this.server.consensus.stakeTx = null; diff --git a/p2p/server.js b/p2p/server.js index 9b174056d..ac2929c98 100644 --- a/p2p/server.js +++ b/p2p/server.js @@ -3,15 +3,14 @@ const Websocket = require('ws'); const ip = require('ip'); const publicIp = require('public-ip'); const axios = require('axios'); -const semver = require('semver'); const disk = require('diskusage'); const os = require('os'); const v8 = require('v8'); const _ = require('lodash'); +const semver = require('semver'); const ainUtil = require('@ainblockchain/ain-util'); const logger = require('../logger')('P2P_SERVER'); const Consensus = require('../consensus'); -const { Block } = require('../blockchain/block'); const Transaction = require('../tx-pool/transaction'); const VersionUtil = require('../common/version-util'); const { @@ -76,7 +75,7 @@ class P2pServer { this.maxInbound = maxInbound; } - listen() { + async listen() { this.wsServer = new Websocket.Server({ port: P2P_PORT, // Enables server-side compression. For option details, see @@ -106,7 +105,7 @@ class P2pServer { this.setPeerEventHandlers(socket); }); logger.info(`Listening to peer-to-peer connections on: ${P2P_PORT}\n`); - this.setUpIpAddresses().then(() => { }); + await this.setUpIpAddresses(); } getNodeAddress() { @@ -323,50 +322,27 @@ class P2pServer { }); } - // TODO(minsulee2): This check will be updated when data compatibility version up. - checkDataProtoVerForAddressRequest(version) { - const majorVersion = VersionUtil.toMajorVersion(version); - const isGreater = semver.gt(this.majorDataProtocolVersion, majorVersion); - if (isGreater) { - // TODO(minsulee2): Compatible message. - } - const isLower = semver.lt(this.majorDataProtocolVersion, majorVersion); - if (isLower) { - // TODO(minsulee2): Compatible message. - } - } - - checkDataProtoVerForConsensus(version) { - const majorVersion = VersionUtil.toMajorVersion(version); - const isGreater = semver.gt(this.majorDataProtocolVersion, majorVersion); - if (isGreater) { - // TODO(minsulee2): Compatible message. - } - const isLower = semver.lt(this.majorDataProtocolVersion, majorVersion); + checkDataProtoVer(messageVersion, msgType) { + const messageMajorVersion = VersionUtil.toMajorVersion(messageVersion); + const isLower = semver.lt(messageMajorVersion, this.majorDataProtocolVersion); if (isLower) { if (FeatureFlags.enableRichP2pCommunicationLogging) { - logger.error('CANNOT deal with higher data protocol version.' + - 'Discard the CONSENSUS message.'); + logger.error(`The given ${msgType} message has unsupported DATA_PROTOCOL_VERSION: ` + + `theirs(${messageVersion}) < ours(${this.majorDataProtocolVersion})`); } - return false; + return -1; } - return true; - } - - checkDataProtoVerForTransaction(version) { - const majorVersion = VersionUtil.toMajorVersion(version); - const isGreater = semver.gt(this.majorDataProtocolVersion, majorVersion); + const isGreater = semver.gt(messageMajorVersion, this.majorDataProtocolVersion); if (isGreater) { - // TODO(minsulee2): Compatible message. - } - const isLower = semver.lt(this.majorDataProtocolVersion, majorVersion); - if (isLower) { if (FeatureFlags.enableRichP2pCommunicationLogging) { - logger.error('CANNOT deal with higher data protocol ver. Discard the TRANSACTION message.'); + logger.error('I may be running of the old DATA_PROTOCOL_VERSION ' + + `theirs(${messageVersion}) > ours(${this.majorDataProtocolVersion}). ` + + 'Please check the new release via visiting the URL below:\n' + + 'https://github.com/ainblockchain/ain-blockchain'); } - return false; + return 1; } - return true; + return 0; } setPeerEventHandlers(socket) { @@ -390,8 +366,12 @@ class P2pServer { switch (_.get(parsedMessage, 'type')) { case MessageTypes.ADDRESS_REQUEST: - // TODO(minsulee2): Add compatibility check here after data version up. - // this.checkDataProtoVerForAddressRequest(dataProtoVer); + const dataVersionCheckForAddress = + this.checkDataProtoVer(dataProtoVer, MessageTypes.ADDRESS_REQUEST); + if (dataVersionCheckForAddress < 0) { + // TODO(minsulee2): need to convert message when updating ADDRESS_REQUEST necessary. + // this.convertAddressMessage(); + } const address = _.get(parsedMessage, 'data.body.address'); if (!address) { logger.error(`Providing an address is compulsary when initiating p2p communication.`); @@ -438,12 +418,16 @@ class P2pServer { } break; case MessageTypes.CONSENSUS: + const dataVersionCheckForConsensus = + this.checkDataProtoVer(dataProtoVer, MessageTypes.CONSENSUS); + if (dataVersionCheckForConsensus !== 0) { + logger.error(`[${LOG_HEADER}] The message DATA_PROTOCOL_VERSION(${dataProtoVer}) ` + + 'is not compatible. CANNOT proceed the CONSENSUS message.'); + return; + } const consensusMessage = _.get(parsedMessage, 'data.message'); logger.debug(`[${LOG_HEADER}] Receiving a consensus message: ` + `${JSON.stringify(consensusMessage)}`); - if (!this.checkDataProtoVerForConsensus(dataProtoVer)) { - return; - } if (this.node.state === BlockchainNodeStates.SERVING) { this.consensus.handleConsensusMessage(consensusMessage); } else { @@ -451,6 +435,16 @@ class P2pServer { } break; case MessageTypes.TRANSACTION: + const dataVersionCheckForTransaction = + this.checkDataProtoVer(dataProtoVer, MessageTypes.TRANSACTION); + if (dataVersionCheckForTransaction > 0) { + logger.error(`[${LOG_HEADER}] CANNOT deal with higher data protocol ` + + `version(${dataProtoVer}). Discard the TRANSACTION message.`); + return; + } else if (dataVersionCheckForTransaction < 0) { + // TODO(minsulee2): need to convert msg when updating TRANSACTION message necessary. + // this.convertTransactionMessage(); + } const tx = _.get(parsedMessage, 'data.transaction'); logger.debug(`[${LOG_HEADER}] Receiving a transaction: ${JSON.stringify(tx)}`); if (this.node.tp.transactionTracker[tx.hash]) { @@ -463,9 +457,6 @@ class P2pServer { return; } if (Transaction.isBatchTransaction(tx)) { - if (!this.checkDataProtoVerForTransaction(dataProtoVer)) { - return; - } const newTxList = []; for (const subTx of tx.tx_list) { const createdTx = Transaction.create(subTx.tx_body, subTx.signature); @@ -490,11 +481,8 @@ class P2pServer { } break; case MessageTypes.CHAIN_SEGMENT_REQUEST: - const lastBlock = _.get(parsedMessage, 'data.lastBlock'); - // NOTE(minsulee2): Communicate with each other - // even if the data protocol is incompatible. - logger.debug(`[${LOG_HEADER}] Receiving a chain segment request: ` + - `${JSON.stringify(lastBlock, null, 2)}`); + const lastBlockNumber = _.get(parsedMessage, 'data.lastBlockNumber'); + logger.debug(`[${LOG_HEADER}] Receiving a chain segment request: ${lastBlockNumber}`); if (this.node.bc.chain.length === 0) { return; } @@ -506,8 +494,7 @@ class P2pServer { // Send a chunk of 20 blocks from your blockchain to the requester. // Requester will continue to request blockchain chunks // until their blockchain height matches the consensus blockchain height - const chainSegment = this.node.bc.requestBlockchainSection( - lastBlock ? Block.parse(lastBlock) : null); + const chainSegment = this.node.bc.getBlockList(lastBlockNumber + 1); if (chainSegment) { const catchUpInfo = this.consensus.getCatchUpInfo(); logger.debug( @@ -531,8 +518,8 @@ class P2pServer { } break; default: - logger.error(`Wrong message type(${parsedMessage.type}) has been specified.`); - logger.error('Ignore the message.'); + logger.error(`[${LOG_HEADER}] Unknown message type(${parsedMessage.type}) has been ` + + 'specified. Ignore the message.'); break; } } catch (err) { diff --git a/p2p/util.js b/p2p/util.js index 82a122eff..596078b73 100644 --- a/p2p/util.js +++ b/p2p/util.js @@ -10,7 +10,7 @@ const logger = require('../logger')('SERVER_UTIL'); const { CURRENT_PROTOCOL_VERSION, DATA_PROTOCOL_VERSION, - P2P_MESSAGE_TIMEOUT_MS + P2P_MESSAGE_TIMEOUT_MS, } = require('../common/constants'); const ChainUtil = require('../common/chain-util'); diff --git a/package.json b/package.json index b22ec4063..1f8d50f05 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ain-blockchain", "description": "AI Network Blockchain", - "version": "0.7.6", + "version": "0.7.7", "private": true, "license": "MIT", "author": "dev@ainetwork.ai", @@ -39,7 +39,6 @@ "axios": "^0.21.1", "bluebird": "^3.5.3", "diskusage": "^1.1.3", - "elliptic": "^6.4.1", "escape-string-regexp": "^2.0.0", "express": "^4.17.1", "fast-json-stable-stringify": "^2.0.0", @@ -56,11 +55,9 @@ "public-ip": "^3.2.0", "request": "^2.88.0", "rimraf": "^2.6.3", - "secp256k1": "^3.7.0", "seedrandom": "^2.4.4", "semver": "^6.3.0", "shuffle-seed": "^1.1.6", - "url": "^0.11.0", "util": "^0.11.1", "uuid": "^3.4.0", "valid-url": "^1.0.9", diff --git a/start_node_incremental_gcp.sh b/start_node_incremental_gcp.sh index 2df4d62ab..e68ca03f7 100644 --- a/start_node_incremental_gcp.sh +++ b/start_node_incremental_gcp.sh @@ -126,6 +126,7 @@ printf "Starting up Node server.." nohup node --async-stack-traces client/index.js >/dev/null 2>error_logs.txt & # 7. Wait until the new node catches up +SECONDS=0 loopCount=0 generate_post_data() @@ -142,7 +143,8 @@ do printf "\nconsensusState = ${consensusState}" printf "\nlastBlockNumber = ${lastBlockNumber}" if [ "$consensusState" == "RUNNING" ]; then - printf "\nNode is synced & running!\n\n" + printf "\nNode is synced & running!" + printf "Time it took to sync in seconds: $SECONDS\n\n" break fi ((loopCount++)) diff --git a/start_servers.sh b/start_servers.sh index 185c6b55d..e4ec5be10 100755 --- a/start_servers.sh +++ b/start_servers.sh @@ -5,7 +5,7 @@ MIN_NUM_VALIDATORS=5 ACCOUNT_INDEX=0 STAKE=100000 CONSOLE_LOG=true ENABLE_DEV_SE sleep 10 MIN_NUM_VALIDATORS=5 ACCOUNT_INDEX=1 STAKE=100000 CONSOLE_LOG=true ENABLE_DEV_SET_CLIENT_API=true ENABLE_TX_SIG_VERIF_WORKAROUND=true ENABLE_GAS_FEE_WORKAROUND=true BLOCKCHAIN_DATA_DIR=~/ain_blockchain_data node ./client/index.js & sleep 1 -MIN_NUM_VALIDATORS=5ACCOUNT_INDEX=2 STAKE=100000 CONSOLE_LOG=true ENABLE_DEV_SET_CLIENT_API=true ENABLE_TX_SIG_VERIF_WORKAROUND=true ENABLE_GAS_FEE_WORKAROUND=true BLOCKCHAIN_DATA_DIR=~/ain_blockchain_data node ./client/index.js & +MIN_NUM_VALIDATORS=5 ACCOUNT_INDEX=2 STAKE=100000 CONSOLE_LOG=true ENABLE_DEV_SET_CLIENT_API=true ENABLE_TX_SIG_VERIF_WORKAROUND=true ENABLE_GAS_FEE_WORKAROUND=true BLOCKCHAIN_DATA_DIR=~/ain_blockchain_data node ./client/index.js & sleep 10 MIN_NUM_VALIDATORS=5 ACCOUNT_INDEX=3 STAKE=100000 CONSOLE_LOG=true ENABLE_DEV_SET_CLIENT_API=true ENABLE_TX_SIG_VERIF_WORKAROUND=true ENABLE_GAS_FEE_WORKAROUND=true BLOCKCHAIN_DATA_DIR=~/ain_blockchain_data node ./client/index.js & sleep 10 diff --git a/tx-pool/transaction.js b/tx-pool/transaction.js index f05c59058..ee4817d29 100644 --- a/tx-pool/transaction.js +++ b/tx-pool/transaction.js @@ -170,6 +170,9 @@ class Transaction { if (txBody.gas_price !== undefined) { sanitized.gas_price = ChainUtil.numberOrZero(txBody.gas_price); } + if (txBody.billing !== undefined) { + sanitized.billing = ChainUtil.stringOrEmpty(txBody.billing); + } // A devel method for bypassing the transaction verification. if (txBody.address !== undefined) { sanitized.address = ChainUtil.stringOrEmpty(txBody.address); @@ -205,6 +208,11 @@ class Transaction { `Transaction body has invalid gas price: ${JSON.stringify(txBody, null, 2)}`); return false; } + if (!Transaction.isValidBilling(txBody.billing)) { + logger.info( + `Transaction body has invalid billing: ${JSON.stringify(txBody, null, 2)}`); + return false; + } return Transaction.isInStandardFormat(txBody); } @@ -222,6 +230,10 @@ class Transaction { return gasPrice > 0 || ENABLE_GAS_FEE_WORKAROUND && (gasPrice === undefined || gasPrice === 0); } + static isValidBilling(billing) { + return billing === undefined || (ChainUtil.isString(billing) && billing.split('|').length === 2); + } + static isInStandardFormat(txBody) { const sanitized = Transaction.sanitizeTxBody(txBody); const isIdentical = _.isEqual(JSON.parse(JSON.stringify(sanitized)), txBody, { strict: true }); diff --git a/unittest/blockchain.test.js b/unittest/blockchain.test.js index daf02db67..f447d9b92 100644 --- a/unittest/blockchain.test.js +++ b/unittest/blockchain.test.js @@ -103,7 +103,7 @@ describe('Blockchain', () => { it('can sync on startup', () => { while (!node1.bc.lastBlock() || !node2.bc.lastBlock() || node1.bc.lastBlock().hash !== node2.bc.lastBlock().hash) { - const blockSection = node1.bc.requestBlockchainSection(node2.bc.lastBlock()); + const blockSection = node1.bc.getBlockList(node2.bc.lastBlock().number + 1); if (blockSection) { node2.mergeChainSegment(blockSection); } @@ -112,9 +112,9 @@ describe('Blockchain', () => { }); it('can be queried by index', () => { - assert.deepEqual(JSON.stringify(node1.bc.getChainSection(10, 30)), + assert.deepEqual(JSON.stringify(node1.bc.getBlockList(10, 30)), JSON.stringify(blocks.slice(9, 29))); - assert.deepEqual(JSON.stringify(node1.bc.getChainSection(980, 1000)), + assert.deepEqual(JSON.stringify(node1.bc.getBlockList(980, 1000)), JSON.stringify(blocks.slice(979, 999))); }); diff --git a/unittest/chain-util.test.js b/unittest/chain-util.test.js index 83d3472cd..c2372a492 100644 --- a/unittest/chain-util.test.js +++ b/unittest/chain-util.test.js @@ -1131,4 +1131,95 @@ describe("ChainUtil", () => { assert.deepEqual(ChainUtil.getTotalGasCost(undefined, 1), 0); }) }) + + describe('getDependentAppNameFromRef', () => { + it("when abnormal input", () => { + assert.deepEqual(ChainUtil.getDependentAppNameFromRef(), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef(null), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef(undefined), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef(''), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/'), null); + }); + + it("when normal input (app-dependent service path)", () => { + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/manage_app/app_a'), 'app_a'); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/payments/app_a'), 'app_a'); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/staking/app_a'), 'app_a'); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/staking/app_a/some/nested/path'), 'app_a'); + }); + + it("when normal input (app-independent service path)", () => { + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/accounts/0xabcd/value'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/service_accounts/staking'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/gas_fee/gas_fee'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/escrow/source/target/id/key/value'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/sharding/config'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/transfer'), null); + assert.deepEqual(ChainUtil.getDependentAppNameFromRef('/transfer/from/to/key/value'), null); + }); + }) + + describe('getServiceDependentAppNameList', () => { + it("when abnormal input", () => { + assert.deepEqual(ChainUtil.getServiceDependentAppNameList(), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList(null), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList(undefined), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({}), []); + }); + + it("when normal input", () => { + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + ref: '/' + }), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + ref: '/transfer/from/to/key/value' + }), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + ref: '/manage_app/app_a/create/key' + }), ['app_a']); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + op_list: [ + { + ref: '/' + } + ] + }), []); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + op_list: [ + { + ref: '/transfer/from/to/key/value' + }, + { + ref: '/manage_app/app_a/create/key' + } + ] + }), ['app_a']); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + op_list: [ + { + ref: '/transfer/from/to/key/value' + }, + { + ref: '/manage_app/app_a/create/key' + }, + { + ref: '/payments/app_a/user/id/pay/key' + } + ] + }), ['app_a']); + assert.deepEqual(ChainUtil.getServiceDependentAppNameList({ + op_list: [ + { + ref: '/transfer/from/to/key/value' + }, + { + ref: '/manage_app/app_a/create/key' + }, + { + ref: '/payments/app_b/user/id/pay/key' + } + ] + }), ['app_a', 'app_b']); + }); + }) }) \ No newline at end of file diff --git a/unittest/db.test.js b/unittest/db.test.js index 1097bcb4b..9b3e4e3c7 100644 --- a/unittest/db.test.js +++ b/unittest/db.test.js @@ -1451,7 +1451,20 @@ describe("DB operations", () => { "write_rule": true, } } - }}; + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } + } + }; assert.deepEqual(node.db.setOwner( "/test/test_owner/some/path", ownerTree, { addr: '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1' }), { @@ -1804,6 +1817,18 @@ describe("DB operations", () => { "write_rule": true, } } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } } } } @@ -1873,6 +1898,18 @@ describe("DB operations", () => { "write_rule": true, } } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } } }); }) @@ -3041,10 +3078,10 @@ describe("DB sharding config", () => { ".owner": { "owners": { "*": { - "branch_owner": false, - "write_function": false, - "write_owner": false, - "write_rule": false, + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, }, "0x09A0d53FDf1c36A131938eb379b98910e55EEfe1": { "branch_owner": true, @@ -3053,6 +3090,18 @@ describe("DB sharding config", () => { "write_rule": true, } } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } } } } @@ -3544,10 +3593,10 @@ describe("DB sharding config", () => { ".owner": { "owners": { "*": { - "branch_owner": false, - "write_function": false, - "write_owner": false, - "write_rule": false, + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, }, "0x09A0d53FDf1c36A131938eb379b98910e55EEfe1": { "branch_owner": true, @@ -3556,18 +3605,49 @@ describe("DB sharding config", () => { "write_rule": true, } } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } + } + }; + const ownerChange = { + ".owner": { + "owners": { + "0x09A0d53FDf1c36A131938eb379b98910e55EEfe1": null + } } }; const newOwner = { ".owner": { "owners": { "*": { - "branch_owner": false, - "write_function": false, - "write_owner": false, - "write_rule": false, + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, }, } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } } }; @@ -3588,7 +3668,7 @@ describe("DB sharding config", () => { it("setOwner with isGlobal = false", () => { expect(node.db.setOwner( - "test/test_sharding/some/path/to", newOwner, + "test/test_sharding/some/path/to", ownerChange, { addr: '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1' }).code) .to.equal(0); assert.deepEqual(node.db.getOwner("test/test_sharding/some/path/to"), newOwner); @@ -3596,7 +3676,7 @@ describe("DB sharding config", () => { it("setOwner with isGlobal = true", () => { expect(node.db.setOwner( - "apps/afan/test/test_sharding/some/path/to", newOwner, + "apps/afan/test/test_sharding/some/path/to", ownerChange, { addr: '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1' }, true).code) .to.equal(0); assert.deepEqual( @@ -3605,7 +3685,7 @@ describe("DB sharding config", () => { it("setOwner with isGlobal = true and non-existing path", () => { expect(node.db.setOwner( - "some/non-existing/path", newOwner, + "some/non-existing/path", ownerChange, { addr: '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1' }, true).code).to.equal(0); }) @@ -3618,10 +3698,10 @@ describe("DB sharding config", () => { "config": { "owners": { "*": { - "branch_owner": false, - "write_function": false, - "write_owner": false, - "write_rule": false, + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, }, "0x09A0d53FDf1c36A131938eb379b98910e55EEfe1": { "branch_owner": true, @@ -3645,10 +3725,10 @@ describe("DB sharding config", () => { "config": { "owners": { "*": { - "branch_owner": false, - "write_function": false, - "write_owner": false, - "write_rule": false, + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, }, "0x09A0d53FDf1c36A131938eb379b98910e55EEfe1": { "branch_owner": true, diff --git a/unittest/p2p.test.js b/unittest/p2p.test.js index 97f46d085..3c63d3979 100644 --- a/unittest/p2p.test.js +++ b/unittest/p2p.test.js @@ -1,5 +1,4 @@ const chai = require('chai'); -const url = require('url'); const BlockchainNode = require('../node'); const VersionUtil = require('../common/version-util'); @@ -270,31 +269,26 @@ describe("p2p", () => { describe("getNetworkStatus", () => { it("shows initial values of connection status", () => { + const extIp = p2pClient.server.getExternalIp(); + const url = new URL(`ws://${extIp}:${P2P_PORT}`); + const p2pUrl = url.toString(); + url.protocol = 'http:'; + url.port = PORT; + const clientApiUrl = url.toString(); + url.pathname = 'json-rpc'; + const jsonRpcUrl = url.toString(); const actual = { - ip: p2pClient.server.getExternalIp(), + ip: extIp, p2p: { - url: url.format({ - protocol: 'ws', - hostname: p2pClient.server.getExternalIp(), - port: P2P_PORT - }), + url: p2pUrl, port: P2P_PORT, }, clientApi: { - url: url.format({ - protocol: 'http', - hostname: p2pClient.server.getExternalIp(), - port: PORT - }), + url: clientApiUrl, port: PORT, }, jsonRpc: { - url: url.format({ - protocol: 'http', - hostname: p2pClient.server.getExternalIp(), - port: PORT, - pathname: '/json-rpc', - }), + url: jsonRpcUrl, port: PORT, }, connectionStatus: p2pClient.getConnectionStatus() diff --git a/unittest/rule-util.test.js b/unittest/rule-util.test.js index 1f8313483..0c737e99d 100644 --- a/unittest/rule-util.test.js +++ b/unittest/rule-util.test.js @@ -352,4 +352,38 @@ describe("RuleUtil", () => { expect(util.isCksumAddr('0xCAcD898dBaEdBD9037aCd25b82417587E972838d')).to.equal(true); }) }) + + describe("isServAcntName", () => { + it("when invalid-address input", () => { + expect(util.isServAcntName(0)).to.equal(false); + expect(util.isServAcntName(10)).to.equal(false); + expect(util.isServAcntName(null)).to.equal(false); + expect(util.isServAcntName(undefined)).to.equal(false); + expect(util.isServAcntName(Infinity)).to.equal(false); + expect(util.isServAcntName(NaN)).to.equal(false); + expect(util.isServAcntName({})).to.equal(false); + expect(util.isServAcntName({a: 'a'})).to.equal(false); + expect(util.isServAcntName('')).to.equal(false); + expect(util.isServAcntName('abc')).to.equal(false); + expect(util.isServAcntName('0')).to.equal(false); + expect(util.isServAcntName([])).to.equal(false); + expect(util.isServAcntName([10])).to.equal(false); + expect(util.isServAcntName([10, 'abc'])).to.equal(false); + expect(util.isServAcntName('staking')).to.equal(false); + expect(util.isServAcntName('staking|consensus')).to.equal(false); + expect(util.isServAcntName( + 'invalid_service_type|consensus|0x09A0d53FDf1c36A131938eb379b98910e55EEfe1|0')) + .to.equal(false); // invalid service account service type + expect(util.isServAcntName( + 'staking|0invalid_service_name|0x09A0d53FDf1c36A131938eb379b98910e55EEfe1|0')) + .to.equal(false); // invalid service account service name + }) + + it("when valid-address input", () => { + expect(util.isServAcntName( + 'staking|consensus|0x09A0d53FDf1c36A131938eb379b98910e55EEfe1')).to.equal(true); + expect(util.isServAcntName( + 'staking|consensus|0x09A0d53FDf1c36A131938eb379b98910e55EEfe1|0')).to.equal(true); + }) + }) }) \ No newline at end of file diff --git a/unittest/state-util.test.js b/unittest/state-util.test.js index 9bccdc91a..99f4b5907 100644 --- a/unittest/state-util.test.js +++ b/unittest/state-util.test.js @@ -3,6 +3,7 @@ const { isWritablePathWithSharding, hasReservedChar, hasAllowedPattern, + isValidServiceName, isValidStateLabel, isValidPathForStates, isValidJsObjectForStates, @@ -13,6 +14,7 @@ const { isValidOwnerConfig, isValidOwnerTree, applyFunctionChange, + applyOwnerChange, setStateTreeVersion, renameStateTreeVersion, deleteStateTree, @@ -23,6 +25,9 @@ const { updateProofHashForAllRootPaths, verifyProofHashForStateTree } = require('../db/state-util'); +const { + STATE_LABEL_LENGTH_LIMIT, +} = require('../common/constants'); const StateNode = require('../db/state-node'); const chai = require('chai'); const expect = chai.expect; @@ -295,6 +300,69 @@ describe("state-util", () => { }) }) + describe("isValidServiceName", () => { + it("when non-string input", () => { + expect(isValidServiceName(null)).to.equal(false); + expect(isValidServiceName(undefined)).to.equal(false); + expect(isValidServiceName(true)).to.equal(false); + expect(isValidServiceName(false)).to.equal(false); + expect(isValidServiceName(0)).to.equal(false); + expect(isValidServiceName([])).to.equal(false); + expect(isValidServiceName({})).to.equal(false); + }) + + it("when string input returning false", () => { + expect(isValidServiceName('')).to.equal(false); + expect(isValidServiceName('.')).to.equal(false); + expect(isValidServiceName('.a')).to.equal(false); + expect(isValidServiceName('$')).to.equal(false); + expect(isValidServiceName('$a')).to.equal(false); + expect(isValidServiceName('*')).to.equal(false); + expect(isValidServiceName('~')).to.equal(false); + expect(isValidServiceName('!')).to.equal(false); + expect(isValidServiceName('@')).to.equal(false); + expect(isValidServiceName('%')).to.equal(false); + expect(isValidServiceName('^')).to.equal(false); + expect(isValidServiceName('&')).to.equal(false); + expect(isValidServiceName('-')).to.equal(false); + expect(isValidServiceName('=')).to.equal(false); + expect(isValidServiceName('+')).to.equal(false); + expect(isValidServiceName('|')).to.equal(false); + expect(isValidServiceName(';')).to.equal(false); + expect(isValidServiceName(',')).to.equal(false); + expect(isValidServiceName('?')).to.equal(false); + expect(isValidServiceName('/')).to.equal(false); + expect(isValidServiceName("'")).to.equal(false); + expect(isValidServiceName('"')).to.equal(false); + expect(isValidServiceName('`')).to.equal(false); + expect(isValidServiceName('\x00')).to.equal(false); + expect(isValidServiceName('\x7F')).to.equal(false); + }) + + it("when string input without alphabetic prefix returning false", () => { + expect(isValidServiceName('0')).to.equal(false); + expect(isValidServiceName('0a')).to.equal(false); + expect(isValidServiceName('0a0')).to.equal(false); + expect(isValidServiceName('0_')).to.equal(false); + expect(isValidServiceName('0_0')).to.equal(false); + }) + + it("when string input returning true", () => { + expect(isValidServiceName('a')).to.equal(true); + expect(isValidServiceName('aa')).to.equal(true); + expect(isValidServiceName('a_')).to.equal(true); + expect(isValidServiceName('a0')).to.equal(true); + expect(isValidServiceName('a0a')).to.equal(true); + expect(isValidServiceName('_')).to.equal(true); + expect(isValidServiceName('_0')).to.equal(true); + expect(isValidServiceName('_0_')).to.equal(true); + expect(isValidServiceName('consensus')).to.equal(true); + expect(isValidServiceName('afan')).to.equal(true); + expect(isValidServiceName('collaborative_ai')).to.equal(true); + expect(isValidServiceName('_a_dapp')).to.equal(true); + }) + }) + describe("isValidStateLabel", () => { it("when non-string input", () => { expect(isValidStateLabel(null)).to.equal(false); @@ -318,6 +386,7 @@ describe("state-util", () => { it("when string input returning true", () => { expect(isValidStateLabel('a')).to.equal(true); + expect(isValidStateLabel('0')).to.equal(true); expect(isValidStateLabel('.a')).to.equal(true); expect(isValidStateLabel('$a')).to.equal(true); expect(isValidStateLabel('*')).to.equal(true); @@ -336,6 +405,13 @@ describe("state-util", () => { expect(isValidStateLabel(',')).to.equal(true); expect(isValidStateLabel('?')).to.equal(true); }) + + it("when long string input", () => { + const labelLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT); + expect(isValidStateLabel(labelLong)).to.equal(true); + const labelTooLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT + 1); + expect(isValidStateLabel(labelTooLong)).to.equal(false); + }) }) describe("isValidPathForStates", () => { @@ -381,6 +457,19 @@ describe("state-util", () => { assert.deepEqual(isValidPathForStates(['a', '$b']), {isValid: true, invalidPath: ''}); assert.deepEqual(isValidPathForStates(['a', '*']), {isValid: true, invalidPath: ''}); }) + + it("when input with long labels", () => { + const labelLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT); + const labelTooLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT + 1); + assert.deepEqual( + isValidPathForStates([labelLong, labelLong]), {isValid: true, invalidPath: ''}); + assert.deepEqual( + isValidPathForStates([labelTooLong, labelLong]), + {isValid: false, invalidPath: `/${labelTooLong}`}); + assert.deepEqual( + isValidPathForStates([labelLong, labelTooLong]), + {isValid: false, invalidPath: `/${labelLong}/${labelTooLong}`}); + }) }) describe("isValidJsObjectForStates", () => { @@ -527,6 +616,29 @@ describe("state-util", () => { } }), {isValid: true, invalidPath: ''}); }) + + it("when input with long labels", () => { + const textLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT); + const textTooLong = 'a'.repeat(STATE_LABEL_LENGTH_LIMIT + 1); + assert.deepEqual( + isValidJsObjectForStates({ + [textLong]: { + [textLong]: textTooLong + } + }), {isValid: true, invalidPath: ''}); + assert.deepEqual( + isValidJsObjectForStates({ + [textTooLong]: { + [textLong]: textTooLong + } + }), {isValid: false, invalidPath: `/${textTooLong}`}); + assert.deepEqual( + isValidJsObjectForStates({ + [textLong]: { + [textTooLong]: textTooLong + } + }), {isValid: false, invalidPath: `/${textLong}/${textTooLong}`}); + }) }) describe("isValidRuleConfig", () => { @@ -1136,7 +1248,20 @@ describe("state-util", () => { "write_function": false, "write_owner": false, "write_rule": false, - } + }, + '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + 'fid:_createApp': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + '0x08Aed7AF9354435c38d52143EE50ac839D20696b': null } } }), {isValid: true, invalidPath: ''}); @@ -1149,7 +1274,20 @@ describe("state-util", () => { "write_function": false, "write_owner": false, "write_rule": false, - } + }, + '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + 'fid:_createApp': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + '0x08Aed7AF9354435c38d52143EE50ac839D20696b': null } } }, @@ -1161,7 +1299,20 @@ describe("state-util", () => { "write_function": false, "write_owner": false, "write_rule": false, - } + }, + '0x09A0d53FDf1c36A131938eb379b98910e55EEfe1': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + 'fid:_createApp': { + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + '0x08Aed7AF9354435c38d52143EE50ac839D20696b': null } } } @@ -1173,17 +1324,25 @@ describe("state-util", () => { const curFunction = { ".function": { "0x111": { - "function_type": "NATIVE", + "function_type": "REST", "function_id": "0x111" }, "0x222": { - "function_type": "NATIVE", + "function_type": "REST", "function_id": "0x222" }, "0x333": { - "function_type": "NATIVE", + "function_type": "REST", "function_id": "0x333" } + }, + "deeper": { + ".function": { // deeper function + "0x999": { + "function_type": "REST", + "function_id": "0x999" + } + } } }; @@ -1198,9 +1357,9 @@ describe("state-util", () => { }, "deeper": { ".function": { // deeper function - "0x999": { + "0x888": { "function_type": "REST", - "function_id": "0x999" + "function_id": "0x888" } } } @@ -1214,9 +1373,9 @@ describe("state-util", () => { }, "deeper": { ".function": { - "0x999": { + "0x888": { "function_type": "REST", - "function_id": "0x999" + "function_id": "0x888" } } } @@ -1229,7 +1388,8 @@ describe("state-util", () => { "0x111": null, // delete "0x222": { // modify "function_type": "REST", - "function_id": "0x222" + "function_id": "0x222", + "service_name": "https://ainetwork.ai", }, "0x444": { // add "function_type": "REST", @@ -1240,27 +1400,36 @@ describe("state-util", () => { ".function": { "0x222": { // modified "function_type": "REST", - "function_id": "0x222" + "function_id": "0x222", + "service_name": "https://ainetwork.ai", }, "0x333": { // untouched - "function_type": "NATIVE", + "function_type": "REST", "function_id": "0x333" }, "0x444": { // added "function_type": "REST", "function_id": "0x444" } + }, + "deeper": { + ".function": { // deeper function + "0x999": { + "function_type": "REST", + "function_id": "0x999" + } + } } }); }); - it("add / delete / modify existing function with deeper function", () => { + it("replace existing function with deeper function", () => { assert.deepEqual(applyFunctionChange(curFunction, { ".function": { - "0x111": null, // delete "0x222": { // modify "function_type": "REST", - "function_id": "0x222" + "function_id": "0x222", + "service_name": "https://ainetwork.ai", }, "0x444": { // add "function_type": "REST", @@ -1269,26 +1438,31 @@ describe("state-util", () => { }, "deeper": { ".function": { // deeper function - "0x999": { + "0x888": { "function_type": "REST", - "function_id": "0x999" + "function_id": "0x888" } } } }), { - ".function": { // deeper function has no effect - "0x222": { // modified + ".function": { // replaced + "0x222": { "function_type": "REST", - "function_id": "0x222" - }, - "0x333": { // untouched - "function_type": "NATIVE", - "function_id": "0x333" + "function_id": "0x222", + "service_name": "https://ainetwork.ai", }, - "0x444": { // added + "0x444": { "function_type": "REST", "function_id": "0x444" } + }, + "deeper": { // replaced + ".function": { + "0x888": { + "function_type": "REST", + "function_id": "0x888" + } + } } }); }); @@ -1298,6 +1472,218 @@ describe("state-util", () => { }); }); + describe("applyOwnerChange()", () => { + const curOwner = { + ".owner": { + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + "aaaa": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + "bbbb": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } + } + }; + + it("add / delete / modify non-existing owner", () => { + assert.deepEqual(applyOwnerChange(null, { + ".owner": { // owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } + } + }), { // the same as the given owner change. + ".owner": { // owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } + } + }); + }); + + it("add / delete / modify existing owner", () => { + assert.deepEqual(applyOwnerChange(curOwner, { + ".owner": { + "owners": { + "*": { // modify + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + "aaaa": null, // delete + "cccc": { // add + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } + }), { + ".owner": { + "owners": { + "*": { // modified + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + "bbbb": { // untouched + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + "cccc": { // added + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "*": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + }, + } + } + } + }); + }); + + it("replace existing owner with deeper owner", () => { + assert.deepEqual(applyOwnerChange(curOwner, { + ".owner": { + "owners": { + "*": { // modify + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + "cccc": { // add + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + }, + "deeper": { + ".owner": { // deeper owner + "owners": { + "CCCC": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } + } + }), { + ".owner": { // replaced + "owners": { + "*": { // modify + "branch_owner": true, + "write_function": false, + "write_owner": false, + "write_rule": false, + }, + "cccc": { // add + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + }, + "deeper": { // replaced + ".owner": { + "owners": { + "CCCC": { + "branch_owner": true, + "write_function": true, + "write_owner": true, + "write_rule": true, + } + } + } + } + }); + }); + + it("with null owner change", () => { + assert.deepEqual(applyOwnerChange(curOwner, null), null); + }); + }); + describe("setStateTreeVersion", () => { it("leaf node", () => { const ver1 = 'ver1'; diff --git a/unittest/transaction.test.js b/unittest/transaction.test.js index b5c849ee0..d3a8d2ed1 100644 --- a/unittest/transaction.test.js +++ b/unittest/transaction.test.js @@ -19,6 +19,8 @@ describe('Transaction', () => { let txCustomAddress; let txBodyParentHash; let txParentHash; + let txBodyBilling; + let txBilling; let txBodyForNode; let txForNode; @@ -66,6 +68,19 @@ describe('Transaction', () => { }; txParentHash = Transaction.fromTxBody(txBodyParentHash, node.account.private_key); + txBodyBilling = { + operation: { + type: 'SET_VALUE', + ref: '/apps/app_a/path', + value: 'val', + }, + timestampe: 1568798344000, + nonce: 10, + gas_price: 1, + billing: 'app_a|0' + }; + txBilling = Transaction.fromTxBody(txBodyBilling, node.account.private_key); + txBodyForNode = { operation: { type: 'SET_VALUE', @@ -151,6 +166,22 @@ describe('Transaction', () => { let tx3 = Transaction.fromTxBody(txBody, node.account.private_key); assert.deepEqual(tx3, null); }); + + it('succeed with absent billing', () => { + delete txBody.billing; + const tx2 = Transaction.fromTxBody(txBody, node.account.private_key); + expect(tx2).to.not.equal(null); + }); + + it('fail with invalid billing', () => { + txBody.billing = 'app_a'; + const tx2 = Transaction.fromTxBody(txBody, node.account.private_key); + assert.deepEqual(tx2, null); + + txBody.billing = 'app_a|0|1'; + const tx3 = Transaction.fromTxBody(txBody, node.account.private_key); + assert.deepEqual(tx3, null); + }); }); describe('isExecutable / toExecutable / toJsObject', () => { @@ -277,5 +308,10 @@ describe('Transaction', () => { txParentHash.tx_body.parent_tx_hash = ''; expect(Transaction.verifyTransaction(txParentHash)).to.equal(false); }); + + it('fail to verify an invalid transaction with altered billing', () => { + txParentHash.tx_body.billing = 'app_b|0'; + expect(Transaction.verifyTransaction(txParentHash)).to.equal(false); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 99ae718eb..f0e615c8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2202,9 +2202,9 @@ getpass@^0.1.1: assert-plus "^1.0.0" glob-parent@^5.0.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" @@ -3276,9 +3276,9 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== ntp-client@^0.5.3: version "0.5.3" @@ -3707,11 +3707,6 @@ pumpify@^2.0.1: inherits "^2.0.3" pump "^3.0.0" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -3737,11 +3732,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4016,7 +4006,7 @@ secp256k1@3.7.1: nan "^2.14.0" safe-buffer "^5.1.2" -secp256k1@^3.6.2, secp256k1@^3.7.0: +secp256k1@^3.6.2: version "3.8.0" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d" integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw== @@ -4653,14 +4643,6 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"