From 686e995eb1b88e44c1e360480808fa359c21738f Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 14 Jun 2026 11:17:05 -0500 Subject: [PATCH 1/4] =?UTF-8?q?fix(dashmate):=20re-sync=20Drive=20ABCI=20&?= =?UTF-8?q?=20rs-dapi=20images=20on=203.0.x=20=E2=86=92=204.0.0-rc=20upgra?= =?UTF-8?q?de=20(#3889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A node already on 3.0.1 skipped the 3.0.0 migration that used to re-sync the Drive ABCI and rs-dapi docker images, and the intervening 3.0.1 / 3.0.2 / 3.1.0 migrations only touched Core / Gateway / Tenderdash. After `dashmate update` to 4.0.0-rc.x the config kept the old protocol-11 images (`dashpay/drive:3`, `dashpay/rs-dapi:3`), so the node stayed on protocol 11 and crash-looped after protocol 12 activation. Add a `4.0.0-rc.2` migration that re-syncs both image fields from the current default config (matching the 3.0.0 migration convention), and a focused regression spec that constructs the migration without the full DI container so it doesn't depend on `@dashevo/wasm-dpp` being built. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../configs/getConfigFileMigrationsFactory.js | 28 ++++ .../configFile/migrate3xTo4xRcImages.spec.js | 152 ++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js diff --git a/packages/dashmate/configs/getConfigFileMigrationsFactory.js b/packages/dashmate/configs/getConfigFileMigrationsFactory.js index da87a2b3516..29fc689b6ae 100644 --- a/packages/dashmate/configs/getConfigFileMigrationsFactory.js +++ b/packages/dashmate/configs/getConfigFileMigrationsFactory.js @@ -1520,6 +1520,34 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs) return configFile; }, + '4.0.0-rc.2': (configFile) => { + // Re-sync Drive ABCI and rs-dapi docker images to the current + // dashmate default. Pre-3.0.0 → 3.0.0 already did this in the + // 3.0.0 migration, but nodes that were already on 3.0.x skip + // that step (semver.gt filter), and the 3.0.1 / 3.0.2 / 3.1.0 + // migrations only touched Core / Gateway / Tenderdash images. + // Without this, a 3.0.x → 4.0.0-rc.x update keeps the old + // protocol-11 images (dashpay/drive:3, dashpay/rs-dapi:3) and + // the node crash-loops after protocol 12 activation (#3889). + Object.entries(configFile.configs) + .forEach(([name, options]) => { + const defaultConfig = getDefaultConfigByNameOrGroup(name, options.group); + + if (options.platform?.drive?.abci?.docker + && defaultConfig.has('platform.drive.abci.docker.image')) { + options.platform.drive.abci.docker.image = defaultConfig + .get('platform.drive.abci.docker.image'); + } + + if (options.platform?.dapi?.rsDapi?.docker + && defaultConfig.has('platform.dapi.rsDapi.docker.image')) { + options.platform.dapi.rsDapi.docker.image = defaultConfig + .get('platform.dapi.rsDapi.docker.image'); + } + }); + + return configFile; + }, }; } diff --git a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js new file mode 100644 index 00000000000..ac5d2215eee --- /dev/null +++ b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js @@ -0,0 +1,152 @@ +import path from 'path'; +import DefaultConfigs from '../../../../src/config/DefaultConfigs.js'; +import getBaseConfigFactory from '../../../../configs/defaults/getBaseConfigFactory.js'; +import getLocalConfigFactory from '../../../../configs/defaults/getLocalConfigFactory.js'; +import getTestnetConfigFactory from '../../../../configs/defaults/getTestnetConfigFactory.js'; +import getMainnetConfigFactory from '../../../../configs/defaults/getMainnetConfigFactory.js'; +import getConfigFileMigrationsFactory from '../../../../configs/getConfigFileMigrationsFactory.js'; +import migrateConfigFileFactory from '../../../../src/config/configFile/migrateConfigFileFactory.js'; + +// Regression coverage for dashpay/platform#3889. +// +// A node already on 3.0.1 skips the 3.0.0 migration (semver.gt filter) +// that used to re-sync Drive ABCI and rs-dapi images. The intervening +// 3.0.2 / 3.1.0 migrations only touched Gateway / Tenderdash, so a +// `dashmate update 3.0.x → 4.0.0-rc.x` kept the old protocol-11 +// images and the node crash-looped after protocol 12 activation. +// +// The new `4.0.0-rc.2` migration must re-sync those two image fields +// from the current default config. +describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () => { + let migrate; + let defaults; + + beforeEach(() => { + // Construct the migration directly (no DI). The full DI container + // transitively imports `@dashevo/dapi-client`, which can fail to + // load when `@dashevo/wasm-dpp` hasn't been built; the migration + // itself only needs the default-config factories. + const homeDirStub = { + joinPath: (...segments) => path.join('/tmp/dashmate-spec', ...segments), + }; + const getBaseConfig = getBaseConfigFactory(); + defaults = new DefaultConfigs([ + getBaseConfig, + getLocalConfigFactory(getBaseConfig), + getTestnetConfigFactory(homeDirStub, getBaseConfig), + getMainnetConfigFactory(homeDirStub, getBaseConfig), + ]); + const getMigrations = getConfigFileMigrationsFactory(homeDirStub, defaults); + migrate = migrateConfigFileFactory(getMigrations); + }); + + // Minimal post-3.0.x config shape carrying the stale protocol-11 + // image tags that dashmate 3.0.x shipped. Earlier migrations expect + // a much richer object, so we set fromVersion >= 3.1.0 to make the + // semver.gt filter run only the new step. + function buildStaleConfig({ network, group, customImages = {} }) { + return { + group, + network, + platform: { + enable: true, + drive: { + abci: { + docker: { + image: customImages.drive ?? 'dashpay/drive:3', + }, + }, + }, + dapi: { + rsDapi: { + docker: { + image: customImages.rsDapi ?? 'dashpay/rs-dapi:3', + }, + }, + }, + }, + }; + } + + it('re-syncs drive.abci and dapi.rsDapi images to current defaults', () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), + mainnet: buildStaleConfig({ network: 'mainnet', group: 'mainnet' }), + }, + }; + + const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); + const expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); + + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.2'); + + expect(migrated.configs.testnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + expect(migrated.configs.mainnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + expect(migrated.configFormatVersion).to.equal('4.0.0-rc.2'); + }); + + it('runs for 3.0.1 fromVersion (the version skipped by the 3.0.0 migration)', () => { + // Direct repro of #3889: nodes that were already on 3.0.1 must + // still get the image re-sync, even though 3.0.0 itself is gated + // out by `semver.gt('3.0.0', '3.0.1') === false`. + const rawConfigFile = { + configFormatVersion: '3.0.1', + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), + }, + }; + + const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); + const expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); + + const migrated = migrate(rawConfigFile, '3.0.1', '4.0.0-rc.2'); + + expect(migrated.configs.testnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + }); + + it('no-ops on configs without platform.dapi.rsDapi', () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + mainnet: { + group: 'mainnet', + network: 'mainnet', + platform: { + enable: true, + drive: { + abci: { + docker: { image: 'dashpay/drive:3' }, + }, + }, + dapi: {}, + }, + }, + }, + }; + + const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); + + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.2'); + + expect(migrated.configs.mainnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.mainnet.platform.dapi.rsDapi).to.equal(undefined); + }); +}); From a662ca68de36c20ec24f8398ee8d2c799fd3502e Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 14 Jun 2026 11:20:21 -0500 Subject: [PATCH 2/4] test(dashmate): tighten 4.0.0-rc.2 image-resync migration spec Apply review polish on the regression added in 686e995eb1: - Match the sibling 3.0.0 migration's single-line `defaultConfig.get(...)` style in the migration body. - Hoist expected default images into `beforeEach`; remove an unused `customImages` helper parameter. - Parameterise the two `fromVersion` cases (3.0.1 and 3.1.0) so both real-world entry points are exercised by one assertion shape. - Pin against the regression with `expect(...).to.not.equal('dashpay/drive:3')` (and rs-dapi:3) so the test keeps failing even if a future default happens to match the stale value again. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../configs/getConfigFileMigrationsFactory.js | 6 +- .../configFile/migrate3xTo4xRcImages.spec.js | 105 ++++++++---------- 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/packages/dashmate/configs/getConfigFileMigrationsFactory.js b/packages/dashmate/configs/getConfigFileMigrationsFactory.js index 29fc689b6ae..ac90e832a26 100644 --- a/packages/dashmate/configs/getConfigFileMigrationsFactory.js +++ b/packages/dashmate/configs/getConfigFileMigrationsFactory.js @@ -1535,14 +1535,12 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs) if (options.platform?.drive?.abci?.docker && defaultConfig.has('platform.drive.abci.docker.image')) { - options.platform.drive.abci.docker.image = defaultConfig - .get('platform.drive.abci.docker.image'); + options.platform.drive.abci.docker.image = defaultConfig.get('platform.drive.abci.docker.image'); } if (options.platform?.dapi?.rsDapi?.docker && defaultConfig.has('platform.dapi.rsDapi.docker.image')) { - options.platform.dapi.rsDapi.docker.image = defaultConfig - .get('platform.dapi.rsDapi.docker.image'); + options.platform.dapi.rsDapi.docker.image = defaultConfig.get('platform.dapi.rsDapi.docker.image'); } }); diff --git a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js index ac5d2215eee..49c7e723b44 100644 --- a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js +++ b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js @@ -11,15 +11,22 @@ import migrateConfigFileFactory from '../../../../src/config/configFile/migrateC // // A node already on 3.0.1 skips the 3.0.0 migration (semver.gt filter) // that used to re-sync Drive ABCI and rs-dapi images. The intervening -// 3.0.2 / 3.1.0 migrations only touched Gateway / Tenderdash, so a -// `dashmate update 3.0.x → 4.0.0-rc.x` kept the old protocol-11 -// images and the node crash-looped after protocol 12 activation. +// 3.0.1 / 3.0.2 / 3.1.0 migrations only touched Core / Gateway / Tenderdash +// `.docker.image` fields (3.1.0 does add `buildArgs` on the drive/rsDapi +// `docker.build` blocks, but never their image tags). The result: a +// `dashmate update 3.0.x → 4.0.0-rc.x` kept the protocol-11 images and +// the node crash-looped after protocol 12 activation. // // The new `4.0.0-rc.2` migration must re-sync those two image fields // from the current default config. describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () => { + const STALE_DRIVE = 'dashpay/drive:3'; + const STALE_RS_DAPI = 'dashpay/rs-dapi:3'; + let migrate; let defaults; + let expectedDriveImage; + let expectedRsDapiImage; beforeEach(() => { // Construct the migration directly (no DI). The full DI container @@ -38,13 +45,16 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () ]); const getMigrations = getConfigFileMigrationsFactory(homeDirStub, defaults); migrate = migrateConfigFileFactory(getMigrations); + + expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); + expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); }); // Minimal post-3.0.x config shape carrying the stale protocol-11 // image tags that dashmate 3.0.x shipped. Earlier migrations expect - // a much richer object, so we set fromVersion >= 3.1.0 to make the + // a much richer object, so we set fromVersion >= 3.0.1 to make the // semver.gt filter run only the new step. - function buildStaleConfig({ network, group, customImages = {} }) { + function buildStaleConfig({ network, group }) { return { group, network, @@ -52,71 +62,52 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () enable: true, drive: { abci: { - docker: { - image: customImages.drive ?? 'dashpay/drive:3', - }, + docker: { image: STALE_DRIVE }, }, }, dapi: { rsDapi: { - docker: { - image: customImages.rsDapi ?? 'dashpay/rs-dapi:3', - }, + docker: { image: STALE_RS_DAPI }, }, }, }, }; } - it('re-syncs drive.abci and dapi.rsDapi images to current defaults', () => { - const rawConfigFile = { - configFormatVersion: '3.1.0', - defaultConfigName: null, - defaultGroupName: null, - configs: { - testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), - mainnet: buildStaleConfig({ network: 'mainnet', group: 'mainnet' }), - }, - }; - - const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); - const expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); - - const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.2'); + // Both fromVersion variants are real-world entry points: a node could + // be on 3.0.1 (the version that originally skipped the 3.0.0 image + // re-sync — the direct #3889 repro) or on 3.1.0 (skipping it for the + // same reason). Either way the new migration must converge them. + ['3.0.1', '3.1.0'].forEach((fromVersion) => { + it(`re-syncs drive.abci and dapi.rsDapi images on ${fromVersion} → 4.0.0-rc.2`, () => { + const rawConfigFile = { + configFormatVersion: fromVersion, + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), + mainnet: buildStaleConfig({ network: 'mainnet', group: 'mainnet' }), + }, + }; - expect(migrated.configs.testnet.platform.drive.abci.docker.image) - .to.equal(expectedDriveImage); - expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) - .to.equal(expectedRsDapiImage); - expect(migrated.configs.mainnet.platform.drive.abci.docker.image) - .to.equal(expectedDriveImage); - expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image) - .to.equal(expectedRsDapiImage); - expect(migrated.configFormatVersion).to.equal('4.0.0-rc.2'); - }); + const migrated = migrate(rawConfigFile, fromVersion, '4.0.0-rc.2'); - it('runs for 3.0.1 fromVersion (the version skipped by the 3.0.0 migration)', () => { - // Direct repro of #3889: nodes that were already on 3.0.1 must - // still get the image re-sync, even though 3.0.0 itself is gated - // out by `semver.gt('3.0.0', '3.0.1') === false`. - const rawConfigFile = { - configFormatVersion: '3.0.1', - defaultConfigName: null, - defaultGroupName: null, - configs: { - testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), - }, - }; + ['testnet', 'mainnet'].forEach((name) => { + const { docker: driveDocker } = migrated.configs[name].platform.drive.abci; + const { docker: rsDapiDocker } = migrated.configs[name].platform.dapi.rsDapi; - const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); - const expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); + expect(driveDocker.image).to.equal(expectedDriveImage); + expect(rsDapiDocker.image).to.equal(expectedRsDapiImage); - const migrated = migrate(rawConfigFile, '3.0.1', '4.0.0-rc.2'); + // Pin against the specific regression so the test stays + // meaningful even if a future default happens to match + // the stale value again. + expect(driveDocker.image).to.not.equal(STALE_DRIVE); + expect(rsDapiDocker.image).to.not.equal(STALE_RS_DAPI); + }); - expect(migrated.configs.testnet.platform.drive.abci.docker.image) - .to.equal(expectedDriveImage); - expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) - .to.equal(expectedRsDapiImage); + expect(migrated.configFormatVersion).to.equal('4.0.0-rc.2'); + }); }); it('no-ops on configs without platform.dapi.rsDapi', () => { @@ -132,7 +123,7 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () enable: true, drive: { abci: { - docker: { image: 'dashpay/drive:3' }, + docker: { image: STALE_DRIVE }, }, }, dapi: {}, @@ -141,8 +132,6 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () }, }; - const expectedDriveImage = defaults.get('base').get('platform.drive.abci.docker.image'); - const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.2'); expect(migrated.configs.mainnet.platform.drive.abci.docker.image) From bce07a219c6f7ab5ea51502306150fb6e65d74f2 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 14 Jun 2026 12:06:54 -0500 Subject: [PATCH 3/4] fix(dashmate): re-key image-resync migration at 4.0.0-rc.3 and guard custom images The 4.0.0-rc.2 entry added in #3889 was both unreachable for already- stamped rc.2 configs (migrateConfigFile short-circuits when fromVersion === toVersion) and unconditional: it overwrote every Drive ABCI / rs-dapi image with the current default, clobbering private forks and vendor-patched builds. Re-key the migration at 4.0.0-rc.3 (the next release; same flow as the 3.0.2 Envoy CVE - #3794 added the migration, #3796 cut the release) and only rewrite the stale dashmate-shipped tags (dashpay/drive:3, dashpay/rs-dapi:3, including their -dev/-rc series), mirroring the 3.0.2 Envoy CVE migration's predicate style. Custom images are preserved. The migration fires for both 3.0.x -> 4.0.0-rc.x upgrades (the loop runs every migration with key > fromVersion) and 4.0.0-rc.2 -> 4.0.0-rc.3 once a chore(release) bumps the package version. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../configs/getConfigFileMigrationsFactory.js | 57 ++++- .../configFile/migrate3xTo4xRcImages.spec.js | 226 +++++++++++++++--- 2 files changed, 242 insertions(+), 41 deletions(-) diff --git a/packages/dashmate/configs/getConfigFileMigrationsFactory.js b/packages/dashmate/configs/getConfigFileMigrationsFactory.js index ac90e832a26..56466658b3e 100644 --- a/packages/dashmate/configs/getConfigFileMigrationsFactory.js +++ b/packages/dashmate/configs/getConfigFileMigrationsFactory.js @@ -1520,27 +1520,58 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs) return configFile; }, - '4.0.0-rc.2': (configFile) => { - // Re-sync Drive ABCI and rs-dapi docker images to the current - // dashmate default. Pre-3.0.0 → 3.0.0 already did this in the - // 3.0.0 migration, but nodes that were already on 3.0.x skip - // that step (semver.gt filter), and the 3.0.1 / 3.0.2 / 3.1.0 - // migrations only touched Core / Gateway / Tenderdash images. - // Without this, a 3.0.x → 4.0.0-rc.x update keeps the old - // protocol-11 images (dashpay/drive:3, dashpay/rs-dapi:3) and - // the node crash-loops after protocol 12 activation (#3889). + '4.0.0-rc.3': (configFile) => { + // Re-sync Drive ABCI and rs-dapi docker images that dashmate + // 3.0.x → 4.0.0-rc.2 left at the protocol-11 default tag + // (`dashpay/drive:3`, `dashpay/rs-dapi:3`), causing nodes to + // crash-loop after protocol 12 activation (#3889). + // + // Pre-3.0.0 → 3.0.0 already re-synced these images, but nodes + // already on 3.0.x skipped the 3.0.0 step (semver.gt filter), + // and the 3.0.1 / 3.0.2 / 3.1.0 migrations only touched + // Core / Gateway / Tenderdash images. + // + // The original attempt (#3889) keyed this migration at the + // same version it shipped with (4.0.0-rc.2), so + // already-stamped 4.0.0-rc.2 configs short-circuit in + // migrateConfigFile (fromVersion === toVersion). Re-keying at + // the next release ensures the fix fires for both: + // • 3.0.x → 4.0.0-rc.x upgrades (the loop runs every + // migration with key > fromVersion, regardless of + // toVersion), and + // • 4.0.0-rc.2 → 4.0.0-rc.3 once a chore(release) bumps + // packages/dashmate/package.json (the convention; cf. + // #3794 + #3796 for the 3.0.2 Envoy CVE). + // + // Only rewrite the stale dashmate-shipped tags — same style + // as the 3.0.2 Envoy CVE migration. A custom / private / + // manually-corrected image (private fork, vendor-patched + // build, `:latest`, etc.) is left untouched. + const isStaleDriveImage = (image) => ( + typeof image === 'string' + && /^dashpay\/drive:3(?:-(?:dev|rc))?$/.test(image) + ); + const isStaleRsDapiImage = (image) => ( + typeof image === 'string' + && /^dashpay\/rs-dapi:3(?:-(?:dev|rc))?$/.test(image) + ); + Object.entries(configFile.configs) .forEach(([name, options]) => { const defaultConfig = getDefaultConfigByNameOrGroup(name, options.group); - if (options.platform?.drive?.abci?.docker + const driveDocker = options.platform?.drive?.abci?.docker; + if (driveDocker + && isStaleDriveImage(driveDocker.image) && defaultConfig.has('platform.drive.abci.docker.image')) { - options.platform.drive.abci.docker.image = defaultConfig.get('platform.drive.abci.docker.image'); + driveDocker.image = defaultConfig.get('platform.drive.abci.docker.image'); } - if (options.platform?.dapi?.rsDapi?.docker + const rsDapiDocker = options.platform?.dapi?.rsDapi?.docker; + if (rsDapiDocker + && isStaleRsDapiImage(rsDapiDocker.image) && defaultConfig.has('platform.dapi.rsDapi.docker.image')) { - options.platform.dapi.rsDapi.docker.image = defaultConfig.get('platform.dapi.rsDapi.docker.image'); + rsDapiDocker.image = defaultConfig.get('platform.dapi.rsDapi.docker.image'); } }); diff --git a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js index 49c7e723b44..c1dd2fb938c 100644 --- a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js +++ b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js @@ -7,24 +7,30 @@ import getMainnetConfigFactory from '../../../../configs/defaults/getMainnetConf import getConfigFileMigrationsFactory from '../../../../configs/getConfigFileMigrationsFactory.js'; import migrateConfigFileFactory from '../../../../src/config/configFile/migrateConfigFileFactory.js'; -// Regression coverage for dashpay/platform#3889. +// Regression coverage for dashpay/platform#3889 and its rework. // // A node already on 3.0.1 skips the 3.0.0 migration (semver.gt filter) // that used to re-sync Drive ABCI and rs-dapi images. The intervening // 3.0.1 / 3.0.2 / 3.1.0 migrations only touched Core / Gateway / Tenderdash -// `.docker.image` fields (3.1.0 does add `buildArgs` on the drive/rsDapi -// `docker.build` blocks, but never their image tags). The result: a -// `dashmate update 3.0.x → 4.0.0-rc.x` kept the protocol-11 images and -// the node crash-looped after protocol 12 activation. +// `.docker.image` fields. The result: a `dashmate update 3.0.x → 4.0.0-rc.x` +// kept the protocol-11 images (`dashpay/drive:3`, `dashpay/rs-dapi:3`) +// and the node crash-looped after protocol 12 activation. // -// The new `4.0.0-rc.2` migration must re-sync those two image fields -// from the current default config. -describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () => { +// The original fix keyed the migration at 4.0.0-rc.2 — the same version +// dashmate already shipped — so any node already stamped +// configFormatVersion: "4.0.0-rc.2" short-circuited in migrateConfigFile +// (fromVersion === toVersion) and never ran the migration. The rework +// re-keys it at 4.0.0-rc.3 (the next release) and adds a stale-shipped- +// image predicate to preserve customised images. +describe('migration 4.0.0-rc.3: re-sync Drive ABCI & rs-dapi images (#3889)', () => { const STALE_DRIVE = 'dashpay/drive:3'; + const STALE_DRIVE_DEV = 'dashpay/drive:3-dev'; const STALE_RS_DAPI = 'dashpay/rs-dapi:3'; + const STALE_RS_DAPI_DEV = 'dashpay/rs-dapi:3-dev'; + const CUSTOM_DRIVE = 'private-registry.internal/drive:patched-v3'; + const CUSTOM_RS_DAPI = 'my-org/rs-dapi:fork-3'; let migrate; - let defaults; let expectedDriveImage; let expectedRsDapiImage; @@ -37,7 +43,7 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () joinPath: (...segments) => path.join('/tmp/dashmate-spec', ...segments), }; const getBaseConfig = getBaseConfigFactory(); - defaults = new DefaultConfigs([ + const defaults = new DefaultConfigs([ getBaseConfig, getLocalConfigFactory(getBaseConfig), getTestnetConfigFactory(homeDirStub, getBaseConfig), @@ -50,11 +56,9 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () expectedRsDapiImage = defaults.get('base').get('platform.dapi.rsDapi.docker.image'); }); - // Minimal post-3.0.x config shape carrying the stale protocol-11 - // image tags that dashmate 3.0.x shipped. Earlier migrations expect - // a much richer object, so we set fromVersion >= 3.0.1 to make the - // semver.gt filter run only the new step. - function buildStaleConfig({ network, group }) { + function buildConfig({ + network, group, driveImage, rsDapiImage, + }) { return { group, network, @@ -62,35 +66,53 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () enable: true, drive: { abci: { - docker: { image: STALE_DRIVE }, + docker: { image: driveImage }, }, }, dapi: { rsDapi: { - docker: { image: STALE_RS_DAPI }, + docker: { image: rsDapiImage }, }, }, }, }; } - // Both fromVersion variants are real-world entry points: a node could - // be on 3.0.1 (the version that originally skipped the 3.0.0 image - // re-sync — the direct #3889 repro) or on 3.1.0 (skipping it for the - // same reason). Either way the new migration must converge them. - ['3.0.1', '3.1.0'].forEach((fromVersion) => { - it(`re-syncs drive.abci and dapi.rsDapi images on ${fromVersion} → 4.0.0-rc.2`, () => { + // 3.0.x and 3.1.0 are real-world entry points: a node could be on 3.0.1 + // (the version that originally skipped the 3.0.0 image re-sync — the + // direct #3889 repro) or on 3.1.0 (skipping it for the same reason). + // Either way the new migration must converge them — and crucially, the + // loop runs every migration with key > fromVersion, so this fires even + // when toVersion is still 4.0.0-rc.2 (the version currently in + // packages/dashmate/package.json). + [ + { fromVersion: '3.0.1', toVersion: '4.0.0-rc.2' }, + { fromVersion: '3.1.0', toVersion: '4.0.0-rc.2' }, + { fromVersion: '3.0.1', toVersion: '4.0.0-rc.3' }, + { fromVersion: '3.1.0', toVersion: '4.0.0-rc.3' }, + ].forEach(({ fromVersion, toVersion }) => { + it(`re-syncs stale drive.abci and dapi.rsDapi images on ${fromVersion} → ${toVersion}`, () => { const rawConfigFile = { configFormatVersion: fromVersion, defaultConfigName: null, defaultGroupName: null, configs: { - testnet: buildStaleConfig({ network: 'testnet', group: 'testnet' }), - mainnet: buildStaleConfig({ network: 'mainnet', group: 'mainnet' }), + testnet: buildConfig({ + network: 'testnet', + group: 'testnet', + driveImage: STALE_DRIVE, + rsDapiImage: STALE_RS_DAPI, + }), + mainnet: buildConfig({ + network: 'mainnet', + group: 'mainnet', + driveImage: STALE_DRIVE, + rsDapiImage: STALE_RS_DAPI, + }), }, }; - const migrated = migrate(rawConfigFile, fromVersion, '4.0.0-rc.2'); + const migrated = migrate(rawConfigFile, fromVersion, toVersion); ['testnet', 'mainnet'].forEach((name) => { const { docker: driveDocker } = migrated.configs[name].platform.drive.abci; @@ -106,10 +128,158 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () expect(rsDapiDocker.image).to.not.equal(STALE_RS_DAPI); }); - expect(migrated.configFormatVersion).to.equal('4.0.0-rc.2'); + expect(migrated.configFormatVersion).to.equal(toVersion); }); }); + it('re-syncs the dev-series stale tags too (dashpay/drive:3-dev, dashpay/rs-dapi:3-dev)', () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildConfig({ + network: 'testnet', + group: 'testnet', + driveImage: STALE_DRIVE_DEV, + rsDapiImage: STALE_RS_DAPI_DEV, + }), + }, + }; + + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); + + expect(migrated.configs.testnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + }); + + // The exact case the rework targets: a node that already upgraded to + // 4.0.0-rc.2 binary kept the stale dashpay/drive:3 / dashpay/rs-dapi:3 + // tags and got configFormatVersion: "4.0.0-rc.2" stamped on disk. + // After a chore(release) bumps packages/dashmate/package.json to + // 4.0.0-rc.3, the next dashmate load enters migrateConfigFile with + // fromVersion=4.0.0-rc.2 / toVersion=4.0.0-rc.3, and the new + // migration finally runs (the original 4.0.0-rc.2 key would have + // been short-circuited by semver.gt('4.0.0-rc.2', '4.0.0-rc.2')). + it('re-syncs already-stamped 4.0.0-rc.2 configs on the rc.3 release bump', () => { + const rawConfigFile = { + configFormatVersion: '4.0.0-rc.2', + defaultConfigName: null, + defaultGroupName: null, + configs: { + mainnet: buildConfig({ + network: 'mainnet', + group: 'mainnet', + driveImage: STALE_DRIVE, + rsDapiImage: STALE_RS_DAPI, + }), + }, + }; + + const migrated = migrate(rawConfigFile, '4.0.0-rc.2', '4.0.0-rc.3'); + + expect(migrated.configs.mainnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + expect(migrated.configFormatVersion).to.equal('4.0.0-rc.3'); + }); + + // Documents the bound of what migration alone can fix: as long as + // packages/dashmate/package.json sits at 4.0.0-rc.2, a config already + // stamped 4.0.0-rc.2 short-circuits in migrateConfigFile before any + // entry can run. This is the structural limitation that motivated + // re-keying the migration at the *next* release rather than reusing + // 4.0.0-rc.2 — and why the full rollout depends on a follow-up + // chore(release) PR bumping the package version (same flow as the + // 3.0.2 Envoy CVE: #3794 added the migration, #3796 cut the release). + it('cannot reach a 4.0.0-rc.2 stale config while toVersion is still 4.0.0-rc.2 (release-bump prerequisite)', () => { + const rawConfigFile = { + configFormatVersion: '4.0.0-rc.2', + defaultConfigName: null, + defaultGroupName: null, + configs: { + mainnet: buildConfig({ + network: 'mainnet', + group: 'mainnet', + driveImage: STALE_DRIVE, + rsDapiImage: STALE_RS_DAPI, + }), + }, + }; + + const migrated = migrate(rawConfigFile, '4.0.0-rc.2', '4.0.0-rc.2'); + + // No migration runs — config returned untouched. + expect(migrated.configs.mainnet.platform.drive.abci.docker.image).to.equal(STALE_DRIVE); + expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image).to.equal(STALE_RS_DAPI); + expect(migrated.configFormatVersion).to.equal('4.0.0-rc.2'); + }); + + // Custom-image preservation. The original 4.0.0-rc.2 migration + // unconditionally overwrote every drive/rsDapi image with the + // current default, clobbering private forks, vendor-patched builds, + // and `:latest` pins. The rework matches only the stale shipped + // defaults — same style as the 3.0.2 Envoy CVE migration that + // rewrote only `dashpay/envoy:1.30.*`. + it('preserves custom Drive / rs-dapi images', () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + mainnet: buildConfig({ + network: 'mainnet', + group: 'mainnet', + driveImage: CUSTOM_DRIVE, + rsDapiImage: CUSTOM_RS_DAPI, + }), + }, + }; + + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); + + expect(migrated.configs.mainnet.platform.drive.abci.docker.image).to.equal(CUSTOM_DRIVE); + expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image).to.equal(CUSTOM_RS_DAPI); + }); + + // Mixed config: one config has stale dashmate defaults (gets rewritten), + // a sibling has a custom image (preserved). Exercises the same predicate + // pattern as the 3.0.2 Envoy CVE migration end-to-end. + it('only rewrites the stale config when siblings have custom images', () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildConfig({ + network: 'testnet', + group: 'testnet', + driveImage: STALE_DRIVE, + rsDapiImage: STALE_RS_DAPI, + }), + mainnet: buildConfig({ + network: 'mainnet', + group: 'mainnet', + driveImage: CUSTOM_DRIVE, + rsDapiImage: CUSTOM_RS_DAPI, + }), + }, + }; + + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); + + expect(migrated.configs.testnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + + expect(migrated.configs.mainnet.platform.drive.abci.docker.image).to.equal(CUSTOM_DRIVE); + expect(migrated.configs.mainnet.platform.dapi.rsDapi.docker.image).to.equal(CUSTOM_RS_DAPI); + }); + it('no-ops on configs without platform.dapi.rsDapi', () => { const rawConfigFile = { configFormatVersion: '3.1.0', @@ -132,7 +302,7 @@ describe('migration 4.0.0-rc.2: re-sync Drive ABCI & rs-dapi images (#3889)', () }, }; - const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.2'); + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); expect(migrated.configs.mainnet.platform.drive.abci.docker.image) .to.equal(expectedDriveImage); From e11795d3f60e724f317993f5980b748e264a1ad5 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sun, 14 Jun 2026 12:11:14 -0500 Subject: [PATCH 4/4] fix(dashmate): also re-sync :3-hotfix Drive ABCI & rs-dapi images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous predicate alternation only matched the `-dev` and `-rc` prerelease label series for `dashpay/drive:3` / `dashpay/rs-dapi:3`, missing the `-hotfix` tag shipped by 3.0.1-hotfix.{1..4} (#3020/#3044/ #3055/#3060) and 3.1.0-hotfix.1. A node sitting on `:3-hotfix` would still crash-loop after protocol 12 activation — the exact failure mode the migration was meant to prevent. Extend the alternation and add a regression case. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../configs/getConfigFileMigrationsFactory.js | 13 ++++- .../configFile/migrate3xTo4xRcImages.spec.js | 53 ++++++++++++------- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/packages/dashmate/configs/getConfigFileMigrationsFactory.js b/packages/dashmate/configs/getConfigFileMigrationsFactory.js index 56466658b3e..ff5363e7ca2 100644 --- a/packages/dashmate/configs/getConfigFileMigrationsFactory.js +++ b/packages/dashmate/configs/getConfigFileMigrationsFactory.js @@ -1547,13 +1547,22 @@ export default function getConfigFileMigrationsFactory(homeDir, defaultConfigs) // as the 3.0.2 Envoy CVE migration. A custom / private / // manually-corrected image (private fork, vendor-patched // build, `:latest`, etc.) is left untouched. + // The 3.x line shipped four prerelease label series: + // `3.0.0-dev.X` / `3.1.0-dev.X` → `:3-dev` + // `3.0.0-rc.X` → `:3-rc` + // `3.0.1-hotfix.{1..4}` / + // `3.1.0-hotfix.1` → `:3-hotfix` + // stable 3.0.0 / 3.0.1 / 3.0.2 / 3.1.0 → `:3` + // All of them resolve to a protocol-11 image and crash-loop + // after protocol 12 activation. Keep the alternation tight so + // custom tags (`:3-mycorp`, `:3.0.0`, `:latest`, etc.) survive. const isStaleDriveImage = (image) => ( typeof image === 'string' - && /^dashpay\/drive:3(?:-(?:dev|rc))?$/.test(image) + && /^dashpay\/drive:3(?:-(?:dev|rc|hotfix))?$/.test(image) ); const isStaleRsDapiImage = (image) => ( typeof image === 'string' - && /^dashpay\/rs-dapi:3(?:-(?:dev|rc))?$/.test(image) + && /^dashpay\/rs-dapi:3(?:-(?:dev|rc|hotfix))?$/.test(image) ); Object.entries(configFile.configs) diff --git a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js index c1dd2fb938c..5484a7e5c21 100644 --- a/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js +++ b/packages/dashmate/test/unit/config/configFile/migrate3xTo4xRcImages.spec.js @@ -25,8 +25,12 @@ import migrateConfigFileFactory from '../../../../src/config/configFile/migrateC describe('migration 4.0.0-rc.3: re-sync Drive ABCI & rs-dapi images (#3889)', () => { const STALE_DRIVE = 'dashpay/drive:3'; const STALE_DRIVE_DEV = 'dashpay/drive:3-dev'; + const STALE_DRIVE_RC = 'dashpay/drive:3-rc'; + const STALE_DRIVE_HOTFIX = 'dashpay/drive:3-hotfix'; const STALE_RS_DAPI = 'dashpay/rs-dapi:3'; const STALE_RS_DAPI_DEV = 'dashpay/rs-dapi:3-dev'; + const STALE_RS_DAPI_RC = 'dashpay/rs-dapi:3-rc'; + const STALE_RS_DAPI_HOTFIX = 'dashpay/rs-dapi:3-hotfix'; const CUSTOM_DRIVE = 'private-registry.internal/drive:patched-v3'; const CUSTOM_RS_DAPI = 'my-org/rs-dapi:fork-3'; @@ -132,27 +136,38 @@ describe('migration 4.0.0-rc.3: re-sync Drive ABCI & rs-dapi images (#3889)', () }); }); - it('re-syncs the dev-series stale tags too (dashpay/drive:3-dev, dashpay/rs-dapi:3-dev)', () => { - const rawConfigFile = { - configFormatVersion: '3.1.0', - defaultConfigName: null, - defaultGroupName: null, - configs: { - testnet: buildConfig({ - network: 'testnet', - group: 'testnet', - driveImage: STALE_DRIVE_DEV, - rsDapiImage: STALE_RS_DAPI_DEV, - }), - }, - }; + // All the prerelease label series the 3.x line shipped derive from + // semver.prerelease(version)[0] in getBaseConfigFactory.js, producing + // these stale tags. The hotfix variant in particular came from + // 3.0.1-hotfix.{1..4} (#3020/#3044/#3055/#3060) and 3.1.0-hotfix.1 — + // an early oversight in the predicate missed it. + [ + { label: 'dev-series', drive: STALE_DRIVE_DEV, rsDapi: STALE_RS_DAPI_DEV }, + { label: 'rc-series', drive: STALE_DRIVE_RC, rsDapi: STALE_RS_DAPI_RC }, + { label: 'hotfix-series', drive: STALE_DRIVE_HOTFIX, rsDapi: STALE_RS_DAPI_HOTFIX }, + ].forEach(({ label, drive, rsDapi }) => { + it(`re-syncs the ${label} stale tags (${drive}, ${rsDapi})`, () => { + const rawConfigFile = { + configFormatVersion: '3.1.0', + defaultConfigName: null, + defaultGroupName: null, + configs: { + testnet: buildConfig({ + network: 'testnet', + group: 'testnet', + driveImage: drive, + rsDapiImage: rsDapi, + }), + }, + }; - const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); + const migrated = migrate(rawConfigFile, '3.1.0', '4.0.0-rc.3'); - expect(migrated.configs.testnet.platform.drive.abci.docker.image) - .to.equal(expectedDriveImage); - expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) - .to.equal(expectedRsDapiImage); + expect(migrated.configs.testnet.platform.drive.abci.docker.image) + .to.equal(expectedDriveImage); + expect(migrated.configs.testnet.platform.dapi.rsDapi.docker.image) + .to.equal(expectedRsDapiImage); + }); }); // The exact case the rework targets: a node that already upgraded to