From 67e6cbf65d6373b3d624a9b8c5ac27f75687a059 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 15 Jul 2026 20:26:00 -0500 Subject: [PATCH 1/3] fix(dashmate): use live Tenderdash app version for protocol status Source Protocol Version from status.application_info.version instead of the process-start snapshot in node_info.protocol_version.app, while keeping Desired Protocol Version from abci_info.app_version. Adds a regression unit test covering a stale node_info app protocol with a newer live application_info version after in-process upgrade. Fixes #4135 --- .../dashmate/src/status/scopes/platform.js | 6 +- .../test/unit/status/scopes/platform.spec.js | 76 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/dashmate/src/status/scopes/platform.js b/packages/dashmate/src/status/scopes/platform.js index 467e8e4ce0e..04b0fb3fb55 100644 --- a/packages/dashmate/src/status/scopes/platform.js +++ b/packages/dashmate/src/status/scopes/platform.js @@ -144,7 +144,11 @@ export default function getPlatformScopeFactory( } info.version = version; - info.protocolVersion = parseInt(tenderdashStatus.node_info.protocol_version.app, 10); + // node_info.protocol_version.app is snapshotted at Tenderdash process start and + // stays stale across in-process protocol upgrades. application_info.version is + // the live active/current app protocol version. + info.protocolVersion = parseInt(tenderdashStatus.application_info.version, 10); + // abci_info app_version reflects the installed software's desired/supported version. info.desiredProtocolVersion = tenderdashAbciInfo.response.app_version; info.listening = listening; info.latestBlockHeight = latestBlockHeight; diff --git a/packages/dashmate/test/unit/status/scopes/platform.spec.js b/packages/dashmate/test/unit/status/scopes/platform.spec.js index 14f0d88cffe..1fae37878a8 100644 --- a/packages/dashmate/test/unit/status/scopes/platform.spec.js +++ b/packages/dashmate/test/unit/status/scopes/platform.spec.js @@ -89,6 +89,9 @@ describe('getPlatformScopeFactory', () => { network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: false, latest_app_hash: 'DEADBEEF', @@ -157,6 +160,73 @@ describe('getPlatformScopeFactory', () => { expect(scope).to.deep.equal(expectedScope); }); + it('should use live application_info version when node_info protocol version is stale', async () => { + mockDetermineDockerStatus.returns(DockerStatusEnum.running); + mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); + mockRpcClient.getBlockchainInfo.returns({ + result: { + softforks: { + mn_rr: { active: true, height: 1337 }, + }, + }, + }); + mockDockerCompose.isServiceRunning.returns(true); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci status').resolves({ exitCode: 0, out: '' }); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '4.0.0' }); + mockMNOWatchProvider.returns(Promise.resolve('OPEN')); + + // After an in-process protocol upgrade, Tenderdash keeps the process-start + // snapshot in node_info.protocol_version.app while application_info.version + // and abci_info.app_version report the live/desired values. + const mockStatus = { + node_info: { + protocol_version: { + p2p: '10', + block: '14', + app: '11', + }, + version: '1.6.0', + network: 'dash-mainnet-1', + moniker: 'evonode', + }, + application_info: { + version: '12', + }, + sync_info: { + catching_up: false, + latest_app_hash: 'DEADBEEF', + latest_block_height: 398435, + latest_block_hash: 'DEADBEEF', + latest_block_time: 1337, + }, + }; + const mockNetInfo = { n_peers: 6, listening: true }; + + const mockAbciInfo = { + response: { + version: '4.0.0', + app_version: 12, + last_block_height: 398435, + last_block_app_hash: 's0CySQxgRg96DrnJ7HCsql+k/Sk4JiT3y0psCaUI3TI=', + }, + }; + + mockFetch + .onFirstCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockStatus) })) + .onSecondCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockNetInfo) })) + .onThirdCall() + .resolves({ json: () => Promise.resolve(mockAbciInfo) }); + + const scope = await getPlatformScope(config); + + expect(scope.tenderdash.protocolVersion).to.equal(12); + expect(scope.tenderdash.desiredProtocolVersion).to.equal(12); + expect(scope.tenderdash.version).to.equal('1.6.0'); + expect(scope.tenderdash.latestBlockHeight).to.equal(398435); + }); + it('should return platform syncing when it is catching up', async () => { mockDetermineDockerStatus.returns(DockerStatusEnum.running); mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); @@ -182,6 +252,9 @@ describe('getPlatformScopeFactory', () => { network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: true, latest_app_hash: 'DEADBEEF', @@ -445,6 +518,9 @@ describe('getPlatformScopeFactory', () => { network: 'test', moniker: 'test', }, + application_info: { + version: '3', + }, sync_info: { catching_up: false, latest_app_hash: 'DEADBEEF', From 554262467c6781ff978fc5766a9a3d74aaa6ae05 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 15 Jul 2026 20:29:26 -0500 Subject: [PATCH 2/3] test(dashmate): fold protocol-version regression into happy-path fixture Prove protocolVersion prefers live application_info.version by setting a stale node_info.protocol_version.app in the existing success case instead of duplicating the full mock bootstrap. --- .../test/unit/status/scopes/platform.spec.js | 71 +------------------ 1 file changed, 3 insertions(+), 68 deletions(-) diff --git a/packages/dashmate/test/unit/status/scopes/platform.spec.js b/packages/dashmate/test/unit/status/scopes/platform.spec.js index 1fae37878a8..28610e56718 100644 --- a/packages/dashmate/test/unit/status/scopes/platform.spec.js +++ b/packages/dashmate/test/unit/status/scopes/platform.spec.js @@ -78,12 +78,14 @@ describe('getPlatformScopeFactory', () => { mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '1.4.1' }); mockMNOWatchProvider.returns(Promise.resolve('OPEN')); + // node_info.protocol_version.app can be stale after in-process upgrades; + // protocolVersion must come from application_info.version (live). const mockStatus = { node_info: { protocol_version: { p2p: '10', block: '14', - app: '3', + app: '11', }, version: '0', network: 'test', @@ -160,73 +162,6 @@ describe('getPlatformScopeFactory', () => { expect(scope).to.deep.equal(expectedScope); }); - it('should use live application_info version when node_info protocol version is stale', async () => { - mockDetermineDockerStatus.returns(DockerStatusEnum.running); - mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); - mockRpcClient.getBlockchainInfo.returns({ - result: { - softforks: { - mn_rr: { active: true, height: 1337 }, - }, - }, - }); - mockDockerCompose.isServiceRunning.returns(true); - mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci status').resolves({ exitCode: 0, out: '' }); - mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '4.0.0' }); - mockMNOWatchProvider.returns(Promise.resolve('OPEN')); - - // After an in-process protocol upgrade, Tenderdash keeps the process-start - // snapshot in node_info.protocol_version.app while application_info.version - // and abci_info.app_version report the live/desired values. - const mockStatus = { - node_info: { - protocol_version: { - p2p: '10', - block: '14', - app: '11', - }, - version: '1.6.0', - network: 'dash-mainnet-1', - moniker: 'evonode', - }, - application_info: { - version: '12', - }, - sync_info: { - catching_up: false, - latest_app_hash: 'DEADBEEF', - latest_block_height: 398435, - latest_block_hash: 'DEADBEEF', - latest_block_time: 1337, - }, - }; - const mockNetInfo = { n_peers: 6, listening: true }; - - const mockAbciInfo = { - response: { - version: '4.0.0', - app_version: 12, - last_block_height: 398435, - last_block_app_hash: 's0CySQxgRg96DrnJ7HCsql+k/Sk4JiT3y0psCaUI3TI=', - }, - }; - - mockFetch - .onFirstCall() - .returns(Promise.resolve({ json: () => Promise.resolve(mockStatus) })) - .onSecondCall() - .returns(Promise.resolve({ json: () => Promise.resolve(mockNetInfo) })) - .onThirdCall() - .resolves({ json: () => Promise.resolve(mockAbciInfo) }); - - const scope = await getPlatformScope(config); - - expect(scope.tenderdash.protocolVersion).to.equal(12); - expect(scope.tenderdash.desiredProtocolVersion).to.equal(12); - expect(scope.tenderdash.version).to.equal('1.6.0'); - expect(scope.tenderdash.latestBlockHeight).to.equal(398435); - }); - it('should return platform syncing when it is catching up', async () => { mockDetermineDockerStatus.returns(DockerStatusEnum.running); mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); From 592b88281b0b2bd4eb83037479dad4a956f0ddf2 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Wed, 15 Jul 2026 20:55:26 -0500 Subject: [PATCH 3/3] fix(dashmate): fall back when Tenderdash omits application_info Prefer live application_info.version for protocol status. When application_info is omitted (Tenderdash json omitempty if ABCIInfo fails, or older responses), fall back to node_info.protocol_version.app so status does not TypeError and report the service as error. The live field still wins when present, even if node_info is stale. Adds a regression unit test for the missing application_info case. --- .../dashmate/src/status/scopes/platform.js | 12 ++-- .../test/unit/status/scopes/platform.spec.js | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/dashmate/src/status/scopes/platform.js b/packages/dashmate/src/status/scopes/platform.js index 04b0fb3fb55..3be7baf69f0 100644 --- a/packages/dashmate/src/status/scopes/platform.js +++ b/packages/dashmate/src/status/scopes/platform.js @@ -144,10 +144,14 @@ export default function getPlatformScopeFactory( } info.version = version; - // node_info.protocol_version.app is snapshotted at Tenderdash process start and - // stays stale across in-process protocol upgrades. application_info.version is - // the live active/current app protocol version. - info.protocolVersion = parseInt(tenderdashStatus.application_info.version, 10); + // Prefer live application_info.version (current after in-process upgrades). + // Tenderdash may omit application_info entirely (json omitempty when ABCIInfo + // fails, or older builds without the field). Fall back to + // node_info.protocol_version.app only then — that snapshot can be stale, so it + // must not win when the live field is present. + const appProtocolVersion = tenderdashStatus.application_info?.version + ?? tenderdashStatus.node_info.protocol_version.app; + info.protocolVersion = parseInt(appProtocolVersion, 10); // abci_info app_version reflects the installed software's desired/supported version. info.desiredProtocolVersion = tenderdashAbciInfo.response.app_version; info.listening = listening; diff --git a/packages/dashmate/test/unit/status/scopes/platform.spec.js b/packages/dashmate/test/unit/status/scopes/platform.spec.js index 28610e56718..4055330e1e1 100644 --- a/packages/dashmate/test/unit/status/scopes/platform.spec.js +++ b/packages/dashmate/test/unit/status/scopes/platform.spec.js @@ -162,6 +162,67 @@ describe('getPlatformScopeFactory', () => { expect(scope).to.deep.equal(expectedScope); }); + it('should fall back to node_info app protocol when application_info is absent', async () => { + mockDetermineDockerStatus.returns(DockerStatusEnum.running); + mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } }); + mockRpcClient.getBlockchainInfo.returns({ + result: { + softforks: { + mn_rr: { active: true, height: 1337 }, + }, + }, + }); + mockDockerCompose.isServiceRunning.returns(true); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci status').resolves({ exitCode: 0, out: '' }); + mockDockerCompose.execCommand.withArgs(config, 'drive_abci', 'drive-abci version').resolves({ exitCode: 0, out: '1.4.1' }); + mockMNOWatchProvider.returns(Promise.resolve('OPEN')); + + // When application_info is omitted (omitempty / unavailable), status must still + // report a numeric protocolVersion from node_info.protocol_version.app. + const mockStatus = { + node_info: { + protocol_version: { + p2p: '10', + block: '14', + app: '3', + }, + version: '0', + network: 'test', + moniker: 'test', + }, + sync_info: { + catching_up: false, + latest_app_hash: 'DEADBEEF', + latest_block_height: 1, + latest_block_hash: 'DEADBEEF', + latest_block_time: 1337, + }, + }; + const mockNetInfo = { n_peers: 6, listening: true }; + const mockAbciInfo = { + response: { + version: '1.4.1', + app_version: 4, + last_block_height: 90, + last_block_app_hash: 's0CySQxgRg96DrnJ7HCsql+k/Sk4JiT3y0psCaUI3TI=', + }, + }; + + mockFetch + .onFirstCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockStatus) })) + .onSecondCall() + .returns(Promise.resolve({ json: () => Promise.resolve(mockNetInfo) })) + .onThirdCall() + .resolves({ json: () => Promise.resolve(mockAbciInfo) }); + + const scope = await getPlatformScope(config); + + expect(scope.tenderdash.serviceStatus).to.equal(ServiceStatusEnum.up); + expect(scope.tenderdash.protocolVersion).to.equal(3); + expect(scope.tenderdash.desiredProtocolVersion).to.equal(4); + }); + it('should return platform syncing when it is catching up', async () => { mockDetermineDockerStatus.returns(DockerStatusEnum.running); mockRpcClient.mnsync.withArgs('status').returns({ result: { IsSynced: true } });