From 643646d620fc53b5a9bdfa51d483650b94a1cd2f Mon Sep 17 00:00:00 2001 From: shuse2 Date: Wed, 18 May 2022 12:00:37 +0200 Subject: [PATCH 1/4] :seedling: Add ABI handler, ABI server and ABI client --- framework/src/abi_handler/abi_client.ts | 334 ++++++++++ framework/src/abi_handler/abi_handler.ts | 587 +++++++++++++++++ framework/src/abi_handler/abi_server.ts | 223 +++++++ framework/src/constants.ts | 9 + framework/src/node/consensus/consensus.ts | 4 + framework/src/node/generator/generator.ts | 4 + .../src/node/state_machine/block_context.ts | 45 ++ .../node/state_machine/generator_context.ts | 71 ++ .../state_machine/genesis_block_context.ts | 45 +- .../src/node/state_machine/state_machine.ts | 17 + framework/src/node/state_machine/types.ts | 35 + .../src/schema/application_config_schema.ts | 6 +- framework/src/testing/create_contexts.ts | 5 + .../__snapshots__/application.spec.ts.snap | 1 + .../__snapshots__/abi_handler.spec.ts.snap | 62 ++ .../test/unit/abi_handler/abi_client.spec.ts | 68 ++ .../test/unit/abi_handler/abi_handler.spec.ts | 619 ++++++++++++++++++ .../test/unit/abi_handler/abi_server.spec.ts | 64 ++ framework/test/unit/application.spec.ts | 2 +- .../node/state_machine/state_machine.spec.ts | 33 + .../application_config_schema.spec.ts.snap | 12 +- 21 files changed, 2235 insertions(+), 11 deletions(-) create mode 100644 framework/src/abi_handler/abi_client.ts create mode 100644 framework/src/abi_handler/abi_handler.ts create mode 100644 framework/src/abi_handler/abi_server.ts create mode 100644 framework/src/node/state_machine/generator_context.ts create mode 100644 framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap create mode 100644 framework/test/unit/abi_handler/abi_client.spec.ts create mode 100644 framework/test/unit/abi_handler/abi_handler.spec.ts create mode 100644 framework/test/unit/abi_handler/abi_server.spec.ts diff --git a/framework/src/abi_handler/abi_client.ts b/framework/src/abi_handler/abi_client.ts new file mode 100644 index 00000000000..a9e01a002ea --- /dev/null +++ b/framework/src/abi_handler/abi_client.ts @@ -0,0 +1,334 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec, Schema } from '@liskhq/lisk-codec'; +import { Dealer } from 'zeromq'; +import { + ABI, + IPCResponse, + InitRequest, + InitResponse, + InitStateMachineRequest, + InitStateMachineResponse, + InitGenesisStateRequest, + InitGenesisStateResponse, + InsertAssetsRequest, + InsertAssetsResponse, + VerifyAssetsRequest, + VerifyAssetsResponse, + BeforeTransactionsExecuteRequest, + BeforeTransactionsExecuteResponse, + AfterTransactionsExecuteRequest, + AfterTransactionsExecuteResponse, + VerifyTransactionRequest, + VerifyTransactionResponse, + ExecuteTransactionRequest, + ExecuteTransactionResponse, + CommitRequest, + CommitResponse, + RevertRequest, + RevertResponse, + ClearRequest, + ClearResponse, + FinalizeRequest, + FinalizeResponse, + MetadataRequest, + MetadataResponse, + QueryRequest, + QueryResponse, + ProveRequest, + ProveResponse, + afterTransactionsExecuteRequestSchema, + afterTransactionsExecuteResponseSchema, + beforeTransactionsExecuteRequestSchema, + beforeTransactionsExecuteResponseSchema, + clearRequestSchema, + clearResponseSchema, + commitRequestSchema, + commitResponseSchema, + executeTransactionRequestSchema, + executeTransactionResponseSchema, + finalizeRequestSchema, + finalizeResponseSchema, + initGenesisStateRequestSchema, + initGenesisStateResponseSchema, + initRequestSchema, + initResponseSchema, + initStateMachineRequestSchema, + initStateMachineResponseSchema, + insertAssetsRequestSchema, + insertAssetsResponseSchema, + ipcRequestSchema, + ipcResponseSchema, + metadataRequestSchema, + metadataResponseSchema, + proveRequestSchema, + proveResponseSchema, + queryRequestSchema, + queryResponseSchema, + revertRequestSchema, + revertResponseSchema, + verifyAssetsRequestSchema, + verifyAssetsResponseSchema, + verifyTransactionRequestSchema, + verifyTransactionResponseSchema, +} from '../abi'; +import { Logger } from '../logger'; + +const DEFAULT_TIMEOUT = 500; + +interface Defer { + promise: Promise; + resolve: (result: T) => void; + reject: (error?: Error) => void; +} + +const defer = (): Defer => { + let resolve!: (res: T) => void; + let reject!: (error?: Error) => void; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return { promise, resolve, reject }; +}; + +const timeout = async (ms: number, message?: string): Promise => + new Promise((_, reject) => { + const id = setTimeout(() => { + clearTimeout(id); + reject(new Error(message ?? `Timed out in ${ms}ms.`)); + }, ms); + }); + +export class ABIClient implements ABI { + private readonly _socketPath: string; + private readonly _dealer: Dealer; + private readonly _logger: Logger; + + private _pendingRequests: { + [key: string]: Defer; + } = {}; + private _globalID = BigInt(0); + + public constructor(logger: Logger, socketPath: string) { + this._logger = logger; + this._socketPath = socketPath; + this._dealer = new Dealer(); + } + + public async start(): Promise { + await new Promise((resolve, reject) => { + const connectionTimeout = setTimeout(() => { + reject( + new Error('IPC Socket client connection timeout. Please check if IPC server is running.'), + ); + }, DEFAULT_TIMEOUT); + this._dealer.events.on('connect', () => { + clearTimeout(connectionTimeout); + resolve(undefined); + }); + this._dealer.events.on('bind:error', reject); + + this._dealer.connect(this._socketPath); + }); + this._listenToRPCResponse().catch(err => { + this._logger.debug({ err: err as Error }, 'Failed to listen to the ABI response'); + }); + } + + public stop(): void { + this._dealer.disconnect(this._socketPath); + this._pendingRequests = {}; + this._globalID = BigInt(0); + } + + public async init(req: InitRequest): Promise { + return this._call('init', req, initRequestSchema, initResponseSchema); + } + + public async initStateMachine(req: InitStateMachineRequest): Promise { + return this._call( + 'initStateMachine', + req, + initStateMachineRequestSchema, + initStateMachineResponseSchema, + ); + } + + public async initGenesisState(req: InitGenesisStateRequest): Promise { + return this._call( + 'initGenesisState', + req, + initGenesisStateRequestSchema, + initGenesisStateResponseSchema, + ); + } + + public async insertAssets(req: InsertAssetsRequest): Promise { + return this._call( + 'insertAssets', + req, + insertAssetsRequestSchema, + insertAssetsResponseSchema, + ); + } + + public async verifyAssets(req: VerifyAssetsRequest): Promise { + return this._call( + 'verifyAssets', + req, + verifyAssetsRequestSchema, + verifyAssetsResponseSchema, + ); + } + + public async beforeTransactionsExecute( + req: BeforeTransactionsExecuteRequest, + ): Promise { + return this._call( + 'beforeTransactionsExecute', + req, + beforeTransactionsExecuteRequestSchema, + beforeTransactionsExecuteResponseSchema, + ); + } + + public async afterTransactionsExecute( + req: AfterTransactionsExecuteRequest, + ): Promise { + return this._call( + 'afterTransactionsExecute', + req, + afterTransactionsExecuteRequestSchema, + afterTransactionsExecuteResponseSchema, + ); + } + + public async verifyTransaction( + req: VerifyTransactionRequest, + ): Promise { + return this._call( + 'verifyTransaction', + req, + verifyTransactionRequestSchema, + verifyTransactionResponseSchema, + ); + } + + public async executeTransaction( + req: ExecuteTransactionRequest, + ): Promise { + return this._call( + 'executeTransaction', + req, + executeTransactionRequestSchema, + executeTransactionResponseSchema, + ); + } + + public async commit(req: CommitRequest): Promise { + return this._call('commit', req, commitRequestSchema, commitResponseSchema); + } + + public async revert(req: RevertRequest): Promise { + return this._call('revert', req, revertRequestSchema, revertResponseSchema); + } + + public async clear(req: ClearRequest): Promise { + return this._call('clear', req, clearRequestSchema, clearResponseSchema); + } + + public async finalize(req: FinalizeRequest): Promise { + return this._call( + 'finalize', + req, + finalizeRequestSchema, + finalizeResponseSchema, + ); + } + + public async getMetadata(req: MetadataRequest): Promise { + return this._call( + 'getMetadata', + req, + metadataRequestSchema, + metadataResponseSchema, + ); + } + + public async query(req: QueryRequest): Promise { + return this._call('query', req, queryRequestSchema, queryResponseSchema); + } + + public async prove(req: ProveRequest): Promise { + return this._call('prove', req, proveRequestSchema, proveResponseSchema); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + private async _call( + method: string, + req: object, + requestSchema: Schema, + respSchema: Schema, + ): Promise { + const params = codec.encode(requestSchema, req); + const requestBody = { + id: this._globalID, + method, + params, + }; + const encodedRequest = codec.encode(ipcRequestSchema, requestBody); + await this._dealer.send([encodedRequest]); + const response = defer(); + this._pendingRequests[this._globalID.toString()] = response as Defer; + + const resp = await Promise.race([ + response.promise, + timeout(DEFAULT_TIMEOUT, `Response not received in ${DEFAULT_TIMEOUT}ms`), + ]); + const decodedResp = codec.decode(respSchema, resp); + + if (this._globalID >= BigInt(2) ** BigInt(64)) { + this._globalID = BigInt(0); + } + return decodedResp; + } + + private async _listenToRPCResponse() { + for await (const [message] of this._dealer) { + let response: IPCResponse; + try { + response = codec.decode(ipcResponseSchema, message); + } catch (error) { + this._logger.debug({ err: error as Error }, 'Failed to decode ABI response'); + continue; + } + const defered = this._pendingRequests[response.id.toString()]; + if (!defered) { + continue; + } + + if (!response.success) { + defered.reject(new Error(response.error.message)); + } else { + defered.resolve(response.result); + } + + delete this._pendingRequests[response.id.toString()]; + } + } +} diff --git a/framework/src/abi_handler/abi_handler.ts b/framework/src/abi_handler/abi_handler.ts new file mode 100644 index 00000000000..4d97a279741 --- /dev/null +++ b/framework/src/abi_handler/abi_handler.ts @@ -0,0 +1,587 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ +import * as path from 'path'; +import { + Block, + BlockAssets, + BlockHeader, + concatDBKeys, + DB_KEY_DIFF_STATE, + SMTStore, + stateDiffSchema, + StateStore, + Transaction, +} from '@liskhq/lisk-chain'; +import { formatInt, KVStore } from '@liskhq/lisk-db'; +import { + DEFAULT_EXPIRY_TIME, + DEFAULT_MAX_TRANSACTIONS, + DEFAULT_MAX_TRANSACTIONS_PER_ACCOUNT, + DEFAULT_MINIMUM_REPLACEMENT_FEE_DIFFERENCE, + DEFAULT_MIN_ENTRANCE_FEE_PRIORITY, +} from '@liskhq/lisk-transaction-pool'; +import { codec } from '@liskhq/lisk-codec'; +import { hash } from '@liskhq/lisk-cryptography'; +import { SparseMerkleTree } from '@liskhq/lisk-tree'; +import { + ABI, + blockHeaderSchema, + InitRequest, + InitResponse, + InitStateMachineRequest, + InitStateMachineResponse, + InitGenesisStateRequest, + InitGenesisStateResponse, + InsertAssetsRequest, + InsertAssetsResponse, + VerifyAssetsRequest, + VerifyAssetsResponse, + BeforeTransactionsExecuteRequest, + BeforeTransactionsExecuteResponse, + AfterTransactionsExecuteRequest, + AfterTransactionsExecuteResponse, + VerifyTransactionRequest, + VerifyTransactionResponse, + ExecuteTransactionRequest, + ExecuteTransactionResponse, + CommitRequest, + CommitResponse, + RevertRequest, + RevertResponse, + ClearRequest, + ClearResponse, + FinalizeRequest, + FinalizeResponse, + MetadataRequest, + MetadataResponse, + QueryRequest, + QueryResponse, + ProveRequest, + ProveResponse, +} from '../abi'; +import { Logger } from '../logger'; +import { BaseModule } from '../modules'; +import { + BlockContext, + EventQueue, + GenesisBlockContext, + StateMachine, + TransactionContext, +} from '../node/state_machine'; +import { ApplicationConfig } from '../types'; +import { + DEFAULT_HOST, + DEFAULT_MAX_INBOUND_CONNECTIONS, + DEFAULT_MAX_OUTBOUND_CONNECTIONS, + DEFAULT_PORT_P2P, + DEFAULT_PORT_RPC, + MAX_BLOCK_CACHE, +} from '../constants'; +import { GenerationContext } from '../node/state_machine/generator_context'; +import { BaseChannel } from '../controller/channels'; +import { systemDirs } from '../system_dirs'; + +export interface ABIHandlerConstructor { + config: ApplicationConfig; + logger: Logger; + stateMachine: StateMachine; + genesisBlock: Block; + stateDB: KVStore; + moduleDB: KVStore; + modules: BaseModule[]; + channel: BaseChannel; +} + +interface ExecutionContext { + id: Buffer; + networkIdentifier: Buffer; + header: BlockHeader; + stateStore: StateStore; + moduleStore: StateStore; +} + +export class ABIHandler implements ABI { + private readonly _config: ApplicationConfig; + private readonly _logger: Logger; + private readonly _stateMachine: StateMachine; + private readonly _stateDB: KVStore; + private readonly _moduleDB: KVStore; + private readonly _modules: BaseModule[]; + private readonly _channel: BaseChannel; + + private _genesisBlock?: Block; + private _executionContext: ExecutionContext | undefined; + + public constructor(args: ABIHandlerConstructor) { + this._config = args.config; + this._logger = args.logger; + this._genesisBlock = args.genesisBlock; + this._stateMachine = args.stateMachine; + this._stateDB = args.stateDB; + this._moduleDB = args.moduleDB; + this._modules = args.modules; + this._channel = args.channel; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async init(_req: InitRequest): Promise { + const { dataPath } = systemDirs(this._config.label, this._config.rootPath); + if (!this._genesisBlock) { + throw new Error('Genesis block must exist at initialization'); + } + return { + genesisBlock: { + header: { + ...this._genesisBlock.header.toObject(), + }, + // TODO: Replace after updating the block header + // header: this._genesisBlock.header.toObject(), + assets: this._genesisBlock.assets.getAll(), + transactions: [], + }, + registeredModules: this._modules.map(mod => ({ + moduleID: mod.id, + commandIDs: mod.commands.map(command => command.id), + })), + config: { + logger: this._config.logger, + system: { + dataPath, + maxBlockCache: MAX_BLOCK_CACHE, + networkVersion: this._config.networkVersion, + }, + genesis: { + bftBatchSize: this._config.genesis.blockTime, + blockTime: this._config.genesis.blockTime, + communityIdentifier: this._config.genesis.communityIdentifier, + maxFeePerByte: this._config.genesis.minFeePerByte, + maxTransactionsSize: this._config.genesis.maxTransactionsSize, + }, + generator: { + force: this._config.generation.force ?? false, + password: this._config.generation.defaultPassword ?? '', + keys: this._config.generation.generators.map(gen => ({ + address: Buffer.from(gen.address, 'hex'), + encryptedPassphrase: gen.encryptedPassphrase, + })), + }, + txpool: { + maxTransactions: this._config.transactionPool.maxTransactions ?? DEFAULT_MAX_TRANSACTIONS, + maxTransactionsPerAccount: + this._config.transactionPool.maxTransactions ?? DEFAULT_MAX_TRANSACTIONS_PER_ACCOUNT, + minEntranceFeePriority: this._config.transactionPool.minEntranceFeePriority + ? BigInt(this._config.transactionPool.minEntranceFeePriority) + : DEFAULT_MIN_ENTRANCE_FEE_PRIORITY, + minReplacementFeeDifference: this._config.transactionPool.minReplacementFeeDifference + ? BigInt(this._config.transactionPool.minReplacementFeeDifference) + : DEFAULT_MINIMUM_REPLACEMENT_FEE_DIFFERENCE, + transactionExpiryTime: + this._config.transactionPool.transactionExpiryTime ?? DEFAULT_EXPIRY_TIME, + }, + network: { + ...this._config.network, + port: this._config.network.port ?? DEFAULT_PORT_P2P, + blackListedIPs: this._config.network.blacklistedIPs ?? [], + advertiseAddress: this._config.network.advertiseAddress ?? false, + fixedPeers: this._config.network.fixedPeers ?? [], + seedPeers: this._config.network.seedPeers, + hostIP: this._config.network.hostIp ?? DEFAULT_HOST, + maxInboundConnections: + this._config.network.maxInboundConnections ?? DEFAULT_MAX_INBOUND_CONNECTIONS, + maxOutboundConnections: + this._config.network.maxOutboundConnections ?? DEFAULT_MAX_OUTBOUND_CONNECTIONS, + whitelistedPeers: this._config.network.whitelistedPeers ?? [], + }, + rpc: { + ...this._config.rpc, + modes: this._config.rpc.modes, + ipc: { + path: this._config.rpc.ipc?.path ?? path.join(dataPath, 'socket', 'ipc'), + }, + http: { + host: this._config.rpc.http?.host ?? DEFAULT_HOST, + port: this._config.rpc.http?.port ?? DEFAULT_PORT_RPC, + }, + ws: { + host: this._config.rpc.http?.host ?? DEFAULT_HOST, + port: this._config.rpc.http?.port ?? DEFAULT_PORT_RPC, + }, + }, + }, + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async initStateMachine(req: InitStateMachineRequest): Promise { + if (this._executionContext !== undefined) { + throw new Error( + `Execution context is already initialized with ${this._executionContext.id.toString( + 'hex', + )}`, + ); + } + const id = hash(codec.encode(blockHeaderSchema, req.header)); + this._executionContext = { + id, + header: new BlockHeader(req.header), + networkIdentifier: req.networkIdentifier, + stateStore: new StateStore(this._stateDB), + moduleStore: new StateStore(this._moduleDB), + }; + return { + contextID: id, + }; + } + + public async initGenesisState(req: InitGenesisStateRequest): Promise { + if (!this._genesisBlock) { + throw new Error('Genesis block must exist at initialization'); + } + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const context = new GenesisBlockContext({ + eventQueue: new EventQueue(), + header: this._executionContext.header, + logger: this._logger, + stateStore: this._executionContext.stateStore, + assets: this._genesisBlock.assets, + }); + + await this._stateMachine.executeGenesisBlock(context); + return { + assets: this._genesisBlock.assets.getAll(), + events: context.eventQueue.getEvents().map(e => e.toObject()), + certificateThreshold: context.nextValidators.certificateThreshold, + nextValidators: context.nextValidators.validators, + preCommitThreshold: context.nextValidators.precommitThreshold, + }; + } + + public async insertAssets(req: InsertAssetsRequest): Promise { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const context = new GenerationContext({ + header: this._executionContext.header, + logger: this._logger, + stateStore: this._executionContext.stateStore, + networkIdentifier: this._executionContext.networkIdentifier, + generatorStore: this._executionContext.moduleStore, + finalizedHeight: req.finalizedHeight, + }); + await this._stateMachine.insertAssets(context); + return { + assets: context.assets.getAll(), + }; + } + + public async verifyAssets(req: VerifyAssetsRequest): Promise { + // Remove genesis block from memory + this._genesisBlock = undefined; + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const context = new BlockContext({ + header: this._executionContext.header, + logger: this._logger, + stateStore: this._executionContext.stateStore, + networkIdentifier: this._executionContext.networkIdentifier, + assets: new BlockAssets(req.assets), + eventQueue: new EventQueue(), + // verifyAssets does not have access to transactions + transactions: [], + // verifyAssets does not have access to those properties + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, + }); + await this._stateMachine.verifyAssets(context); + + return {}; + } + + public async beforeTransactionsExecute( + req: BeforeTransactionsExecuteRequest, + ): Promise { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const context = new BlockContext({ + header: this._executionContext.header, + logger: this._logger, + stateStore: this._executionContext.stateStore, + networkIdentifier: this._executionContext.networkIdentifier, + assets: new BlockAssets(req.assets), + eventQueue: new EventQueue(), + currentValidators: req.consensus.currentValidators, + impliesMaxPrevote: req.consensus.implyMaxPrevote, + maxHeightCertified: req.consensus.maxHeightCertified, + transactions: [], + }); + await this._stateMachine.beforeExecuteBlock(context); + + return { + events: context.eventQueue.getEvents().map(e => e.toObject()), + }; + } + + public async afterTransactionsExecute( + req: AfterTransactionsExecuteRequest, + ): Promise { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + + const context = new BlockContext({ + header: this._executionContext.header, + logger: this._logger, + stateStore: this._executionContext.stateStore, + networkIdentifier: this._executionContext.networkIdentifier, + assets: new BlockAssets(req.assets), + eventQueue: new EventQueue(), + currentValidators: req.consensus.currentValidators, + impliesMaxPrevote: req.consensus.implyMaxPrevote, + maxHeightCertified: req.consensus.maxHeightCertified, + transactions: req.transactions.map(tx => new Transaction(tx)), + }); + await this._stateMachine.afterExecuteBlock(context); + + return { + certificateThreshold: context.nextValidators.certificateThreshold, + nextValidators: context.nextValidators.validators, + preCommitThreshold: context.nextValidators.precommitThreshold, + events: context.eventQueue.getEvents().map(e => e.toObject()), + }; + } + + public async verifyTransaction( + req: VerifyTransactionRequest, + ): Promise { + let stateStore: StateStore; + let networkIdentifier: Buffer; + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + stateStore = new StateStore(this._stateDB); + networkIdentifier = req.networkIdentifier; + } else { + stateStore = this._executionContext.stateStore; + networkIdentifier = this._executionContext.networkIdentifier; + } + const context = new TransactionContext({ + eventQueue: new EventQueue(), + logger: this._logger, + transaction: new Transaction(req.transaction), + stateStore, + networkIdentifier, + }); + const result = await this._stateMachine.verifyTransaction(context); + + return { + result: result.status, + }; + } + + public async executeTransaction( + req: ExecuteTransactionRequest, + ): Promise { + let stateStore: StateStore; + let header: BlockHeader; + let networkIdentifier: Buffer; + if (!req.dryRun) { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + stateStore = this._executionContext.stateStore; + header = this._executionContext.header; + networkIdentifier = this._executionContext.networkIdentifier; + } else { + stateStore = new StateStore(this._stateDB); + header = new BlockHeader(req.header); + networkIdentifier = req.networkIdentifier; + } + const context = new TransactionContext({ + eventQueue: new EventQueue(), + logger: this._logger, + transaction: new Transaction(req.transaction), + stateStore, + networkIdentifier, + assets: new BlockAssets(req.assets), + header, + }); + await this._stateMachine.executeTransaction(context); + return { + events: context.eventQueue.getEvents().map(e => e.toObject()), + result: 0, + }; + } + + // TODO: Logic should be re-written with https://github.com/LiskHQ/lisk-sdk/issues/7128 + public async commit(req: CommitRequest): Promise { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const smtStore = new SMTStore(this._stateDB); + const smt = new SparseMerkleTree({ + db: smtStore, + rootHash: req.stateRoot, + }); + const batch = this._stateDB.batch(); + await this._executionContext.stateStore.finalize(batch, smt); + if (req.dryRun) { + return { + stateRoot: smt.rootHash, + }; + } + if (req.expectedStateRoot.length > 0 && req.expectedStateRoot.equals(smt.rootHash)) { + throw new Error( + `State root ${smt.rootHash.toString( + 'hex', + )} does not match with expected state root ${req.expectedStateRoot.toString('hex')}.`, + ); + } + + await batch.write(); + return { + stateRoot: smt.rootHash, + }; + } + + // TODO: Logic should be re-written with https://github.com/LiskHQ/lisk-sdk/issues/7128 + public async revert(req: RevertRequest): Promise { + if (!this._executionContext || !this._executionContext.id.equals(req.contextID)) { + throw new Error( + `Invalid context id ${req.contextID.toString( + 'hex', + )}. Context is not initialized or different.`, + ); + } + const heightBuf = formatInt(this._executionContext.header.height); + const diffKey = concatDBKeys(DB_KEY_DIFF_STATE, heightBuf); + + const stateDiff = await this._stateDB.get(diffKey); + + const { + created: createdStates, + updated: updatedStates, + deleted: deletedStates, + } = codec.decode<{ + created: Buffer[]; + updated: { key: Buffer; value: Buffer }[]; + deleted: { key: Buffer; value: Buffer }[]; + }>(stateDiffSchema, stateDiff); + const smtStore = new SMTStore(this._stateDB); + const smt = new SparseMerkleTree({ + db: smtStore, + rootHash: req.stateRoot, + }); + + const batch = this._stateDB.batch(); + const SMT_PREFIX_SIZE = 6; + const toSMTKey = (value: Buffer): Buffer => + // First byte is the DB prefix + Buffer.concat([value.slice(1, SMT_PREFIX_SIZE + 1), hash(value.slice(SMT_PREFIX_SIZE + 1))]); + // Delete all the newly created states + for (const key of createdStates) { + batch.del(key); + await smt.remove(toSMTKey(key)); + } + // Revert all deleted values + for (const { key, value: previousValue } of deletedStates) { + batch.put(key, previousValue); + await smt.update(toSMTKey(key), hash(previousValue)); + } + for (const { key, value: previousValue } of updatedStates) { + batch.put(key, previousValue); + await smt.update(toSMTKey(key), hash(previousValue)); + } + + smtStore.finalize(batch); + // Delete stored diff at particular height + batch.del(diffKey); + await batch.write(); + return { + stateRoot: smt.rootHash, + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async clear(_req: ClearRequest): Promise { + this._executionContext = undefined; + return {}; + } + + public async finalize(req: FinalizeRequest): Promise { + if (req.finalizedHeight === 0) { + return {}; + } + await this._stateDB.clear({ + gte: Buffer.concat([concatDBKeys(DB_KEY_DIFF_STATE), formatInt(0)]), + lte: concatDBKeys(DB_KEY_DIFF_STATE, formatInt(req.finalizedHeight - 1)), + }); + return {}; + } + + // eslint-disable-next-line @typescript-eslint/require-await + public async getMetadata(_req: MetadataRequest): Promise { + throw new Error('Method not implemented.'); + } + + public async query(req: QueryRequest): Promise { + const params = JSON.parse(req.params.toString('utf8')) as Record; + const resp = await this._channel.invoke(req.method, params); + return { + data: Buffer.from(JSON.stringify(resp), 'utf-8'), + }; + } + + public async prove(req: ProveRequest): Promise { + const smtStore = new SMTStore(this._stateDB); + const smt = new SparseMerkleTree({ + db: smtStore, + rootHash: req.stateRoot, + }); + const proof = await smt.generateMultiProof(req.keys); + return { + proof, + }; + } +} diff --git a/framework/src/abi_handler/abi_server.ts b/framework/src/abi_handler/abi_server.ts new file mode 100644 index 00000000000..aa40dde0188 --- /dev/null +++ b/framework/src/abi_handler/abi_server.ts @@ -0,0 +1,223 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { codec, Schema } from '@liskhq/lisk-codec'; +import { Router } from 'zeromq'; +import { + ABI, + IPCRequest, + ipcRequestSchema, + ipcResponseSchema, + afterTransactionsExecuteRequestSchema, + afterTransactionsExecuteResponseSchema, + beforeTransactionsExecuteRequestSchema, + beforeTransactionsExecuteResponseSchema, + clearRequestSchema, + clearResponseSchema, + commitRequestSchema, + commitResponseSchema, + executeTransactionRequestSchema, + executeTransactionResponseSchema, + finalizeRequestSchema, + finalizeResponseSchema, + initGenesisStateRequestSchema, + initGenesisStateResponseSchema, + initRequestSchema, + initResponseSchema, + initStateMachineRequestSchema, + initStateMachineResponseSchema, + insertAssetsRequestSchema, + insertAssetsResponseSchema, + metadataRequestSchema, + metadataResponseSchema, + proveRequestSchema, + proveResponseSchema, + queryRequestSchema, + queryResponseSchema, + revertRequestSchema, + revertResponseSchema, + verifyAssetsRequestSchema, + verifyAssetsResponseSchema, + verifyTransactionRequestSchema, + verifyTransactionResponseSchema, +} from '../abi'; +import { Logger } from '../logger'; + +export class ABIServer { + private readonly _socketPath: string; + private readonly _router: Router; + private readonly _logger: Logger; + + private readonly _abiHandlers: Record< + string, + { + request: Schema; + response: Schema; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + func: (req: any) => Promise; + } + > = {}; + + public constructor(logger: Logger, socketPath: string, abi: ABI) { + this._socketPath = socketPath; + this._logger = logger; + this._router = new Router(); + this._abiHandlers[abi.init.name] = { + request: initRequestSchema, + response: initResponseSchema, + func: abi.init.bind(abi), + }; + this._abiHandlers[abi.initStateMachine.name] = { + request: initStateMachineRequestSchema, + response: initStateMachineResponseSchema, + func: abi.initStateMachine.bind(abi), + }; + this._abiHandlers[abi.initGenesisState.name] = { + request: initGenesisStateRequestSchema, + response: initGenesisStateResponseSchema, + func: abi.initGenesisState.bind(abi), + }; + this._abiHandlers[abi.insertAssets.name] = { + request: insertAssetsRequestSchema, + response: insertAssetsResponseSchema, + func: abi.insertAssets.bind(abi), + }; + this._abiHandlers[abi.verifyAssets.name] = { + request: verifyAssetsRequestSchema, + response: verifyAssetsResponseSchema, + func: abi.verifyAssets.bind(abi), + }; + this._abiHandlers[abi.beforeTransactionsExecute.name] = { + request: beforeTransactionsExecuteRequestSchema, + response: beforeTransactionsExecuteResponseSchema, + func: abi.beforeTransactionsExecute.bind(abi), + }; + this._abiHandlers[abi.afterTransactionsExecute.name] = { + request: afterTransactionsExecuteRequestSchema, + response: afterTransactionsExecuteResponseSchema, + func: abi.afterTransactionsExecute.bind(abi), + }; + this._abiHandlers[abi.verifyTransaction.name] = { + request: verifyTransactionRequestSchema, + response: verifyTransactionResponseSchema, + func: abi.verifyTransaction.bind(abi), + }; + this._abiHandlers[abi.executeTransaction.name] = { + request: executeTransactionRequestSchema, + response: executeTransactionResponseSchema, + func: abi.executeTransaction.bind(abi), + }; + this._abiHandlers[abi.commit.name] = { + request: commitRequestSchema, + response: commitResponseSchema, + func: abi.commit.bind(abi), + }; + this._abiHandlers[abi.revert.name] = { + request: revertRequestSchema, + response: revertResponseSchema, + func: abi.revert.bind(abi), + }; + this._abiHandlers[abi.clear.name] = { + request: clearRequestSchema, + response: clearResponseSchema, + func: abi.clear.bind(abi), + }; + this._abiHandlers[abi.finalize.name] = { + request: finalizeRequestSchema, + response: finalizeResponseSchema, + func: abi.finalize.bind(abi), + }; + this._abiHandlers[abi.getMetadata.name] = { + request: metadataRequestSchema, + response: metadataResponseSchema, + func: abi.getMetadata.bind(abi), + }; + this._abiHandlers[abi.query.name] = { + request: queryRequestSchema, + response: queryResponseSchema, + func: abi.query.bind(abi), + }; + this._abiHandlers[abi.prove.name] = { + request: proveRequestSchema, + response: proveResponseSchema, + func: abi.prove.bind(abi), + }; + } + + public async start(): Promise { + await this._router.bind(this._socketPath); + this._listenToRequest().catch(err => { + this._logger.error({ err: err as Error }, 'Fail to listen to ABI request'); + }); + } + + public stop(): void { + this._router.close(); + } + + private async _listenToRequest() { + for await (const [sender, message] of this._router) { + let request: IPCRequest; + try { + request = codec.decode(ipcRequestSchema, message); + } catch (error) { + await this._replyError(sender, 'Failed to decode message'); + this._logger.debug({ err: error as Error }, 'Failed to decode ABI request'); + continue; + } + const handler = this._abiHandlers[request.method]; + if (!handler) { + await this._replyError(sender, `Method ${request.method} is not registered.`, request.id); + } + try { + // eslint-disable-next-line @typescript-eslint/ban-types + const params = codec.decode(handler.request, request.params); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const resp = await handler.func(params); + await this._router.send([ + sender, + codec.encode(ipcResponseSchema, { + id: request.id, + success: true, + error: { + message: '', + }, + result: codec.encode(handler.response, resp), + }), + ]); + } catch (error) { + await this._replyError(sender, (error as Error).message, request.id); + continue; + } + } + } + + private async _replyError(sender: Buffer, msg: string, id?: bigint): Promise { + await this._router + .send([ + sender, + codec.encode(ipcResponseSchema, { + id: id ?? BigInt(0), + success: false, + error: { + message: msg, + }, + result: Buffer.alloc(0), + }), + ]) + .catch(sendErr => { + this._logger.error({ err: sendErr as Error }, 'Failed to send response to the ABI request'); + }); + } +} diff --git a/framework/src/constants.ts b/framework/src/constants.ts index 6e9d4416406..cd325c6b610 100644 --- a/framework/src/constants.ts +++ b/framework/src/constants.ts @@ -25,3 +25,12 @@ export const RPC_MODES = { WS: 'ws', HTTP: 'http', }; + +export const DEFAULT_HOST = '127.0.0.1'; +export const DEFAULT_PORT_P2P = 7667; +export const DEFAULT_PORT_RPC = 7887; + +export const MAX_BLOCK_CACHE = 515; + +export const DEFAULT_MAX_INBOUND_CONNECTIONS = 100; +export const DEFAULT_MAX_OUTBOUND_CONNECTIONS = 20; diff --git a/framework/src/node/consensus/consensus.ts b/framework/src/node/consensus/consensus.ts index 2fb3c24c3fc..e301e8c036e 100644 --- a/framework/src/node/consensus/consensus.ts +++ b/framework/src/node/consensus/consensus.ts @@ -529,6 +529,10 @@ export class Consensus { header: block.header, assets: block.assets, transactions: block.transactions, + // TODO: Add real value with https://github.com/LiskHQ/lisk-sdk/issues/7130 + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await this._verify(block); await this._stateMachine.verifyAssets(ctx); diff --git a/framework/src/node/generator/generator.ts b/framework/src/node/generator/generator.ts index 4084868abd5..ffb6f8d94e9 100644 --- a/framework/src/node/generator/generator.ts +++ b/framework/src/node/generator/generator.ts @@ -597,6 +597,10 @@ export class Generator { logger: this._logger, networkIdentifier: this._chain.networkIdentifier, stateStore, + // TODO: Add real value with https://github.com/LiskHQ/lisk-sdk/issues/7130 + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await this._stateMachine.beforeExecuteBlock(blockCtx); diff --git a/framework/src/node/state_machine/block_context.ts b/framework/src/node/state_machine/block_context.ts index b81686bcf7c..adc29c6e633 100644 --- a/framework/src/node/state_machine/block_context.ts +++ b/framework/src/node/state_machine/block_context.ts @@ -24,6 +24,7 @@ import { BlockVerifyContext, BlockHeader, BlockAssets, + Validator, } from './types'; export interface ContextParams { @@ -32,6 +33,9 @@ export interface ContextParams { header: BlockHeader; assets: BlockAssets; logger: Logger; + currentValidators: Validator[]; + impliesMaxPrevote: boolean; + maxHeightCertified: number; eventQueue: EventQueue; transactions?: ReadonlyArray; } @@ -43,7 +47,15 @@ export class BlockContext { private readonly _eventQueue: EventQueue; private readonly _header: BlockHeader; private readonly _assets: BlockAssets; + private readonly _currentValidators: Validator[]; + private readonly _impliesMaxPrevote: boolean; + private readonly _maxHeightCertified: number; private _transactions?: ReadonlyArray; + private _nextValidators?: { + precommitThreshold: bigint; + certificateThreshold: bigint; + validators: Validator[]; + }; public constructor(params: ContextParams) { this._logger = params.logger; @@ -52,6 +64,9 @@ export class BlockContext { this._eventQueue = params.eventQueue; this._header = params.header; this._assets = params.assets; + this._currentValidators = params.currentValidators; + this._impliesMaxPrevote = params.impliesMaxPrevote; + this._maxHeightCertified = params.maxHeightCertified; this._transactions = params.transactions; } @@ -90,6 +105,9 @@ export class BlockContext { this._stateStore.getStore(moduleID, storePrefix), header: this._header, assets: this._assets, + currentValidators: this._currentValidators, + impliesMaxPrevote: this._impliesMaxPrevote, + maxHeightCertified: this._maxHeightCertified, }; } @@ -109,6 +127,23 @@ export class BlockContext { header: this._header, assets: this._assets, transactions: this._transactions, + currentValidators: this._currentValidators, + impliesMaxPrevote: this._impliesMaxPrevote, + maxHeightCertified: this._maxHeightCertified, + setNextValidators: ( + precommitThreshold: bigint, + certificateThreshold: bigint, + validators: Validator[], + ) => { + if (this._nextValidators) { + throw new Error('Next validators can be set only once'); + } + this._nextValidators = { + precommitThreshold, + certificateThreshold, + validators: [...validators], + }; + }, }; } @@ -127,4 +162,14 @@ export class BlockContext { public get eventQueue(): EventQueue { return this._eventQueue; } + + public get nextValidators() { + return ( + this._nextValidators ?? { + certificateThreshold: BigInt(0), + precommitThreshold: BigInt(0), + validators: [], + } + ); + } } diff --git a/framework/src/node/state_machine/generator_context.ts b/framework/src/node/state_machine/generator_context.ts new file mode 100644 index 00000000000..33e6883e5e2 --- /dev/null +++ b/framework/src/node/state_machine/generator_context.ts @@ -0,0 +1,71 @@ +/* + * Copyright © 2021 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { BlockAssets, BlockHeader, StateStore } from '@liskhq/lisk-chain'; +import { Logger } from '../../logger'; +import { createAPIContext } from './api_context'; +import { EventQueue } from './event_queue'; +import { InsertAssetContext } from './types'; + +interface GenerationContextArgs { + logger: Logger; + stateStore: StateStore; + header: BlockHeader; + generatorStore: StateStore; + networkIdentifier: Buffer; + finalizedHeight: number; +} + +export class GenerationContext { + private readonly _logger: Logger; + private readonly _networkIdentifier: Buffer; + private readonly _stateStore: StateStore; + private readonly _header: BlockHeader; + private readonly _assets: BlockAssets; + private readonly _generatorStore: StateStore; + private readonly _finalizedHeight: number; + + public constructor(args: GenerationContextArgs) { + this._logger = args.logger; + this._networkIdentifier = args.networkIdentifier; + this._header = args.header; + this._stateStore = args.stateStore; + this._generatorStore = args.generatorStore; + this._assets = new BlockAssets(); + this._finalizedHeight = args.finalizedHeight; + } + + public get blockHeader(): BlockHeader { + return this._header; + } + + public getInsertAssetContext(): InsertAssetContext { + return { + logger: this._logger, + getAPIContext: () => + createAPIContext({ stateStore: this._stateStore, eventQueue: new EventQueue() }), + getStore: (moduleID: number, storePrefix: number) => + this._stateStore.getStore(moduleID, storePrefix), + getGeneratorStore: (moduleID: number) => this._generatorStore.getStore(moduleID, 0), + header: this._header, + assets: this._assets, + networkIdentifier: this._networkIdentifier, + getFinalizedHeight: () => this._finalizedHeight, + }; + } + + public get assets(): BlockAssets { + return this._assets; + } +} diff --git a/framework/src/node/state_machine/genesis_block_context.ts b/framework/src/node/state_machine/genesis_block_context.ts index 4b017c0ffc3..91d44f79eee 100644 --- a/framework/src/node/state_machine/genesis_block_context.ts +++ b/framework/src/node/state_machine/genesis_block_context.ts @@ -17,7 +17,7 @@ import { Logger } from '../../logger'; import { APIContext, wrapEventQueue } from './api_context'; import { EVENT_INDEX_FINALIZE_GENESIS_STATE, EVENT_INDEX_INIT_GENESIS_STATE } from './constants'; import { EventQueue } from './event_queue'; -import { BlockAssets, BlockHeader, GenesisBlockExecuteContext } from './types'; +import { BlockAssets, BlockHeader, GenesisBlockExecuteContext, Validator } from './types'; export interface ContextParams { logger: Logger; @@ -33,6 +33,11 @@ export class GenesisBlockContext { private readonly _header: BlockHeader; private readonly _assets: BlockAssets; private readonly _eventQueue: EventQueue; + private _nextValidators?: { + precommitThreshold: bigint; + certificateThreshold: bigint; + validators: Validator[]; + }; public constructor(params: ContextParams) { this._logger = params.logger; @@ -53,6 +58,20 @@ export class GenesisBlockContext { header: this._header, logger: this._logger, assets: this._assets, + setNextValidators: ( + precommitThreshold: bigint, + certificateThreshold: bigint, + validators: Validator[], + ) => { + if (this._nextValidators) { + throw new Error('Next validators can be set only once'); + } + this._nextValidators = { + precommitThreshold, + certificateThreshold, + validators: [...validators], + }; + }, }; } @@ -67,10 +86,34 @@ export class GenesisBlockContext { header: this._header, logger: this._logger, assets: this._assets, + setNextValidators: ( + precommitThreshold: bigint, + certificateThreshold: bigint, + validators: Validator[], + ) => { + if (this._nextValidators) { + throw new Error('Next validators can be set only once'); + } + this._nextValidators = { + precommitThreshold, + certificateThreshold, + validators: [...validators], + }; + }, }; } public get eventQueue(): EventQueue { return this._eventQueue; } + + public get nextValidators() { + return ( + this._nextValidators ?? { + certificateThreshold: BigInt(0), + precommitThreshold: BigInt(0), + validators: [], + } + ); + } } diff --git a/framework/src/node/state_machine/state_machine.ts b/framework/src/node/state_machine/state_machine.ts index e145ca9cd75..1ffedd5ef3e 100644 --- a/framework/src/node/state_machine/state_machine.ts +++ b/framework/src/node/state_machine/state_machine.ts @@ -16,6 +16,7 @@ import { EVENT_STANDARD_TYPE_ID } from '@liskhq/lisk-chain'; import { standardEventDataSchema } from '@liskhq/lisk-chain/dist-node/schema'; import { codec, Schema } from '@liskhq/lisk-codec'; import { BlockContext } from './block_context'; +import { GenerationContext } from './generator_context'; import { GenesisBlockContext } from './genesis_block_context'; import { TransactionContext } from './transaction_context'; import { @@ -29,6 +30,7 @@ import { CommandVerifyContext, CommandExecuteContext, BlockAfterExecuteContext, + InsertAssetContext, } from './types'; export interface StateMachineCommand { @@ -41,6 +43,7 @@ export interface StateMachineCommand { export interface StateMachineModule { id: number; commands: StateMachineCommand[]; + initBlock?: (ctx: InsertAssetContext) => Promise; verifyTransaction?: (ctx: TransactionVerifyContext) => Promise; initGenesisState?: (ctx: GenesisBlockExecuteContext) => Promise; finalizeGenesisState?: (ctx: GenesisBlockExecuteContext) => Promise; @@ -99,6 +102,20 @@ export class StateMachine { } } + public async insertAssets(ctx: GenerationContext): Promise { + const initContext = ctx.getInsertAssetContext(); + for (const mod of this._systemModules) { + if (mod.initBlock) { + await mod.initBlock(initContext); + } + } + for (const mod of this._modules) { + if (mod.initBlock) { + await mod.initBlock(initContext); + } + } + } + public async verifyTransaction(ctx: TransactionContext): Promise { const transactionContext = ctx.createTransactionVerifyContext(); try { diff --git a/framework/src/node/state_machine/types.ts b/framework/src/node/state_machine/types.ts index 676c8321423..67cd2daa9f4 100644 --- a/framework/src/node/state_machine/types.ts +++ b/framework/src/node/state_machine/types.ts @@ -69,6 +69,10 @@ export interface BlockAssets { getAsset: (moduleID: number) => Buffer | undefined; } +export interface WritableBlockAssets extends BlockAssets { + setAsset: (moduleID: number, value: Buffer) => void; +} + export interface VerificationResult { status: VerifyStatus; error?: Error; @@ -110,6 +114,11 @@ export interface GenesisBlockExecuteContext { getStore: (moduleID: number, storePrefix: number) => SubStore; header: BlockHeader; assets: BlockAssets; + setNextValidators: ( + preCommitThreshold: bigint, + certificateThreshold: bigint, + validators: Validator[], + ) => void; } export interface TransactionExecuteContext { @@ -140,8 +149,34 @@ export interface BlockExecuteContext { getStore: (moduleID: number, storePrefix: number) => SubStore; header: BlockHeader; assets: BlockAssets; + currentValidators: Validator[]; + impliesMaxPrevote: boolean; + maxHeightCertified: number; +} + +export interface Validator { + address: Buffer; + bftWeight: bigint; + generatorKey: Buffer; + blsKey: Buffer; } export interface BlockAfterExecuteContext extends BlockExecuteContext { transactions: ReadonlyArray; + setNextValidators: ( + preCommitThreshold: bigint, + certificateThreshold: bigint, + validators: Validator[], + ) => void; +} + +export interface InsertAssetContext { + logger: Logger; + networkIdentifier: Buffer; + getAPIContext: () => APIContext; + getStore: (moduleID: number, storePrefix: number) => ImmutableSubStore; + header: BlockHeader; + assets: WritableBlockAssets; + getGeneratorStore: (moduleID: number) => SubStore; + getFinalizedHeight(): number; } diff --git a/framework/src/schema/application_config_schema.ts b/framework/src/schema/application_config_schema.ts index e52a419e1f6..142e45cb1d2 100644 --- a/framework/src/schema/application_config_schema.ts +++ b/framework/src/schema/application_config_schema.ts @@ -355,7 +355,7 @@ export const applicationConfigSchema = { }, generation: { type: 'object', - required: ['force', 'waitThreshold', 'delegates', 'modules'], + required: ['force', 'waitThreshold', 'generators', 'modules'], properties: { password: { type: 'string', @@ -368,7 +368,7 @@ export const applicationConfigSchema = { description: 'Number of seconds to wait for previous block before forging', type: 'integer', }, - delegates: { ...delegatesConfigSchema }, + generators: { ...delegatesConfigSchema }, modules: { ...moduleConfigSchema, }, @@ -425,7 +425,7 @@ export const applicationConfigSchema = { generation: { force: false, waitThreshold: 2, - delegates: [], + generators: [], modules: {}, }, }, diff --git a/framework/src/testing/create_contexts.ts b/framework/src/testing/create_contexts.ts index 7437ee8e206..12b110f06ca 100644 --- a/framework/src/testing/create_contexts.ts +++ b/framework/src/testing/create_contexts.ts @@ -32,6 +32,7 @@ import { loggerMock } from './mocks'; import { BlockGenerateContext } from '../node/generator'; import { WritableBlockAssets } from '../node/generator/types'; import { GeneratorStore } from '../node/generator/generator_store'; +import { Validator } from '../abi'; export const createGenesisBlockContext = (params: { header?: BlockHeader; @@ -80,6 +81,7 @@ export const createBlockContext = (params: { header?: BlockHeader; assets?: BlockAssets; transactions?: Transaction[]; + validators?: Validator[]; }): BlockContext => { const logger = params.logger ?? loggerMock; const stateStore = params.stateStore ?? new StateStore(new InMemoryKVStore()); @@ -112,6 +114,9 @@ export const createBlockContext = (params: { header, assets: params.assets ?? new BlockAssets(), networkIdentifier: getRandomBytes(32), + currentValidators: params.validators ?? [], + impliesMaxPrevote: true, + maxHeightCertified: 0, }); return ctx; }; diff --git a/framework/test/unit/__snapshots__/application.spec.ts.snap b/framework/test/unit/__snapshots__/application.spec.ts.snap index 4ff470f3afe..fc58b8328a8 100644 --- a/framework/test/unit/__snapshots__/application.spec.ts.snap +++ b/framework/test/unit/__snapshots__/application.spec.ts.snap @@ -2170,6 +2170,7 @@ Object { }, ], "force": true, + "generators": Array [], "modules": Object {}, "waitThreshold": 2, }, diff --git a/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap b/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap new file mode 100644 index 00000000000..d75a7aeb753 --- /dev/null +++ b/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`abi handler init should return valid response 1`] = ` +Object { + "generator": Object { + "force": false, + "keys": Array [], + "password": "", + }, + "genesis": Object { + "bftBatchSize": 10, + "blockTime": 10, + "communityIdentifier": "sdk", + "maxFeePerByte": 1000, + "maxTransactionsSize": 15360, + }, + "logger": Object { + "consoleLogLevel": "none", + "fileLogLevel": "info", + "logFileName": "lisk.log", + }, + "network": Object { + "advertiseAddress": false, + "blackListedIPs": Array [], + "fixedPeers": Array [], + "hostIP": "127.0.0.1", + "maxInboundConnections": 100, + "maxOutboundConnections": 20, + "port": 5000, + "seedPeers": Array [], + "whitelistedPeers": Array [], + }, + "rpc": Object { + "http": Object { + "host": "127.0.0.1", + "port": 8000, + }, + "ipc": Object { + "path": "/User/lisk/.lisk/beta-sdk-app/socket/ipc", + }, + "modes": Array [ + "ipc", + ], + "ws": Object { + "host": "127.0.0.1", + "port": 8000, + }, + }, + "system": Object { + "dataPath": "/User/lisk/.lisk/beta-sdk-app", + "maxBlockCache": 515, + "networkVersion": "1.1", + }, + "txpool": Object { + "maxTransactions": 4096, + "maxTransactionsPerAccount": 4096, + "minEntranceFeePriority": 0n, + "minReplacementFeeDifference": 10n, + "transactionExpiryTime": 10800000, + }, +} +`; diff --git a/framework/test/unit/abi_handler/abi_client.spec.ts b/framework/test/unit/abi_handler/abi_client.spec.ts new file mode 100644 index 00000000000..58563d64931 --- /dev/null +++ b/framework/test/unit/abi_handler/abi_client.spec.ts @@ -0,0 +1,68 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; +import { ABIClient } from '../../../src/abi_handler/abi_client'; +import { ABIHandler } from '../../../src/abi_handler/abi_handler'; +import { StateMachine } from '../../../src/node/state_machine'; +import { TokenModule } from '../../../src/modules/token'; +import { BaseModule } from '../../../src/modules'; +import { fakeLogger } from '../../utils/node'; +import { channelMock } from '../../../src/testing/mocks'; +import { genesisBlock } from '../../fixtures'; +import { applicationConfigSchema } from '../../../src/schema'; + +jest.mock('zeromq', () => { + return { + Dealer: jest.fn().mockReturnValue({ connect: jest.fn(), close: jest.fn() }), + }; +}); + +describe('ABI client', () => { + const genesis = genesisBlock(); + + let client: ABIClient; + let abiHandler: ABIHandler; + + beforeEach(() => { + const stateMachine = new StateMachine(); + const mod = new TokenModule(); + stateMachine.registerModule(mod as BaseModule); + abiHandler = new ABIHandler({ + logger: fakeLogger, + channel: channelMock, + stateDB: (new InMemoryKVStore() as unknown) as KVStore, + moduleDB: (new InMemoryKVStore() as unknown) as KVStore, + genesisBlock: genesis, + stateMachine, + modules: [mod], + config: applicationConfigSchema.default, + }); + + client = new ABIClient(fakeLogger, '/path/to/ipc'); + }); + + describe('constructor', () => { + it('should have all abi handlers', () => { + const allFuncs = Object.getOwnPropertyNames(Object.getPrototypeOf(abiHandler)).filter( + name => name !== 'constructor', + ); + + const clientFuncs = Object.getOwnPropertyNames(Object.getPrototypeOf(client)); + for (const expectedFunc of allFuncs) { + expect(clientFuncs).toContain(expectedFunc); + } + }); + }); +}); diff --git a/framework/test/unit/abi_handler/abi_handler.spec.ts b/framework/test/unit/abi_handler/abi_handler.spec.ts new file mode 100644 index 00000000000..5d670b771fc --- /dev/null +++ b/framework/test/unit/abi_handler/abi_handler.spec.ts @@ -0,0 +1,619 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import * as os from 'os'; +import { StateStore, Transaction } from '@liskhq/lisk-chain'; +import { codec } from '@liskhq/lisk-codec'; +import { getRandomBytes } from '@liskhq/lisk-cryptography'; +import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; +import { BaseModule, TokenModule } from '../../../src'; +import { ABIHandler } from '../../../src/abi_handler/abi_handler'; +import { transferParamsSchema } from '../../../src/modules/token/schemas'; +import { StateMachine } from '../../../src/node/state_machine'; +import { applicationConfigSchema } from '../../../src/schema'; +import { createFakeBlockHeader } from '../../../src/testing'; +import { channelMock } from '../../../src/testing/mocks'; +import { genesisBlock } from '../../fixtures'; +import { fakeLogger } from '../../utils/node'; + +describe('abi handler', () => { + let abiHandler: ABIHandler; + const genesis = genesisBlock(); + + beforeEach(() => { + const stateMachine = new StateMachine(); + const mod = new TokenModule(); + jest.spyOn(mod.commands[0], 'execute').mockResolvedValue(); + stateMachine.registerModule(mod as BaseModule); + abiHandler = new ABIHandler({ + logger: fakeLogger, + channel: channelMock, + stateDB: (new InMemoryKVStore() as unknown) as KVStore, + moduleDB: (new InMemoryKVStore() as unknown) as KVStore, + genesisBlock: genesis, + stateMachine, + modules: [mod], + config: applicationConfigSchema.default, + }); + }); + + describe('init', () => { + it('should return valid response', async () => { + jest.spyOn(os, 'homedir').mockReturnValue('/User/lisk'); + const resp = await abiHandler.init({}); + + expect(resp.genesisBlock).toEqual({ + header: genesis.header.toObject(), + assets: genesis.assets.getAll(), + transactions: [], + }); + expect(resp.registeredModules).toHaveLength(1); + expect(resp.config).toMatchSnapshot(); + }); + }); + + describe('initStateMachine', () => { + it('should fail if execution context exists', async () => { + abiHandler['_executionContext'] = { + id: getRandomBytes(32), + } as never; + await expect( + abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }), + ).rejects.toThrow('Execution context is already initialized'); + }); + + it('should create execution context and resolve context id', async () => { + const networkIdentifier = getRandomBytes(32); + const resp = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier, + }); + expect(resp.contextID).toHaveLength(32); + expect(abiHandler['_executionContext']).not.toBeUndefined(); + expect(abiHandler['_executionContext']?.networkIdentifier).toEqual(networkIdentifier); + expect(abiHandler['_executionContext']?.stateStore).toBeInstanceOf(StateStore); + }); + }); + + describe('initGenesisState', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.initGenesisState({ + contextID: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.initGenesisState({ + contextID: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should execute genesis block and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'executeGenesisBlock'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + const resp = await abiHandler.initGenesisState({ + contextID, + stateRoot: getRandomBytes(32), + }); + expect(abiHandler['_stateMachine'].executeGenesisBlock).toHaveBeenCalledTimes(1); + + expect(resp.events).toBeArray(); + expect(resp.assets).toEqual(genesis.assets.getAll()); + expect(resp.nextValidators).toBeArray(); + expect(resp.certificateThreshold).toEqual(BigInt(0)); + expect(resp.preCommitThreshold).toEqual(BigInt(0)); + }); + }); + + describe('insertAssets', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.insertAssets({ + contextID: getRandomBytes(32), + finalizedHeight: 0, + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.insertAssets({ + contextID: getRandomBytes(32), + finalizedHeight: 0, + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should execute insertAssets and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'insertAssets'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + const resp = await abiHandler.insertAssets({ + contextID, + finalizedHeight: 0, + }); + expect(abiHandler['_stateMachine'].insertAssets).toHaveBeenCalledTimes(1); + + expect(resp.assets).toEqual([]); + }); + }); + + describe('verifyAssets', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.verifyAssets({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.verifyAssets({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should execute verifyAssets and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'verifyAssets'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + const resp = await abiHandler.verifyAssets({ + contextID, + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + }); + expect(abiHandler['_stateMachine'].verifyAssets).toHaveBeenCalledTimes(1); + + expect(resp).not.toBeUndefined(); + }); + }); + + describe('beforeTransactionsExecute', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.beforeTransactionsExecute({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.beforeTransactionsExecute({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should execute beforeTransactionsExecute and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'beforeExecuteBlock'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + const resp = await abiHandler.beforeTransactionsExecute({ + contextID, + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + }); + expect(abiHandler['_stateMachine'].beforeExecuteBlock).toHaveBeenCalledTimes(1); + + expect(resp.events).toBeArray(); + }); + }); + + describe('afterTransactionsExecute', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.afterTransactionsExecute({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + transactions: [], + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.afterTransactionsExecute({ + contextID: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + transactions: [], + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should execute afterTransactionsExecute and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'afterExecuteBlock'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + const resp = await abiHandler.afterTransactionsExecute({ + contextID, + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + consensus: { + currentValidators: [ + { + address: getRandomBytes(20), + bftWeight: BigInt(1), + blsKey: getRandomBytes(48), + generatorKey: getRandomBytes(32), + }, + ], + implyMaxPrevote: false, + maxHeightCertified: 0, + }, + transactions: [], + }); + expect(abiHandler['_stateMachine'].afterExecuteBlock).toHaveBeenCalledTimes(1); + + expect(resp.events).toBeArray(); + }); + }); + + describe('verifyTransaction', () => { + it('should execute verifyTransaction with existing context when context ID is not empty and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'verifyTransaction'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + // Add random data to check if new state store is used or not + await abiHandler['_executionContext']?.stateStore.set( + getRandomBytes(20), + getRandomBytes(100), + ); + const tx = new Transaction({ + commandID: 2, + fee: BigInt(30), + moduleID: 2, + nonce: BigInt(2), + params: getRandomBytes(100), + senderPublicKey: getRandomBytes(32), + signatures: [getRandomBytes(64)], + }); + const resp = await abiHandler.verifyTransaction({ + contextID, + networkIdentifier: getRandomBytes(32), + transaction: tx.toObject(), + }); + + expect(abiHandler['_stateMachine'].verifyTransaction).toHaveBeenCalledTimes(1); + expect( + (abiHandler['_stateMachine'].verifyTransaction as jest.Mock).mock.calls[0][0][ + '_stateStore' + ], + ).toEqual(abiHandler['_executionContext']?.stateStore); + expect(resp.result).toEqual(0); + }); + + it('should execute verifyTransaction with new context when context ID is empty and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'verifyTransaction'); + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + // Add random data to check if new state store is used or not + await abiHandler['_executionContext']?.stateStore.set( + getRandomBytes(20), + getRandomBytes(100), + ); + const tx = new Transaction({ + commandID: 2, + fee: BigInt(30), + moduleID: 2, + nonce: BigInt(2), + params: getRandomBytes(100), + senderPublicKey: getRandomBytes(32), + signatures: [getRandomBytes(64)], + }); + const resp = await abiHandler.verifyTransaction({ + contextID: Buffer.alloc(0), + networkIdentifier: getRandomBytes(32), + transaction: tx.toObject(), + }); + + expect(abiHandler['_stateMachine'].verifyTransaction).toHaveBeenCalledTimes(1); + expect( + (abiHandler['_stateMachine'].verifyTransaction as jest.Mock).mock.calls[0][0][ + '_stateStore' + ], + ).not.toEqual(abiHandler['_executionContext']?.stateStore); + expect(resp.result).toEqual(0); + }); + }); + + describe('executeTransaction', () => { + it('should execute executeTransaction with existing context when context ID is not empty and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'executeTransaction'); + const { contextID } = await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + // Add random data to check if new state store is used or not + await abiHandler['_executionContext']?.stateStore.set( + getRandomBytes(20), + getRandomBytes(100), + ); + const tx = new Transaction({ + commandID: 0, + fee: BigInt(30), + moduleID: 2, + nonce: BigInt(2), + params: codec.encode(transferParamsSchema, {}), + senderPublicKey: getRandomBytes(32), + signatures: [getRandomBytes(64)], + }); + const resp = await abiHandler.executeTransaction({ + contextID, + networkIdentifier: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + dryRun: false, + header: createFakeBlockHeader().toObject(), + transaction: tx.toObject(), + }); + + expect(abiHandler['_stateMachine'].executeTransaction).toHaveBeenCalledTimes(1); + expect( + (abiHandler['_stateMachine'].executeTransaction as jest.Mock).mock.calls[0][0][ + '_stateStore' + ], + ).toEqual(abiHandler['_executionContext']?.stateStore); + expect(resp.result).toEqual(0); + }); + + it('should execute executeTransaction with new context when context ID is empty and resolve the response', async () => { + jest.spyOn(abiHandler['_stateMachine'], 'executeTransaction'); + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + // Add random data to check if new state store is used or not + await abiHandler['_executionContext']?.stateStore.set( + getRandomBytes(20), + getRandomBytes(100), + ); + const tx = new Transaction({ + commandID: 0, + fee: BigInt(30), + moduleID: 2, + nonce: BigInt(2), + params: codec.encode(transferParamsSchema, {}), + senderPublicKey: getRandomBytes(32), + signatures: [getRandomBytes(64)], + }); + const resp = await abiHandler.executeTransaction({ + contextID: getRandomBytes(0), + networkIdentifier: getRandomBytes(32), + assets: [{ data: getRandomBytes(30), moduleID: 2 }], + dryRun: true, + header: createFakeBlockHeader().toObject(), + transaction: tx.toObject(), + }); + + expect(abiHandler['_stateMachine'].executeTransaction).toHaveBeenCalledTimes(1); + expect( + (abiHandler['_stateMachine'].executeTransaction as jest.Mock).mock.calls[0][0][ + '_stateStore' + ], + ).not.toEqual(abiHandler['_executionContext']?.stateStore); + expect(resp.result).toEqual(0); + }); + }); + + describe('commit', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.commit({ + contextID: getRandomBytes(32), + dryRun: true, + expectedStateRoot: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.commit({ + contextID: getRandomBytes(32), + dryRun: true, + expectedStateRoot: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it.todo('should resolve updated state root without saving when dryRun == true'); + + it.todo( + 'should reject before saving if new state root is different from the expected stateRoot', + ); + + it.todo('should resolve updated state root after saving all the states'); + }); + + describe('revert', () => { + it('should fail if execution context does not exist', async () => { + await expect( + abiHandler.revert({ + contextID: getRandomBytes(32), + expectedStateRoot: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it('should fail if execution context does not match', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + await expect( + abiHandler.revert({ + contextID: getRandomBytes(32), + expectedStateRoot: getRandomBytes(32), + stateRoot: getRandomBytes(32), + }), + ).rejects.toThrow('Context is not initialized or different'); + }); + + it.todo('should resolve updated state root'); + + it.todo('should revert all the sates'); + }); + + describe('clear', () => { + it('should clear the execution context', async () => { + await abiHandler.initStateMachine({ + header: createFakeBlockHeader().toObject(), + networkIdentifier: getRandomBytes(32), + }); + expect(abiHandler['_executionContext']).not.toBeUndefined(); + await abiHandler.clear({}); + + expect(abiHandler['_executionContext']).toBeUndefined(); + }); + }); + + describe('finalized', () => { + it('should clean up all the finalized state', async () => { + jest.spyOn(abiHandler['_stateDB'], 'clear'); + + await abiHandler.finalize({ + finalizedHeight: 10, + }); + expect(abiHandler['_stateDB'].clear).toHaveBeenCalledTimes(1); + }); + }); + + describe('getMetadata', () => { + it.todo('should resolve metadata from all the modules'); + }); + + describe('query', () => { + it.todo('should query module endpoint with expected context'); + + it.todo('should query plugin endpoint with expected context'); + }); + + describe('prove', () => { + it.todo('should provide proof based on the queries provided'); + }); +}); diff --git a/framework/test/unit/abi_handler/abi_server.spec.ts b/framework/test/unit/abi_handler/abi_server.spec.ts new file mode 100644 index 00000000000..cf6edcde446 --- /dev/null +++ b/framework/test/unit/abi_handler/abi_server.spec.ts @@ -0,0 +1,64 @@ +/* + * Copyright © 2022 Lisk Foundation + * + * See the LICENSE file at the top-level directory of this distribution + * for licensing information. + * + * Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation, + * no part of this software, including this file, may be copied, modified, + * propagated, or distributed except according to the terms contained in the + * LICENSE file. + * + * Removal or modification of this copyright notice is prohibited. + */ + +import { InMemoryKVStore, KVStore } from '@liskhq/lisk-db'; +import { ABIServer } from '../../../src/abi_handler/abi_server'; +import { ABIHandler } from '../../../src/abi_handler/abi_handler'; +import { StateMachine } from '../../../src/node/state_machine'; +import { TokenModule } from '../../../src/modules/token'; +import { BaseModule } from '../../../src/modules'; +import { fakeLogger } from '../../utils/node'; +import { channelMock } from '../../../src/testing/mocks'; +import { genesisBlock } from '../../fixtures'; +import { applicationConfigSchema } from '../../../src/schema'; + +jest.mock('zeromq', () => { + return { + Router: jest.fn().mockReturnValue({ bind: jest.fn(), close: jest.fn() }), + }; +}); + +describe('ABI server', () => { + const genesis = genesisBlock(); + + let server: ABIServer; + let abiHandler: ABIHandler; + + beforeEach(() => { + const stateMachine = new StateMachine(); + const mod = new TokenModule(); + stateMachine.registerModule(mod as BaseModule); + abiHandler = new ABIHandler({ + logger: fakeLogger, + channel: channelMock, + stateDB: (new InMemoryKVStore() as unknown) as KVStore, + moduleDB: (new InMemoryKVStore() as unknown) as KVStore, + genesisBlock: genesis, + stateMachine, + modules: [mod], + config: applicationConfigSchema.default, + }); + + server = new ABIServer(fakeLogger, '/path/to/ipc', abiHandler); + }); + + describe('constructor', () => { + it('should register abi handlers', () => { + const allFuncs = Object.getOwnPropertyNames(Object.getPrototypeOf(abiHandler)).filter( + name => name !== 'constructor', + ); + expect(Object.keys(server['_abiHandlers'])).toEqual(allFuncs); + }); + }); +}); diff --git a/framework/test/unit/application.spec.ts b/framework/test/unit/application.spec.ts index f33044e4b67..44d40dfdc60 100644 --- a/framework/test/unit/application.spec.ts +++ b/framework/test/unit/application.spec.ts @@ -179,7 +179,7 @@ describe('Application', () => { // Arrange const invalidConfig = objects.mergeDeep({}, config, { generation: { - delegates: [ + generators: [ { encryptedPassphrase: '0dbd21ac5c154dbb72ce90a4e252a64b692203a4f8e25f8bfa1b1993e2ba7a9bd9e1ef1896d8d584a62daf17a8ccf12b99f29521b92cc98b74434ff501374f7e1c6d8371a6ce4e2d083489', diff --git a/framework/test/unit/node/state_machine/state_machine.spec.ts b/framework/test/unit/node/state_machine/state_machine.spec.ts index 44e4fa6dbfa..e6aaaa66706 100644 --- a/framework/test/unit/node/state_machine/state_machine.spec.ts +++ b/framework/test/unit/node/state_machine/state_machine.spec.ts @@ -72,6 +72,7 @@ describe('state_machine', () => { getStore: expect.any(Function), header: genesisHeader, assets, + setNextValidators: expect.any(Function), }); }); @@ -185,6 +186,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.verifyAssets(ctx); expect(mod.verifyAssets).toHaveBeenCalledWith({ @@ -210,6 +214,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.beforeExecuteBlock(ctx); expect(mod.beforeTransactionsExecute).toHaveBeenCalledWith({ @@ -220,6 +227,9 @@ describe('state_machine', () => { eventQueue: expect.any(Object), getAPIContext: expect.any(Function), getStore: expect.any(Function), + currentValidators: expect.any(Array), + impliesMaxPrevote: expect.any(Boolean), + maxHeightCertified: expect.any(Number), }); expect(systemMod.beforeTransactionsExecute).toHaveBeenCalledTimes(1); expect(mod.beforeTransactionsExecute).toHaveBeenCalledTimes(1); @@ -235,6 +245,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.beforeExecuteBlock(ctx); @@ -254,6 +267,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.afterExecuteBlock(ctx); expect(mod.afterTransactionsExecute).toHaveBeenCalledWith({ @@ -265,6 +281,10 @@ describe('state_machine', () => { getAPIContext: expect.any(Function), getStore: expect.any(Function), transactions: [transaction], + currentValidators: expect.any(Array), + impliesMaxPrevote: expect.any(Boolean), + maxHeightCertified: expect.any(Number), + setNextValidators: expect.any(Function), }); expect(mod.afterTransactionsExecute).toHaveBeenCalledTimes(1); }); @@ -279,6 +299,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.afterExecuteBlock(ctx); @@ -298,6 +321,9 @@ describe('state_machine', () => { assets, networkIdentifier, transactions: [transaction], + currentValidators: [], + impliesMaxPrevote: false, + maxHeightCertified: 0, }); await stateMachine.executeBlock(ctx); expect(mod.beforeTransactionsExecute).toHaveBeenCalledWith({ @@ -308,6 +334,9 @@ describe('state_machine', () => { eventQueue: expect.any(Object), getAPIContext: expect.any(Function), getStore: expect.any(Function), + currentValidators: expect.any(Array), + impliesMaxPrevote: expect.any(Boolean), + maxHeightCertified: expect.any(Number), }); expect(systemMod.beforeTransactionsExecute).toHaveBeenCalledTimes(1); expect(mod.beforeTransactionsExecute).toHaveBeenCalledTimes(1); @@ -320,6 +349,10 @@ describe('state_machine', () => { getAPIContext: expect.any(Function), getStore: expect.any(Function), transactions: [transaction], + currentValidators: expect.any(Array), + impliesMaxPrevote: expect.any(Boolean), + maxHeightCertified: expect.any(Number), + setNextValidators: expect.any(Function), }); expect(mod.afterTransactionsExecute).toHaveBeenCalledTimes(1); }); diff --git a/framework/test/unit/schema/__snapshots__/application_config_schema.spec.ts.snap b/framework/test/unit/schema/__snapshots__/application_config_schema.spec.ts.snap index 5089703f802..e98d4942e4f 100644 --- a/framework/test/unit/schema/__snapshots__/application_config_schema.spec.ts.snap +++ b/framework/test/unit/schema/__snapshots__/application_config_schema.spec.ts.snap @@ -6,8 +6,8 @@ Object { "additionalProperties": false, "default": Object { "generation": Object { - "delegates": Array [], "force": false, + "generators": Array [], "modules": Object {}, "waitThreshold": 2, }, @@ -61,7 +61,10 @@ Object { "properties": Object { "generation": Object { "properties": Object { - "delegates": Object { + "force": Object { + "type": "boolean", + }, + "generators": Object { "items": Object { "properties": Object { "address": Object { @@ -107,9 +110,6 @@ Object { }, "type": "array", }, - "force": Object { - "type": "boolean", - }, "modules": Object { "additionalProperties": Object { "type": "object", @@ -131,7 +131,7 @@ Object { "required": Array [ "force", "waitThreshold", - "delegates", + "generators", "modules", ], "type": "object", From 1b67be4cc5688293985866ed8ca7ff596269760d Mon Sep 17 00:00:00 2001 From: shuse2 Date: Fri, 20 May 2022 15:51:16 +0200 Subject: [PATCH 2/4] :bug: Fix merge conflict --- framework/src/abi_handler/abi_handler.ts | 6 ++++-- .../unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/framework/src/abi_handler/abi_handler.ts b/framework/src/abi_handler/abi_handler.ts index 4d97a279741..5804893c643 100644 --- a/framework/src/abi_handler/abi_handler.ts +++ b/framework/src/abi_handler/abi_handler.ts @@ -158,6 +158,8 @@ export class ABIHandler implements ABI { logger: this._config.logger, system: { dataPath, + keepEventsForHeights: this._config.system.keepEventsForHeights, + version: this._config.version, maxBlockCache: MAX_BLOCK_CACHE, networkVersion: this._config.networkVersion, }, @@ -165,7 +167,7 @@ export class ABIHandler implements ABI { bftBatchSize: this._config.genesis.blockTime, blockTime: this._config.genesis.blockTime, communityIdentifier: this._config.genesis.communityIdentifier, - maxFeePerByte: this._config.genesis.minFeePerByte, + minFeePerByte: this._config.genesis.minFeePerByte, maxTransactionsSize: this._config.genesis.maxTransactionsSize, }, generator: { @@ -192,7 +194,7 @@ export class ABIHandler implements ABI { network: { ...this._config.network, port: this._config.network.port ?? DEFAULT_PORT_P2P, - blackListedIPs: this._config.network.blacklistedIPs ?? [], + blacklistedIPs: this._config.network.blacklistedIPs ?? [], advertiseAddress: this._config.network.advertiseAddress ?? false, fixedPeers: this._config.network.fixedPeers ?? [], seedPeers: this._config.network.seedPeers, diff --git a/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap b/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap index d75a7aeb753..14e13344801 100644 --- a/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap +++ b/framework/test/unit/abi_handler/__snapshots__/abi_handler.spec.ts.snap @@ -11,8 +11,8 @@ Object { "bftBatchSize": 10, "blockTime": 10, "communityIdentifier": "sdk", - "maxFeePerByte": 1000, "maxTransactionsSize": 15360, + "minFeePerByte": 1000, }, "logger": Object { "consoleLogLevel": "none", @@ -21,7 +21,7 @@ Object { }, "network": Object { "advertiseAddress": false, - "blackListedIPs": Array [], + "blacklistedIPs": Array [], "fixedPeers": Array [], "hostIP": "127.0.0.1", "maxInboundConnections": 100, @@ -48,8 +48,10 @@ Object { }, "system": Object { "dataPath": "/User/lisk/.lisk/beta-sdk-app", + "keepEventsForHeights": 300, "maxBlockCache": 515, "networkVersion": "1.1", + "version": "0.0.0", }, "txpool": Object { "maxTransactions": 4096, From 51fd4bf59a6d2b5f5acf20e43dc2d68630e2be20 Mon Sep 17 00:00:00 2001 From: shuse2 Date: Mon, 23 May 2022 14:37:28 +0200 Subject: [PATCH 3/4] :recycle: Fix typo --- framework/src/abi_handler/abi_client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/src/abi_handler/abi_client.ts b/framework/src/abi_handler/abi_client.ts index a9e01a002ea..fc2fd5f3796 100644 --- a/framework/src/abi_handler/abi_client.ts +++ b/framework/src/abi_handler/abi_client.ts @@ -317,15 +317,15 @@ export class ABIClient implements ABI { this._logger.debug({ err: error as Error }, 'Failed to decode ABI response'); continue; } - const defered = this._pendingRequests[response.id.toString()]; - if (!defered) { + const deferred = this._pendingRequests[response.id.toString()]; + if (!deferred) { continue; } if (!response.success) { - defered.reject(new Error(response.error.message)); + deferred.reject(new Error(response.error.message)); } else { - defered.resolve(response.result); + deferred.resolve(response.result); } delete this._pendingRequests[response.id.toString()]; From a99dfa30598c103d9025ef74fa4ed70fca461933 Mon Sep 17 00:00:00 2001 From: shuse2 Date: Wed, 25 May 2022 14:52:24 +0200 Subject: [PATCH 4/4] :bug: Fix the error condition and result --- framework/src/abi_handler/abi_handler.ts | 5 +++-- framework/test/unit/abi_handler/abi_handler.spec.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/framework/src/abi_handler/abi_handler.ts b/framework/src/abi_handler/abi_handler.ts index 5804893c643..6476e4eaf6e 100644 --- a/framework/src/abi_handler/abi_handler.ts +++ b/framework/src/abi_handler/abi_handler.ts @@ -69,6 +69,7 @@ import { QueryResponse, ProveRequest, ProveResponse, + TransactionExecutionResult, } from '../abi'; import { Logger } from '../logger'; import { BaseModule } from '../modules'; @@ -448,7 +449,7 @@ export class ABIHandler implements ABI { await this._stateMachine.executeTransaction(context); return { events: context.eventQueue.getEvents().map(e => e.toObject()), - result: 0, + result: TransactionExecutionResult.OK, }; } @@ -473,7 +474,7 @@ export class ABIHandler implements ABI { stateRoot: smt.rootHash, }; } - if (req.expectedStateRoot.length > 0 && req.expectedStateRoot.equals(smt.rootHash)) { + if (req.expectedStateRoot.length > 0 && !req.expectedStateRoot.equals(smt.rootHash)) { throw new Error( `State root ${smt.rootHash.toString( 'hex', diff --git a/framework/test/unit/abi_handler/abi_handler.spec.ts b/framework/test/unit/abi_handler/abi_handler.spec.ts index 5d670b771fc..f3ccc110fa5 100644 --- a/framework/test/unit/abi_handler/abi_handler.spec.ts +++ b/framework/test/unit/abi_handler/abi_handler.spec.ts @@ -26,6 +26,7 @@ import { createFakeBlockHeader } from '../../../src/testing'; import { channelMock } from '../../../src/testing/mocks'; import { genesisBlock } from '../../fixtures'; import { fakeLogger } from '../../utils/node'; +import { TransactionExecutionResult } from '../../../src/abi'; describe('abi handler', () => { let abiHandler: ABIHandler; @@ -471,7 +472,7 @@ describe('abi handler', () => { '_stateStore' ], ).toEqual(abiHandler['_executionContext']?.stateStore); - expect(resp.result).toEqual(0); + expect(resp.result).toEqual(TransactionExecutionResult.OK); }); it('should execute executeTransaction with new context when context ID is empty and resolve the response', async () => { @@ -509,7 +510,7 @@ describe('abi handler', () => { '_stateStore' ], ).not.toEqual(abiHandler['_executionContext']?.stateStore); - expect(resp.result).toEqual(0); + expect(resp.result).toEqual(TransactionExecutionResult.OK); }); });