diff --git a/README.md b/README.md index b7189d06c..9cf6afab8 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,73 @@ $ npm run build ## Run Hyperledger Explorer +### Bootup Mode + +The Bootup Mode feature allows you to specify how many blocks should be loaded when starting the Hyperledger Explorer. You can choose from the below two modes: + +- **ALL**: Load all available blocks. +- **CUSTOM**: Load a specific number of blocks as configured in the `config.json` file. + +To set the Bootup Mode, update the `app/platform/fabric/config.json` file in your project with the desired mode. + +- **ALL** + ```json + { + "network-configs": { + "test-network": { + "name": "Test Network", + "profile": "./connection-profile/test-network.json", + "enableAuthentication": false, + "bootMode": "ALL", + "noOfBlocks": 0 + } + }, + "license": "Apache-2.0" + } + ``` +**Note:** In ALL Mode, Please make sure that `noOfBlocks` paramater is set to `0` + +- **CUSTOM** + + The `noOfBlocks` parameter allows you to specify the number of blocks that the Hyperledger Explorer will use when booting up. If you are using custom mode and want to control the number of blocks, make sure to pass your desired value to the `noOfBlocks` parameter. + + ```json + { + "network-configs": { + "test-network": { + "name": "Test Network", + "profile": "./connection-profile/test-network.json", + "enableAuthentication": false, + "bootMode": "CUSTOM", + "noOfBlocks": 5 + } + }, + "license": "Apache-2.0" + } + ``` + +**Note:** Setting `noOfBlocks` to `0` will load Hyperledger Explorer with the latest block. + +### Bootup Mode example for reference + +Let's say your blockchain network consists of a total of 20 blocks, numbered from 1 to 20. You are interested in loading only the latest 5 blocks, which are blocks 20, 19, 18, 17, and 16. + +Here is an example of how you can configure Hyperledger Explorer to achieve this: + ```json + { + "network-configs": { + "test-network": { + "name": "Test Network", + "profile": "./connection-profile/test-network.json", + "enableAuthentication": false, + "bootMode": "CUSTOM", + "noOfBlocks": 5 + } + }, + "license": "Apache-2.0" + } + ``` + ### Run Locally in the Same Location * Modify `app/explorerconfig.json` to update sync settings. diff --git a/app/persistence/fabric/CRUDService.ts b/app/persistence/fabric/CRUDService.ts index 26ba30ee4..4c0c87da3 100644 --- a/app/persistence/fabric/CRUDService.ts +++ b/app/persistence/fabric/CRUDService.ts @@ -545,6 +545,21 @@ export class CRUDService { } // Orderer BE-303 + /** + * Returns whether the block is available in the DB or not + * + * @param {*} blockHeight + * @returns + * @memberof CRUDService + */ + async isBlockAvailableInDB(channel_genesis_hash: string, blockHeight: number) { + const count: any = await this.sql.getRowsBySQlCase( + `SELECT COUNT(*) FROM blocks WHERE channel_genesis_hash= $1 AND blocknum = $2`, + [channel_genesis_hash, blockHeight] + ); + return count.count > 0; + } + /** * * Returns the block by block number. diff --git a/app/platform/fabric/Proxy.ts b/app/platform/fabric/Proxy.ts index e21a0bdd8..bd1f3e79b 100644 --- a/app/platform/fabric/Proxy.ts +++ b/app/platform/fabric/Proxy.ts @@ -226,23 +226,40 @@ export class Proxy { * @memberof Proxy */ async getChannelsInfo(network_id) { - const client = this.platform.getClient(network_id); - const channels = await this.persistence - .getCrudService() - .getChannelsInfo(network_id); - const currentchannels = []; - for (const channel of channels) { - const channel_genesis_hash = client.getChannelGenHash(channel.channelname); - let agoBlockTimes = this.getLatestBlockTime(channel); - if ( - channel_genesis_hash && - channel_genesis_hash === channel.channel_genesis_hash - ) { - currentchannels.push({ ...channel, agoBlockTimes }); + try { + const client = this.platform.getClient(network_id); + const channels = await this.persistence.getCrudService().getChannelsInfo(network_id); + const updatedChannels = []; + + for (const channel of channels) { + const channel_genesis_hash = client.getChannelGenHash(channel.channelname); + let agoBlockTimes = this.getLatestBlockTime(channel); + + try { + const chainInfo = await client.fabricGateway.queryChainInfo(channel.channelname); + + if (chainInfo && chainInfo.height && chainInfo.height.low >= 0) { + const totalBlocks = chainInfo.height.low; + + if (channel_genesis_hash && channel_genesis_hash === channel.channel_genesis_hash) { + updatedChannels.push({ ...channel, totalBlocks, agoBlockTimes }); + } else { + updatedChannels.push({ ...channel, totalBlocks }); + } + } else { + logger.warn(`Invalid chain information for channel: ${channel.channelname}`); + } + } catch (error) { + logger.error(`Error querying chain information for channel: ${channel.channelname}`, error); + } } + + logger.debug('getChannelsInfo %j', updatedChannels); + return updatedChannels; + } catch (error) { + logger.error("Error querying channel information:", error); + return null; } - logger.debug('getChannelsInfo >> %j', currentchannels); - return currentchannels; } /** diff --git a/app/platform/fabric/config.json b/app/platform/fabric/config.json index f99d37b59..a707df117 100644 --- a/app/platform/fabric/config.json +++ b/app/platform/fabric/config.json @@ -2,7 +2,9 @@ "network-configs": { "test-network": { "name": "Test Network", - "profile": "./connection-profile/test-network.json" + "profile": "./connection-profile/test-network.json", + "bootMode": "ALL", + "noOfBlocks": 0 } }, "license": "Apache-2.0" diff --git a/app/platform/fabric/sync/SyncService.ts b/app/platform/fabric/sync/SyncService.ts index 5f6e55d9f..aaed50bda 100644 --- a/app/platform/fabric/sync/SyncService.ts +++ b/app/platform/fabric/sync/SyncService.ts @@ -5,6 +5,10 @@ import fabprotos from 'fabric-protos'; import includes from 'lodash/includes'; import * as sha from 'js-sha256'; + +import * as PATH from 'path'; +import * as fs from 'fs'; + import { helper } from '../../../common/helper'; import { ExplorerError } from '../../../common/ExplorerError'; @@ -16,6 +20,12 @@ const logger = helper.getLogger('SyncServices'); const fabric_const = FabricConst.fabric.const; +const config_path = PATH.resolve(__dirname, '../config.json'); +const all_config = JSON.parse(fs.readFileSync(config_path, 'utf8')); +const network_configs = all_config[fabric_const.NETWORK_CONFIGS]; + +const boot_modes = FabricConst.BootModes; + // Transaction validation code const _validation_codes = {}; for (const key in fabprotos.protos.TxValidationCode) { @@ -356,52 +366,123 @@ export class SyncServices { .saveChaincodPeerRef(network_id, chaincode_peer_row); } + /** + * + * + * @param {*} channel_name + * @memberof SyncServices + */ async syncBlocks(client, channel_name, noDiscovery) { const network_id = client.getNetworkId(); - - // Get channel information from ledger - const channelInfo = await client.fabricGateway.queryChainInfo(channel_name); - - if (!channelInfo) { - logger.info(`syncBlocks: Failed to retrieve channelInfo >> ${channel_name}`); - return; - } const synch_key = `${network_id}_${channel_name}`; - logger.info(`syncBlocks: Start >> ${synch_key}`); + + // Check if block synchronization is already in process if (this.synchInProcess.includes(synch_key)) { logger.info(`syncBlocks: Block sync in process for >> ${synch_key}`); return; } - this.synchInProcess.push(synch_key); + try { + // Get channel information from ledger + const channelInfo = await client.fabricGateway.queryChainInfo(channel_name); + if (!channelInfo) { + logger.info( + `syncBlocks: Failed to retrieve channelInfo >> ${channel_name}` + ); + return; + } - const channel_genesis_hash = client.getChannelGenHash(channel_name); - const blockHeight = parseInt(channelInfo.height.low) - 1; - // Query missing blocks from DB - const results = await this.persistence - .getMetricService() - .findMissingBlockNumber(network_id, channel_genesis_hash, blockHeight); - - if (results) { - for (const result of results) { - // Get block by number - try { - const block = await client.fabricGateway.queryBlock( - channel_name, - result.missing_id + // Getting necessary information from configuration file + const bootMode = network_configs[network_id].bootMode.toUpperCase(); + const channel_genesis_hash = client.getChannelGenHash(channel_name); + const latestBlockHeight = parseInt(channelInfo.height.low) - 1; + + // Get the value of noOfBlocks from configuration file + let noOfBlocks = 0; + + if (bootMode === boot_modes[0]) { + // Sync all available blocks + noOfBlocks = latestBlockHeight + 1; + } else if (bootMode === boot_modes[1]) { + // Get the value of noOfBlocks from configuration file + noOfBlocks = parseInt(network_configs[network_id].noOfBlocks); + + if (isNaN(noOfBlocks)) { + logger.error( + 'Invalid noOfBlocks configuration, please either provide in numeric eg: (1) or ("1")' ); - if (block) { + return; + } + } + + // Calculate the starting block height for sync + const startingBlockHeight = Math.max(0, latestBlockHeight - noOfBlocks + 1); + + // Syncing Details + logger.info( + `Syncing blocks from ${startingBlockHeight} to ${latestBlockHeight}` + ); + + // Load the latest blocks as per the configuration + for ( + let blockHeight = latestBlockHeight; + blockHeight >= startingBlockHeight; + blockHeight-- + ) { + try { + const [block, isBlockAvailable] = await Promise.all([ + client.fabricGateway.queryBlock(channel_name, blockHeight), + this.persistence + .getCrudService() + .isBlockAvailableInDB(channel_genesis_hash, blockHeight) + ]); + + if (block && !isBlockAvailable) { await this.processBlockEvent(client, block, noDiscovery); } + logger.info(`Synced block #${blockHeight}`); } catch { - logger.error(`Failed to process Block # ${result.missing_id}`); + logger.error(`Failed to process Block # ${blockHeight}`); + } + } + + const missingBlocks = await this.persistence + .getMetricService() + .findMissingBlockNumber( + network_id, + channel_genesis_hash, + latestBlockHeight + ); + + if (missingBlocks) { + // Filter missing blocks to start syncing from 'startingBlockHeight' + const missingBlocksToSync = missingBlocks.filter( + missingBlock => missingBlock.missing_id >= startingBlockHeight + ); + for (const missingBlock of missingBlocksToSync) { + try { + const block = await client.fabricGateway.queryBlock( + channel_name, + missingBlock.missing_id + ); + if (block) { + await this.processBlockEvent(client, block, noDiscovery); + } + logger.info(`Synced missing block #${missingBlock.missing_id}`); + } catch { + logger.error( + `Failed to process Missing Block # ${missingBlock.missing_id}` + ); + } } + } else { + logger.debug('Missing blocks not found for %s', channel_name); } - } else { - logger.debug('Missing blocks not found for %s', channel_name); + const index = this.synchInProcess.indexOf(synch_key); + this.synchInProcess.splice(index, 1); + logger.info(`syncBlocks: Finish >> ${synch_key}`); + } catch (error) { + logger.error(`Error in syncBlocks: ${error}`); } - const index = this.synchInProcess.indexOf(synch_key); - this.synchInProcess.splice(index, 1); - logger.info(`syncBlocks: Finish >> ${synch_key}`); } async updateDiscoveredChannel(client, channel_name, channel_genesis_hash) { diff --git a/app/platform/fabric/utils/FabricConst.ts b/app/platform/fabric/utils/FabricConst.ts index 72517bfe7..a2236c27c 100644 --- a/app/platform/fabric/utils/FabricConst.ts +++ b/app/platform/fabric/utils/FabricConst.ts @@ -18,3 +18,8 @@ export const fabric = { NOTITY_TYPE_CLIENTERROR: '6' } }; + +export enum BootModes { + 'ALL', + 'CUSTOM' +} diff --git a/app/test/SyncService.test.ts b/app/test/SyncService.test.ts index 50a2219f6..48d8070c4 100644 --- a/app/test/SyncService.test.ts +++ b/app/test/SyncService.test.ts @@ -210,35 +210,120 @@ describe('processBlockEvent', () => { }); describe('syncBlocks', () => { - let sync: SyncServices; - - before(() => { - sync = getSyncServicesInstance(); - }); - - beforeEach(() => { - resetAllStubs(sync); - }); - - it('should return without error', async () => { - const stubClient = setupClient(); - const stubProcessBlockEvent = sinon.stub(sync, 'processBlockEvent'); - - await sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false); - expect(stubProcessBlockEvent.calledTwice).to.be.true; - stubProcessBlockEvent.restore(); - }); - - it('should return without error when processBlockEvent throws exception', async () => { - const stubClient = setupClient(); - const stubProcessBlockEvent = sinon.stub(sync, 'processBlockEvent'); - stubProcessBlockEvent.onFirstCall().throws('Block already in processing'); - stubError.reset(); - - await sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false); - expect(stubProcessBlockEvent.calledTwice).to.be.true; - expect(stubError.calledWith('Failed to process Block # 1')).to.be.true; - expect(stubError.calledWith('Failed to process Block # 2')).to.be.false; - stubProcessBlockEvent.restore(); - }); + let sync; + let stubClient; + let fakeIsBlockAvailableInDB; + const boot_modes = ['ALL', 'CUSTOM']; + const network_configs = { + network_id: { + noOfBlocks: '10' + } + }; + const latestBlockHeight = 20; + + before(() => { + sync = getSyncServicesInstance(); + }); + + beforeEach(() => { + resetAllStubs(sync); + stubClient = setupClient(); + fakeIsBlockAvailableInDB = false; + }); + + it('should handle block synchronization already in process', async () => { + const stubClient = setupClient(); + sync.synchInProcess.push(`${VALID_NETWORK_ID}_${VALID_CHANNEL_NAME}`); + + await expect(sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false)).eventually.to.be.undefined; + sinon.assert.notCalled(stubClient.fabricGateway.queryChainInfo); + sinon.assert.notCalled(stubClient.getChannelGenHash); + }); + + it('should return without error', async () => { + const stubClient = setupClient(); + const stubProcessBlockEvent = sinon.stub(sync, 'processBlockEvent'); + + await sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false); + expect(stubProcessBlockEvent.notCalled).to.be.true; + stubProcessBlockEvent.restore(); + }); + + it('should calculate starting block height correctly for mode1 - ALL', () => { + const bootMode = boot_modes[0]; + let noOfBlocks; + + if (bootMode === boot_modes[0]) { + noOfBlocks = latestBlockHeight + 1; + } else if (bootMode === boot_modes[1]) { + noOfBlocks = parseInt(network_configs.network_id.noOfBlocks); + if (isNaN(noOfBlocks)) { + return; + } + } + + const startingBlockHeight = Math.max(0, latestBlockHeight - noOfBlocks + 1); + expect(startingBlockHeight).to.equal(0); + }); + + it('should calculate starting block height correctly for mode2 - CUSTOM', () => { + const bootMode = boot_modes[1]; + let noOfBlocks; + + if (bootMode === boot_modes[0]) { + noOfBlocks = latestBlockHeight + 1; + } else if (bootMode === boot_modes[1]) { + noOfBlocks = parseInt(network_configs.network_id.noOfBlocks); + if (isNaN(noOfBlocks)) { + return; + } + } + + const startingBlockHeight = Math.max(0, latestBlockHeight - noOfBlocks + 1); + expect(startingBlockHeight).to.equal(11); + }); + + it('should sync all available blocks in "ALL" mode', async () => { + stubClient.getNetworkId.returns(VALID_NETWORK_ID); + + const fakeProcessBlockEvent = sinon.fake.resolves(true); + sync.processBlockEvent = fakeProcessBlockEvent; + + await sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false); + + // Mock isBlockAvailableInDB to return true for all blocks + fakeIsBlockAvailableInDB = true; + const fakePersistence = { + getCrudService: () => ({ + isBlockAvailableInDB: () => fakeIsBlockAvailableInDB, + }) + }; + sync.persistence = fakePersistence; + sinon.assert.notCalled(fakeProcessBlockEvent); + }); + + it('should accept a valid numeric noOfBlocks in "CUSTOM" mode', async () => { + stubClient.getNetworkId.returns(VALID_NETWORK_ID); + + const fakeProcessBlockEvent = sinon.fake.resolves(true); + sync.processBlockEvent = fakeProcessBlockEvent; + + boot_modes[0] = 'CUSTOM'; + network_configs.network_id.noOfBlocks = '5'; + + // Mock isBlockAvailableInDB to return true for all blocks + fakeIsBlockAvailableInDB = true; + const fakePersistence = { + getCrudService: () => ({ + isBlockAvailableInDB: () => fakeIsBlockAvailableInDB, + }) + }; + sync.persistence = fakePersistence; + + await sync.syncBlocks(stubClient, VALID_CHANNEL_NAME, false); + + // Ensure the function executes without errors + sinon.assert.notCalled(fakeProcessBlockEvent); + }); }); +