diff --git a/README.md b/README.md index 535694c..539926e 100644 --- a/README.md +++ b/README.md @@ -32,43 +32,37 @@ git clone https://github.com/base/task-signing-tool.git ### Expected Directory Layout -Place this repository at the root of your task repository. Network folders (e.g., `mainnet`, `sepolia`, `zeronet`) must live alongside it, and each task must be a date-prefixed folder inside a network folder. +Place this repository at the root of your task repository. Tasks live under `active/evm/tasks/`, each a date-prefixed folder (`YYYY-MM-DD-task-name`) holding a per-network `config//` directory (Foundry project + validation configs) and a per-network `signatures//` directory. ```12:40:root-of-your-task-repo contract-deployments/ # your task repo root (example) ├─ task-signing-tool/ # this repo cloned here │ ├─ src/ │ └─ ... -├─ mainnet/ # network directory -│ ├─ 2025-06-04-upgrade-foo/ # task directory (YYYY-MM-DD-task-name) -│ │ ├─ README.md # optional, used for status parsing -│ │ ├─ validations/ -│ │ │ ├─ base-sc.json # config for "Base SC" user type -│ │ │ ├─ coinbase.json # config for "Coinbase" user type -│ │ │ └─ op.json # config for "OP" user type -│ │ └─ foundry-project/ # directory where you run Foundry scripts -│ │ └─ ... -│ ├─ 2025-07-12-upgrade-bar/ -│ │ └─ ... -│ └─ signatures/ # signatures directory (separate from tasks) -│ ├─ 2025-06-04-upgrade-foo/ # matches task directory name -│ │ ├─ creator-signature.json -│ │ ├─ base-facilitator-signature.json -│ │ └─ base-sc-facilitator-signature.json -│ └─ 2025-07-12-upgrade-bar/ -│ └─ ... -├─ sepolia/ -│ ├─ 2025-05-10-upgrade-baz/ -│ │ └─ ... -│ └─ signatures/ -│ └─ 2025-05-10-upgrade-baz/ -│ └─ ... -└─ zeronet/ - ├─ 2025-08-01-upgrade-qux/ - │ └─ ... - └─ signatures/ - └─ 2025-08-01-upgrade-qux/ - └─ ... +└─ active/ + └─ evm/ + └─ tasks/ + ├─ 2025-06-04-upgrade-foo/ # task directory (YYYY-MM-DD-task-name) + │ ├─ config/ + │ │ ├─ mainnet/ # per-network config + Foundry project + │ │ │ ├─ README.md # optional, used for status parsing + │ │ │ ├─ validations/ + │ │ │ │ ├─ base-sc.json # config for "Base SC" user type + │ │ │ │ ├─ coinbase.json # config for "Coinbase" user type + │ │ │ │ └─ op.json # config for "OP" user type + │ │ │ ├─ script/ # Foundry scripts + │ │ │ └─ lib/ # Foundry dependencies + │ │ └─ sepolia/ + │ │ └─ ... + │ └─ signatures/ # signatures (separate from config, per-network) + │ ├─ mainnet/ + │ │ ├─ creator-signature.json + │ │ ├─ base-facilitator-signature.json + │ │ └─ base-sc-facilitator-signature.json + │ └─ sepolia/ + │ └─ ... + └─ 2025-07-12-upgrade-bar/ + └─ ... ``` Key requirements and notes: @@ -79,13 +73,13 @@ Key requirements and notes: - "Base SC" → `base-sc.json` - "Coinbase" → `coinbase.json` - "OP" → `op.json` -- **Task origin signatures**: By default, tasks require three signature files stored in `/signatures//`: `creator-signature.json`, `base-facilitator-signature.json`, and `base-sc-facilitator-signature.json`. Signatures are stored separately from task directories to avoid changing the tarball content. Use the `genTaskOriginSig.ts` script to generate these. Set `skipTaskOriginValidation: true` in the validation config to opt out. -- **Script execution**: The tool executes Foundry from the task directory root (`//`). Ensure your Foundry project or script context is available under that path; the tool will run the `cmd` specified in your validation config. Temporary outputs like `temp-script-output.txt` will be written there. -- **Optional README parsing**: If `//README.md` exists, the tool may parse it to display status and execution links. +- **Task origin signatures**: By default, tasks require three signature files stored per-network in `tasks//signatures//`: `creator-signature.json`, `base-facilitator-signature.json`, and `base-sc-facilitator-signature.json`. Signatures are stored separately from the config directory (which is what the tarball is signed over) so signing does not change the signed content. Use the `genTaskOriginSig.ts` script to generate these. Set `skipTaskOriginValidation: true` in the validation config to opt out. +- **Script execution**: The tool executes Foundry from `active/evm`. Ensure your Foundry project or script context is available under the per-network config directory (`tasks//config//`); the tool will run the `cmd` specified in your validation config. Temporary outputs like `temp-script-output.txt` will be written there. +- **Optional README parsing**: If `tasks//config//README.md` exists, the tool may parse it to display status and execution links. ### Task README structure -When present, each task's `README.md` is parsed to populate the UI. Place it at `//README.md` and follow these rules: +When present, each task's `README.md` is parsed to populate the UI. Place it at `active/evm/tasks//config//README.md` and follow these rules: - **Status line (required for status display)**: Include a line containing `Status:` within the first 20 lines. Recognized values are `PENDING`, `READY TO SIGN`, and `EXECUTED` (case-insensitive). If `EXECUTED` is not present and `READY TO SIGN` is not present, the status is treated as `PENDING`. Supported formats: - **Single-line with link (any one of these)**: @@ -156,7 +150,7 @@ Executed upgrade; links include both onchain transaction and governance proposal ### Validation file structure -Validation configs live under each task directory at `//validations/` and are selected by user type: +Validation configs live under each task directory at `active/evm/tasks//config//validations/` and are selected by user type: - `base-sc.json` for **Base SC** - `coinbase.json` for **Coinbase** @@ -196,7 +190,7 @@ Notes: - Sorting is not required; the tool sorts by address and storage slot for comparison. - The tool reads `rpcUrl` and `ledgerId` directly from this file. -- When task origin validation is enabled, three signature files must exist in `/signatures//` (see **Task Origin Signing** below). +- When task origin validation is enabled, three signature files must exist in `tasks//signatures//` (see **Task Origin Signing** below). Minimal example (`validations/base-sc.json`): @@ -274,9 +268,9 @@ General usage (tsx): npm ci npx tsx scripts/genValidationFile.ts \ --rpc-url https://mainnet.example \ - --workdir / \ + --workdir active/evm \ --forge-cmd "forge script : --sig 'run()' --sender 0xabc..." \ - --out //validations/.json + --out active/evm/tasks//config//validations/.json ``` Alternative (bun): @@ -286,9 +280,9 @@ Alternative (bun): npm ci bun run scripts/genValidationFile.ts \ --rpc-url https://mainnet.example \ - --workdir / \ + --workdir active/evm \ --forge-cmd "forge script : --sig 'run()' --sender 0xabc..." \ - --out //validations/.json + --out active/evm/tasks//config//validations/.json ``` Example from a Makefile (variables expanded in your environment): @@ -297,15 +291,15 @@ Example from a Makefile (variables expanded in your environment): cd "$SIGNER_TOOL_PATH" && \ npm ci && \ bun run scripts/genValidationFile.ts --rpc-url "$L1_RPC_URL" \ - --workdir .. \ + --workdir ../active/evm \ --forge-cmd 'forge script --rpc-url "$L1_RPC_URL" SwapOwner --sig "sign()" --sender "$SENDER"' \ - --out ../validations/test.json + --out ../active/evm/tasks//config//validations/test.json ``` Notes: - Quote the entire `--forge-cmd` so that inner quotes for `--sig` are preserved by your shell. On macOS/Linux, prefer single quotes around the whole command and double quotes inside for signatures/addresses. -- `--workdir` typically points to the task directory (e.g., `mainnet/2025-06-04-upgrade-foo`). If you keep this repo inside the task repo root, `..` will refer to the task directory when running from `task-signing-tool/`. +- `--workdir` points to the forge script root, `active/evm`. If you keep this repo inside the task repo root, `../active/evm` refers to it when running from `task-signing-tool/`. - If `--out` is omitted, the JSON is printed to stdout. ### Task Origin Signing @@ -314,7 +308,7 @@ Use `scripts/genTaskOriginSig.ts` to sign task folders for origin validation. Ta #### Signature files -When task origin validation is enabled, three signature files must exist in `/signatures//`: +When task origin validation is enabled, three signature files must exist in `tasks//signatures//`: - **Task Creator**: `creator-signature.json` — common name is the email of the task author (e.g., `alice@example.com`) - **Base Facilitator**: `base-facilitator-signature.json` — common name is `base-facilitators` (group) @@ -322,7 +316,7 @@ When task origin validation is enabled, three signature files must exist in `/` directory, separate from the `config//` directory that the tarball is signed over, to ensure the signed content remains unchanged after signing. #### Requirements @@ -359,8 +353,8 @@ npx tsx scripts/genTaskOriginSig.ts --help ```bash npm ci npx tsx scripts/genTaskOriginSig.ts sign \ - --task-folder // \ - --signature-path //signatures/ + --task-folder /tasks//config/ \ + --signature-path /tasks//signatures/ ``` **Sign as Base facilitator:** @@ -368,8 +362,8 @@ npx tsx scripts/genTaskOriginSig.ts sign \ ```bash npm ci npx tsx scripts/genTaskOriginSig.ts sign \ - --task-folder // \ - --signature-path //signatures/ \ + --task-folder /tasks//config/ \ + --signature-path /tasks//signatures/ \ --facilitator base ``` @@ -378,8 +372,8 @@ npx tsx scripts/genTaskOriginSig.ts sign \ ```bash npm ci npx tsx scripts/genTaskOriginSig.ts verify \ - --task-folder // \ - --signature-path //signatures/ \ + --task-folder /tasks//config/ \ + --signature-path /tasks//signatures/ \ --common-name alice@example.com ``` @@ -423,12 +417,12 @@ When you enable `--estimate-l2-gas`: ```bash npx tsx scripts/genValidationFile.ts \ --rpc-url https://mainnet.example \ - --workdir mainnet/2025-06-04-my-l2-deposit \ + --workdir active/evm \ --forge-cmd "forge script script/MyDeposit.s.sol:MyDeposit --sig 'run()' --sender 0xabc --json" \ --estimate-l2-gas \ --l2-rpc-url https://base-mainnet.example \ --l2-gas-buffer 25 \ - --out validations/base-sc.json + --out active/evm/tasks/2025-06-04-my-l2-deposit/config//validations/base-sc.json ``` #### Validation File Output diff --git a/__tests__/fixtures/modified-task/README.md b/__tests__/fixtures/modified-task/config/chain1/README.md similarity index 100% rename from __tests__/fixtures/modified-task/README.md rename to __tests__/fixtures/modified-task/config/chain1/README.md diff --git a/__tests__/fixtures/modified-task/script/Test.s.sol b/__tests__/fixtures/modified-task/config/chain1/script/Test.s.sol similarity index 100% rename from __tests__/fixtures/modified-task/script/Test.s.sol rename to __tests__/fixtures/modified-task/config/chain1/script/Test.s.sol diff --git a/__tests__/fixtures/valid-task/README.md b/__tests__/fixtures/valid-task/config/chain1/README.md similarity index 100% rename from __tests__/fixtures/valid-task/README.md rename to __tests__/fixtures/valid-task/config/chain1/README.md diff --git a/__tests__/fixtures/valid-task/lib/dep/TestLib.sol b/__tests__/fixtures/valid-task/config/chain1/lib/dep/TestLib.sol similarity index 100% rename from __tests__/fixtures/valid-task/lib/dep/TestLib.sol rename to __tests__/fixtures/valid-task/config/chain1/lib/dep/TestLib.sol diff --git a/__tests__/fixtures/valid-task/script/Test.s.sol b/__tests__/fixtures/valid-task/config/chain1/script/Test.s.sol similarity index 100% rename from __tests__/fixtures/valid-task/script/Test.s.sol rename to __tests__/fixtures/valid-task/config/chain1/script/Test.s.sol diff --git a/__tests__/genTaskOriginSig.test.ts b/__tests__/genTaskOriginSig.test.ts index a3d2b84..a72410b 100644 --- a/__tests__/genTaskOriginSig.test.ts +++ b/__tests__/genTaskOriginSig.test.ts @@ -18,11 +18,12 @@ import { resolveSignatureHash, FacilitatorType, } from '../scripts/genTaskOriginSig'; +import { createDeterministicTarball } from '../src/lib/task-origin-validate'; // Fixture paths const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); -const VALID_TASK_FOLDER = path.join(FIXTURES_DIR, 'valid-task'); +const VALID_TASK_FOLDER = path.join(FIXTURES_DIR, 'valid-task', 'config', 'chain1'); const VALID_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/valid'); const INVALID_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/invalid'); const MISSING_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/missing'); @@ -295,13 +296,10 @@ describe('signTaskWithCert', () => { expect(bundle.messageSignature.messageDigest.algorithm).toBe('SHA2_256'); // RSA key -> SHA-256 expect(bundle.verificationMaterial.x509CertificateChain.certificates[0].rawBytes).toBeTruthy(); - const tarballPath = path.resolve(process.cwd(), 'task.tar'); - const tarball = await fsp.readFile(tarballPath).catch(() => null); - if (tarball) { - const expectedDigest = createHash('sha256').update(new Uint8Array(tarball)).digest('base64'); - expect(bundle.messageSignature.messageDigest.digest).toBe(expectedDigest); - await fsp.unlink(tarballPath).catch(() => {}); - } + const tarballPath = await createDeterministicTarball(taskFolder); + const tarball = await fsp.readFile(tarballPath); + const expectedDigest = createHash('sha256').update(new Uint8Array(tarball)).digest('base64'); + expect(bundle.messageSignature.messageDigest.digest).toBe(expectedDigest); }); }); diff --git a/__tests__/task-origin-validate.test.ts b/__tests__/task-origin-validate.test.ts index b8f6b11..dde6ba7 100644 --- a/__tests__/task-origin-validate.test.ts +++ b/__tests__/task-origin-validate.test.ts @@ -14,8 +14,10 @@ import type { TaskOriginRole } from '../src/lib/types'; // Fixture paths const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURES_DIR = path.resolve(__dirname, 'fixtures'); -const VALID_TASK_FOLDER = path.join(FIXTURES_DIR, 'valid-task'); -const MODIFIED_TASK_FOLDER = path.join(FIXTURES_DIR, 'modified-task'); +// The tarball is scoped to a single network config dir; cache/ and out/ build +// artifacts live at the task root, outside this leaf, so they are never signed. +const VALID_TASK_FOLDER = path.join(FIXTURES_DIR, 'valid-task', 'config', 'chain1'); +const MODIFIED_TASK_FOLDER = path.join(FIXTURES_DIR, 'modified-task', 'config', 'chain1'); const VALID_SIGNATURES_DIR = path.join(FIXTURES_DIR, 'signatures/valid'); // Task creator email @@ -80,32 +82,6 @@ describe('createDeterministicTarball', () => { expect(entries).toContain('test.txt'); }); - it('excludes cache/, out/, and signer-tool/ directories', async () => { - // Create excluded directories with files - await fs.mkdir(path.join(tempDir, 'cache'), { recursive: true }); - await fs.mkdir(path.join(tempDir, 'out'), { recursive: true }); - await fs.mkdir(path.join(tempDir, 'signer-tool'), { recursive: true }); - await fs.writeFile(path.join(tempDir, 'cache', 'cached.txt'), 'cached data'); - await fs.writeFile(path.join(tempDir, 'out', 'output.txt'), 'output data'); - await fs.writeFile(path.join(tempDir, 'signer-tool', 'tool.txt'), 'tool data'); - - // Create an included file - await fs.writeFile(path.join(tempDir, 'included.txt'), 'included data'); - - const tarballPath = await createDeterministicTarball(tempDir); - createdTarballs.push(tarballPath); - - const entries = await listTarEntries(tarballPath); - - // Verify included file is present - expect(entries).toContain('included.txt'); - - // Verify excluded directories' contents are not present - expect(entries.some(e => e.includes('cache'))).toBe(false); - expect(entries.some(e => e.includes('out'))).toBe(false); - expect(entries.some(e => e.includes('signer-tool'))).toBe(false); - }); - it('produces deterministic output with identical hash', async () => { // Create some files await fs.writeFile(path.join(tempDir, 'file1.txt'), 'content 1'); @@ -170,39 +146,18 @@ describe('createDeterministicTarball', () => { }); describe('buildAndValidateSignature', () => { - let createdTarballs: string[] = []; - beforeEach(() => { - createdTarballs = []; jest.spyOn(console, 'log').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); jest.spyOn(console, 'warn').mockImplementation(() => {}); }); - afterEach(async () => { - // Clean up any created tarballs - for (const tarball of createdTarballs) { - try { - await fs.unlink(tarball); - } catch { - // Ignore errors if file doesn't exist - } - } - + afterEach(() => { jest.restoreAllMocks(); }); - // Helper to track tarballs created during tests - const trackTarball = (taskFolder: string) => { - const folderName = taskFolder.split('/').pop(); - const tarballPath = path.resolve(process.cwd(), `${folderName}.tar`); - createdTarballs.push(tarballPath); - }; - describe('valid signatures', () => { it('validates a valid signature with matching tarball', async () => { - trackTarball(VALID_TASK_FOLDER); - await expect( buildAndValidateSignature({ taskFolderPath: VALID_TASK_FOLDER, @@ -216,8 +171,6 @@ describe('buildAndValidateSignature', () => { describe('tarball mismatch', () => { it('fails validation when signature does not match tarball content', async () => { - trackTarball(MODIFIED_TASK_FOLDER); - // Use valid signature but with modified task folder await expect( buildAndValidateSignature({ @@ -232,8 +185,6 @@ describe('buildAndValidateSignature', () => { describe('SAN mismatch', () => { it('fails validation with wrong email for task creator', async () => { - trackTarball(VALID_TASK_FOLDER); - await expect( buildAndValidateSignature({ taskFolderPath: VALID_TASK_FOLDER, @@ -245,8 +196,6 @@ describe('buildAndValidateSignature', () => { }); it('fails validation with a common name that is a prefix of the real identity', async () => { - trackTarball(VALID_TASK_FOLDER); - const commonName = TASK_CREATOR_EMAIL.replace(/\.com$/, ''); // alexis.williams.1@coinbase await expect( buildAndValidateSignature({ @@ -261,8 +210,6 @@ describe('buildAndValidateSignature', () => { }); it('fails validation with a common name that uses a regex wildcard', async () => { - trackTarball(VALID_TASK_FOLDER); - const commonName = TASK_CREATOR_EMAIL.replace('.1@', '..@'); // alexis.williams..@coinbase.com await expect( buildAndValidateSignature({ @@ -277,8 +224,6 @@ describe('buildAndValidateSignature', () => { }); it('fails validation when task creator signature is verified as facilitator role', async () => { - trackTarball(VALID_TASK_FOLDER); - // Try to verify a task creator signature with facilitator role // This should fail because SAN prefix is user:/// but we expect ldap:/// await expect( @@ -292,8 +237,6 @@ describe('buildAndValidateSignature', () => { }); it('fails validation when facilitator signature is verified as task creator role', async () => { - trackTarball(VALID_TASK_FOLDER); - // Try to verify a facilitator signature with task creator role // This should fail because SAN prefix is ldap:/// but we expect user:/// await expect( diff --git a/scripts/genValidationFile.ts b/scripts/genValidationFile.ts index 946486f..3864d08 100644 --- a/scripts/genValidationFile.ts +++ b/scripts/genValidationFile.ts @@ -30,19 +30,19 @@ Examples: # Basic validation file generation tsx scripts/genValidationFile.ts \ --rpc-url https://mainnet.example \ - --workdir mainnet/2025-06-04-upgrade-foo \ + --workdir active/evm \ --forge-cmd "forge script script/Simulate.s.sol:Simulate --sig 'run()' --sender 0xabc --json" \ - --out mainnet/2025-06-04-upgrade-foo/validations/base-sc.json + --out active/evm/tasks//config//validations/base-sc.json # With L2 gas estimation for deposit transactions (-vvvv is added automatically) tsx scripts/genValidationFile.ts \ --rpc-url https://mainnet.example \ - --workdir mainnet/2025-06-04-my-l2-deposit \ + --workdir active/evm \ --forge-cmd "forge script script/MyDeposit.s.sol:MyDeposit --sig 'run()' --sender 0xabc --json" \ --estimate-l2-gas \ --l2-rpc-url https://base-mainnet.example \ --l2-gas-buffer 25 \ - --out validations/base-sc.json + --out active/evm/tasks//config//validations/base-sc.json `; console.log(msg); } diff --git a/src/lib/state-diff.ts b/src/lib/state-diff.ts index 7bd6eee..dd1a1df 100644 --- a/src/lib/state-diff.ts +++ b/src/lib/state-diff.ts @@ -93,7 +93,7 @@ export class StateDiffClient { console.log(`🔧 Running forge in ${normalizedWorkdir}: ${cmd}`); const { command, args, env: envAssignments } = this.extractCommandDetails(forgeCmdParts); - const spawnEnv = { ...process.env, ...envAssignments }; + const spawnEnv = { ...process.env, ...envAssignments, RECORD_STATE_DIFF: 'true' }; const { stdout, stderr, code } = await this.runCommand( command, diff --git a/src/lib/task-origin-validate.ts b/src/lib/task-origin-validate.ts index b3a99f6..811ba62 100644 --- a/src/lib/task-origin-validate.ts +++ b/src/lib/task-origin-validate.ts @@ -34,16 +34,10 @@ async function getAllFilesRecursively( const entries = await fs.readdir(currentDir, { withFileTypes: true }); const files: string[] = []; - // Exclude cache, out, and signer-tool folders from the tarball - const excludedFolders = ['cache', 'out', 'signer-tool']; for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { - // Skip excluded folders - if (excludedFolders.includes(entry.name)) { - continue; - } files.push(...(await getAllFilesRecursively(fullPath, baseDir, allowedDir))); } else if (entry.isFile()) { files.push(path.relative(baseDir, fullPath)); @@ -64,8 +58,7 @@ export async function createDeterministicTarball( assertWithinDir(resolvedTaskFolderPath, allowedDir); } - // Take the last '/' separate part of the folder path to be the tarfile name - const folderName = resolvedTaskFolderPath.split('/').pop(); + const folderName = path.basename(resolvedTaskFolderPath); const tarballPath = path.resolve(process.cwd(), `${folderName}.tar`); // Check if lib/ folder exists for reproducibility diff --git a/src/lib/validation-service.ts b/src/lib/validation-service.ts index ddf1baf..3c9f101 100644 --- a/src/lib/validation-service.ts +++ b/src/lib/validation-service.ts @@ -30,15 +30,31 @@ export type ValidationServiceOpts = { const CONTRACT_DEPLOYMENTS_ROOT = findContractDeploymentsRoot(); const stateDiffClient = new StateDiffClient(0, CONTRACT_DEPLOYMENTS_ROOT); -async function getConfigData( - opts: ValidationServiceOpts -): Promise<{ cfg: TaskConfig; scriptPath: string }> { - const upgradePath = path.join(CONTRACT_DEPLOYMENTS_ROOT, opts.network, opts.upgradeId); - assertWithinDir(upgradePath, CONTRACT_DEPLOYMENTS_ROOT); - +async function getConfigData(opts: ValidationServiceOpts): Promise<{ + cfg: TaskConfig; + scriptPath: string; + networkConfigDir: string; + signatureDir: string; +}> { + const scriptPath = assertWithinDir( + path.join(CONTRACT_DEPLOYMENTS_ROOT, 'active', 'evm'), + CONTRACT_DEPLOYMENTS_ROOT + ); + const taskPath = assertWithinDir( + path.join(scriptPath, 'tasks', opts.upgradeId), + CONTRACT_DEPLOYMENTS_ROOT + ); + const networkConfigDir = assertWithinDir( + path.join(taskPath, 'config', opts.network), + CONTRACT_DEPLOYMENTS_ROOT + ); + const configDir = assertWithinDir( + path.join(networkConfigDir, 'validations'), + CONTRACT_DEPLOYMENTS_ROOT + ); const configFileName = `${opts.taskConfigFileName}.json`; - const configPath = path.join(upgradePath, 'validations', configFileName); - assertWithinDir(configPath, CONTRACT_DEPLOYMENTS_ROOT); + const configPath = path.join(configDir, configFileName); + assertWithinDir(configPath, configDir); let configContent: string; try { @@ -63,7 +79,15 @@ async function getConfigData( } console.log(`✅ Loaded config data from ${configFileName}`); - return { cfg: parsedConfig.config, scriptPath: upgradePath }; + return { + cfg: parsedConfig.config, + scriptPath, + networkConfigDir, + signatureDir: assertWithinDir( + path.join(taskPath, 'signatures', opts.network), + CONTRACT_DEPLOYMENTS_ROOT + ), + }; } function getExpectedData(parsedConfig: TaskConfig): { @@ -115,17 +139,16 @@ async function runStateDiffSimulation( } async function validateSigner( - opts: ValidationServiceOpts, + taskOriginDir: string, + signatureDir: string, role: TaskOriginRole, commonNameOverride?: string // Only used for taskCreator ): Promise { - const networkPath = path.join(CONTRACT_DEPLOYMENTS_ROOT, opts.network); - const taskFolderPath = path.join(networkPath, opts.upgradeId); - assertWithinDir(taskFolderPath, CONTRACT_DEPLOYMENTS_ROOT); + assertWithinDir(taskOriginDir, CONTRACT_DEPLOYMENTS_ROOT); // Get signatureFileName from constants (hardcoded for all roles) const signatureFileName = TASK_ORIGIN_SIGNATURE_FILE_NAMES[role]; - const signatureFile = path.join(networkPath, 'signatures', opts.upgradeId, signatureFileName); + const signatureFile = path.join(signatureDir, signatureFileName); assertWithinDir(signatureFile, CONTRACT_DEPLOYMENTS_ROOT); // Get commonName: from config for taskCreator, from constants for facilitators @@ -133,7 +156,7 @@ async function validateSigner( commonNameOverride ?? TASK_ORIGIN_COMMON_NAMES[role as keyof typeof TASK_ORIGIN_COMMON_NAMES]; await verifyTaskOrigin({ - taskFolderPath, + taskFolderPath: taskOriginDir, signatureFile, commonName, role, @@ -150,7 +173,8 @@ async function validateSigner( * Validates task origin signatures. Aggregates all results and returns instead of throwing. */ async function runTaskOriginValidation( - opts: ValidationServiceOpts, + taskOriginDir: string, + signatureDir: string, config: TaskOriginValidationConfig ): Promise { const results: TaskOriginSignerResult[] = []; @@ -158,7 +182,14 @@ async function runTaskOriginValidation( // Validate task creator - uses commonName from config console.log(`🔐 Validating ${TASK_ORIGIN_ROLE_LABELS.taskCreator} signature...`); try { - results.push(await validateSigner(opts, 'taskCreator', config.taskCreator.commonName)); + results.push( + await validateSigner( + taskOriginDir, + signatureDir, + 'taskCreator', + config.taskCreator.commonName + ) + ); console.log(` ✓ ${TASK_ORIGIN_ROLE_LABELS.taskCreator} signature verified`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; @@ -173,7 +204,7 @@ async function runTaskOriginValidation( // Validate base facilitator console.log(`🔐 Validating ${TASK_ORIGIN_ROLE_LABELS.baseFacilitator} signature...`); try { - results.push(await validateSigner(opts, 'baseFacilitator')); + results.push(await validateSigner(taskOriginDir, signatureDir, 'baseFacilitator')); console.log(` ✓ ${TASK_ORIGIN_ROLE_LABELS.baseFacilitator} signature verified`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; @@ -188,7 +219,7 @@ async function runTaskOriginValidation( // Validate security council facilitator console.log(`🔐 Validating ${TASK_ORIGIN_ROLE_LABELS.securityCouncilFacilitator} signature...`); try { - results.push(await validateSigner(opts, 'securityCouncilFacilitator')); + results.push(await validateSigner(taskOriginDir, signatureDir, 'securityCouncilFacilitator')); console.log(` ✓ ${TASK_ORIGIN_ROLE_LABELS.securityCouncilFacilitator} signature verified`); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; @@ -224,7 +255,7 @@ async function runTaskOriginValidation( export async function validateUpgrade(opts: ValidationServiceOpts): Promise { console.log(`🚀 Starting validation for ${opts.upgradeId} on ${opts.network}`); - const { cfg, scriptPath } = await getConfigData(opts); + const { cfg, scriptPath, networkConfigDir, signatureDir } = await getConfigData(opts); // Determine task origin validation state let taskOriginValidation: TaskOriginValidation; @@ -244,7 +275,11 @@ export async function validateUpgrade(opts: ValidationServiceOpts): Promise