diff --git a/package-lock.json b/package-lock.json index 8248b91a7..2fae59830 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "origintrail_node", - "version": "8.2.4", + "version": "8.2.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "origintrail_node", - "version": "8.2.4", + "version": "8.2.5", "license": "ISC", "dependencies": { "@comunica/query-sparql": "^4.0.2", diff --git a/package.json b/package.json index 7cc8b8bf1..36aafc45e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "origintrail_node", - "version": "8.2.4", + "version": "8.2.5", "description": "OTNode V8", "main": "index.js", "type": "module", diff --git a/src/controllers/http-api/v1/publish-http-api-controller-v1.js b/src/controllers/http-api/v1/publish-http-api-controller-v1.js index 4d7d19667..1c72ac638 100644 --- a/src/controllers/http-api/v1/publish-http-api-controller-v1.js +++ b/src/controllers/http-api/v1/publish-http-api-controller-v1.js @@ -5,6 +5,7 @@ import { OPERATION_STATUS, LOCAL_STORE_TYPES, COMMAND_PRIORITY, + PUBLISH_MIN_NUM_OF_NODE_REPLICATIONS, } from '../../../constants/constants.js'; class PublishController extends BaseController { @@ -16,6 +17,7 @@ class PublishController extends BaseController { this.repositoryModuleManager = ctx.repositoryModuleManager; this.pendingStorageService = ctx.pendingStorageService; this.networkModuleManager = ctx.networkModuleManager; + this.blockchainModuleManager = ctx.blockchainModuleManager; } async handleRequest(req, res) { @@ -62,6 +64,37 @@ class PublishController extends BaseController { datasetRoot, }); + let effectiveMinReplications = minimumNumberOfNodeReplications; + let chainMinNumber = null; + try { + const chainMin = await this.blockchainModuleManager.getMinimumRequiredSignatures( + blockchain, + ); + chainMinNumber = Number(chainMin); + } catch (err) { + this.logger.warn( + `Failed to fetch on-chain minimumRequiredSignatures for ${blockchain}: ${err.message}`, + ); + } + + const userMinNumber = Number(effectiveMinReplications); + const resolvedUserMin = + !Number.isNaN(userMinNumber) && userMinNumber > 0 + ? userMinNumber + : PUBLISH_MIN_NUM_OF_NODE_REPLICATIONS; + + if (!Number.isNaN(chainMinNumber) && chainMinNumber > 0) { + effectiveMinReplications = Math.max(chainMinNumber, resolvedUserMin); + } else { + effectiveMinReplications = resolvedUserMin; + } + + if (effectiveMinReplications === 0) { + this.logger.error( + `Effective minimum replications resolved to 0 for operationId: ${operationId}, blockchain: ${blockchain}. This should never happen.`, + ); + } + const publisherNodePeerId = this.networkModuleManager.getPeerId().toB58String(); await this.pendingStorageService.cacheDataset( operationId, @@ -80,7 +113,7 @@ class PublishController extends BaseController { blockchain, operationId, storeType: LOCAL_STORE_TYPES.TRIPLE, - minimumNumberOfNodeReplications, + minimumNumberOfNodeReplications: effectiveMinReplications, }, transactional: false, priority: COMMAND_PRIORITY.HIGHEST, diff --git a/src/modules/blockchain/blockchain-module-manager.js b/src/modules/blockchain/blockchain-module-manager.js index 11313d347..d128cf616 100644 --- a/src/modules/blockchain/blockchain-module-manager.js +++ b/src/modules/blockchain/blockchain-module-manager.js @@ -211,6 +211,10 @@ class BlockchainModuleManager extends BaseModuleManager { return this.callImplementationFunction(blockchain, 'getMaximumStake'); } + async getMinimumRequiredSignatures(blockchain) { + return this.callImplementationFunction(blockchain, 'getMinimumRequiredSignatures'); + } + async getLatestBlock(blockchain) { return this.callImplementationFunction(blockchain, 'getLatestBlock'); } diff --git a/src/modules/blockchain/implementation/web3-service.js b/src/modules/blockchain/implementation/web3-service.js index 4b1149cce..1f3157f51 100644 --- a/src/modules/blockchain/implementation/web3-service.js +++ b/src/modules/blockchain/implementation/web3-service.js @@ -1024,6 +1024,15 @@ class Web3Service { return Number(ethers.utils.formatEther(maximumStake)); } + async getMinimumRequiredSignatures() { + return this.callContractFunction( + this.contracts.ParametersStorage, + 'minimumRequiredSignatures', + [], + CONTRACTS.PARAMETERS_STORAGE, + ); + } + async getShardingTableHead() { return this.callContractFunction(this.contracts.ShardingTableStorage, 'head', []); } diff --git a/test/unit/controllers/publish-http-api-controller-v1.test.js b/test/unit/controllers/publish-http-api-controller-v1.test.js new file mode 100644 index 000000000..3f25856b2 --- /dev/null +++ b/test/unit/controllers/publish-http-api-controller-v1.test.js @@ -0,0 +1,132 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; + +import PublishController from '../../../src/controllers/http-api/v1/publish-http-api-controller-v1.js'; +import { PUBLISH_MIN_NUM_OF_NODE_REPLICATIONS } from '../../../src/constants/constants.js'; + +const createRes = () => { + const res = { + statusCode: null, + body: null, + status(code) { + this.statusCode = code; + return this; + }, + json(payload) { + this.body = payload; + return this; + }, + send(payload) { + this.body = payload; + return this; + }, + }; + return res; +}; + +describe('publish-http-api-controller-v1', () => { + const baseCtx = () => { + const addedCommands = []; + return { + commandExecutor: { + add: async (cmd) => { + addedCommands.push(cmd); + }, + _added: addedCommands, + }, + publishService: { + getOperationName: () => 'publish', + }, + operationIdService: { + generateOperationId: async () => 'op-id-123', + emitChangeEvent: () => {}, + updateOperationIdStatus: async () => {}, + cacheOperationIdDataToMemory: async () => {}, + cacheOperationIdDataToFile: async () => {}, + }, + repositoryModuleManager: { + createOperationRecord: async () => {}, + }, + pendingStorageService: { + cacheDataset: async () => {}, + }, + networkModuleManager: { + getPeerId: () => ({ toB58String: () => 'peer-self' }), + }, + blockchainModuleManager: { + getMinimumRequiredSignatures: async () => PUBLISH_MIN_NUM_OF_NODE_REPLICATIONS, + }, + logger: { + info: () => {}, + warn: () => {}, + error: () => {}, + }, + }; + }; + + it('clamps minimumNumberOfNodeReplications to on-chain minimum', async () => { + const ctx = baseCtx(); + ctx.blockchainModuleManager.getMinimumRequiredSignatures = async () => 5; // on-chain min + const controller = new PublishController(ctx); + + const req = { + body: { + dataset: { public: {} }, + datasetRoot: '0xroot', + blockchain: 'hardhat', + minimumNumberOfNodeReplications: 2, // below chain min + }, + }; + const res = createRes(); + + await controller.handleRequest(req, res); + + expect(res.statusCode).to.equal(202); + const added = ctx.commandExecutor._added[0]; + expect(added.data.minimumNumberOfNodeReplications).to.equal(5); + }); + + it('allows higher user override than on-chain minimum', async () => { + const ctx = baseCtx(); + ctx.blockchainModuleManager.getMinimumRequiredSignatures = async () => 3; // on-chain min + const controller = new PublishController(ctx); + + const req = { + body: { + dataset: { public: {} }, + datasetRoot: '0xroot', + blockchain: 'hardhat', + minimumNumberOfNodeReplications: 7, // above chain min + }, + }; + const res = createRes(); + + await controller.handleRequest(req, res); + + expect(res.statusCode).to.equal(202); + const added = ctx.commandExecutor._added[0]; + expect(added.data.minimumNumberOfNodeReplications).to.equal(7); + }); + + it('falls back to on-chain minimum when user value is zero or invalid', async () => { + const ctx = baseCtx(); + ctx.blockchainModuleManager.getMinimumRequiredSignatures = async () => 4; // on-chain min + const controller = new PublishController(ctx); + + const req = { + body: { + dataset: { public: {} }, + datasetRoot: '0xroot', + blockchain: 'hardhat', + minimumNumberOfNodeReplications: 0, // invalid/zero + }, + }; + const res = createRes(); + + await controller.handleRequest(req, res); + + expect(res.statusCode).to.equal(202); + const added = ctx.commandExecutor._added[0]; + expect(added.data.minimumNumberOfNodeReplications).to.equal(4); + }); +});