From 2478c8dfb2ec7812a257997b60a11f0d3f84f33a Mon Sep 17 00:00:00 2001 From: NSPG13 Date: Wed, 12 Aug 2026 23:19:02 -0600 Subject: [PATCH 1/4] Add bounded KeeperHub execution adapter --- docs/keeperhub-execution.md | 90 ++++++ ...uild_keeperhub_open_competition_canary.mjs | 138 +++++++++ scripts/keeperhub_direct_execution.mjs | 277 ++++++++++++++++++ scripts/test_keeperhub_direct_execution.mjs | 105 +++++++ 4 files changed, 610 insertions(+) create mode 100644 docs/keeperhub-execution.md create mode 100644 scripts/build_keeperhub_open_competition_canary.mjs create mode 100644 scripts/keeperhub_direct_execution.mjs create mode 100644 scripts/test_keeperhub_direct_execution.mjs diff --git a/docs/keeperhub-execution.md b/docs/keeperhub-execution.md new file mode 100644 index 00000000..8fe5c4f9 --- /dev/null +++ b/docs/keeperhub-execution.md @@ -0,0 +1,90 @@ +# KeeperHub execution adapter + +Agent Bounties uses KeeperHub as a bounded onchain execution layer. The first +public integration creates one **unfunded Base Sepolia Open Competition +canary** through the rehearsed V1 factory. It spends testnet gas only, transfers +no USDC, and does not modify an existing bounty. + +This integration is deliberately narrower than KeeperHub's generic direct +execution API: + +- chain: Base Sepolia (`84532`) +- contract: `0x7231f1312448fa60078fb56cdb6e2c392bd1269b` +- function: `createCompetition` +- native value: zero +- initial USDC funding: zero +- verifier: `LeadingZeroWorkVerifier(16)` at + `0x9601a40b35ad6843846732c6cb73c4c82f9ba850` + +The adapter rejects every other chain, contract, function, native value, and +nonzero initial-funding request. + +## Authentication + +Create an organization API key (`kh_`) in KeeperHub under **Settings → API +Keys → Organisation**. Store it only in the local `KH_API_KEY` environment +variable. Never put it in a request file, shell history, issue, receipt, commit, +or chat message. + +KeeperHub's organization wallet needs a small Base Sepolia ETH balance for gas. +No USDC is needed for this canary. + +## Prepare the exact request + +Use the KeeperHub organization wallet shown in the KeeperHub Wallet page: + +```powershell +node scripts/build_keeperhub_open_competition_canary.mjs ` + --wallet 0xKEEPERHUB_ORG_WALLET ` + --source-url https://github.com/NSPG13/agent-bounties/issues/931 ` + --output target/keeperhub-open-competition-canary.json +``` + +The request commits to a 0.10 test-USDC solver reward and 0.01 test-USDC +verifier reward, but `initialFunding` is zero. The resulting bounty remains in +`funding_needed` unless it is separately funded later. + +## Simulate before signing + +```powershell +node scripts/keeperhub_direct_execution.mjs simulate ` + --request target/keeperhub-open-competition-canary.json +``` + +Continue only if KeeperHub returns `success: true` and `wouldRevert: false`. +A simulation is not a transaction or payment receipt. + +## Execute once and retain the receipt + +Execution requires a fresh idempotency key and a new receipt path. The receipt +writer uses create-only semantics, so it cannot overwrite earlier evidence. + +```powershell +$keeperhubIdempotencyKey = "agent-bounties-keeperhub-" + [guid]::NewGuid() +node scripts/keeperhub_direct_execution.mjs execute ` + --request target/keeperhub-open-competition-canary.json ` + --idempotency-key $keeperhubIdempotencyKey ` + --receipt target/keeperhub-open-competition-receipt.json +``` + +The adapter polls KeeperHub's status endpoint using its poll-interval hint and +accepts success only when the final response contains all of: + +- `status: completed` +- a 32-byte transaction hash +- an HTTPS block-explorer link + +The public receipt deliberately excludes the API key. It proves one +KeeperHub-submitted Base Sepolia transaction. It does not prove bounty funding, +solver settlement, or payment; only canonical contract events establish those +states. + +## Verification + +```powershell +node scripts/test_keeperhub_direct_execution.mjs +``` + +The tests cover simulation enforcement, the chain/contract/function allowlist, +zero initial funding, idempotency, status polling, receipt requirements, and +secret exclusion. diff --git a/scripts/build_keeperhub_open_competition_canary.mjs b/scripts/build_keeperhub_open_competition_canary.mjs new file mode 100644 index 00000000..b0cc46d4 --- /dev/null +++ b/scripts/build_keeperhub_open_competition_canary.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +import { createHash, randomUUID } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { + BASE_SEPOLIA_CHAIN_ID, + BASE_SEPOLIA_OPEN_COMPETITION_FACTORY, + REQUEST_SCHEMA, + validateRequest, +} from "./keeperhub_direct_execution.mjs"; + +export const BASE_SEPOLIA_LEADING_ZERO_VERIFIER = + "0x9601a40b35ad6843846732c6cb73c4c82f9ba850"; + +function invariant(condition, message) { + if (!condition) throw new Error(message); +} + +function bytes32(label) { + return `0x${createHash("sha256").update(label).digest("hex")}`; +} + +function isAddress(value) { + return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value); +} + +export function buildCanaryRequest({ wallet, sourceUrl, nowSeconds = Math.floor(Date.now() / 1_000) }) { + invariant(isAddress(wallet), "--wallet must be an EVM address"); + const parsedSource = new URL(sourceUrl); + invariant(parsedSource.protocol === "https:", "--source-url must use HTTPS"); + invariant(Number.isSafeInteger(nowSeconds) && nowSeconds > 0, "nowSeconds is invalid"); + + const canaryId = `keeperhub-agents-onchain-${nowSeconds}-${randomUUID()}`; + const params = { + solverReward: "100000", + verifierReward: "10000", + termsHash: bytes32(`${canaryId}:terms:${sourceUrl}`), + policyHash: bytes32(`${canaryId}:policy:deterministic-first`), + acceptanceCriteriaHash: bytes32(`${canaryId}:criteria:keeperhub-receipt`), + benchmarkHash: bytes32(`${canaryId}:benchmark:leading-zero-16`), + evidenceSchemaHash: bytes32("agent-bounties/keeperhub-direct-execution-receipt-v1"), + fundingDeadline: String(nowSeconds + 7 * 24 * 60 * 60), + competitionWindowSeconds: "86400", + revealWindowSeconds: "3600", + maxEntries: 4, + verifierModule: BASE_SEPOLIA_LEADING_ZERO_VERIFIER, + verifierRewardRecipient: wallet, + }; + + const request = { + schema_version: REQUEST_SCHEMA, + operation: "contract_call", + chain_id: BASE_SEPOLIA_CHAIN_ID, + contract_address: BASE_SEPOLIA_OPEN_COMPETITION_FACTORY, + function_name: "createCompetition", + function_args: [params, "0", bytes32(`${canaryId}:creation-nonce`)], + abi: [ + { + type: "function", + name: "createCompetition", + stateMutability: "nonpayable", + inputs: [ + { + name: "params", + type: "tuple", + components: [ + { name: "solverReward", type: "uint256" }, + { name: "verifierReward", type: "uint256" }, + { name: "termsHash", type: "bytes32" }, + { name: "policyHash", type: "bytes32" }, + { name: "acceptanceCriteriaHash", type: "bytes32" }, + { name: "benchmarkHash", type: "bytes32" }, + { name: "evidenceSchemaHash", type: "bytes32" }, + { name: "fundingDeadline", type: "uint64" }, + { name: "competitionWindowSeconds", type: "uint64" }, + { name: "revealWindowSeconds", type: "uint64" }, + { name: "maxEntries", type: "uint8" }, + { name: "verifierModule", type: "address" }, + { name: "verifierRewardRecipient", type: "address" }, + ], + }, + { name: "initialFunding", type: "uint256" }, + { name: "creationNonce", type: "bytes32" }, + ], + outputs: [ + { name: "bountyAddress", type: "address" }, + { name: "bountyId", type: "bytes32" }, + ], + }, + ], + value: "0", + source_url: sourceUrl, + title: "KeeperHub execution canary — unfunded Open Competition", + expected_effect: { + event: "CanonicalCompetitionCreated", + initial_funding_usdc_units: "0", + target_usdc_units: "110000", + public_inventory_state: "funding_needed", + }, + evidence_boundary: + "This request creates one new unfunded Base Sepolia canary. It moves no USDC, changes no existing bounty, and cannot prove settlement or payment.", + }; + return validateRequest(request); +} + +function parseCliArgs(argv) { + const options = {}; + for (let index = 0; index < argv.length; index += 1) { + const key = argv[index]; + invariant(key.startsWith("--"), `unexpected argument: ${key}`); + const value = argv[index + 1]; + invariant(value !== undefined && !value.startsWith("--"), `missing value for ${key}`); + options[key.slice(2)] = value; + index += 1; + } + return options; +} + +async function main() { + const options = parseCliArgs(process.argv.slice(2)); + invariant(options.wallet, "--wallet is required"); + invariant(options["source-url"], "--source-url is required"); + invariant(options.output, "--output is required"); + const request = buildCanaryRequest({ wallet: options.wallet, sourceUrl: options["source-url"] }); + await writeFile(resolve(options.output), `${JSON.stringify(request, null, 2)}\n`, { flag: "wx" }); + process.stdout.write(`${resolve(options.output)}\n`); +} + +const invokedAsScript = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; +if (invokedAsScript) { + main().catch((error) => { + process.stderr.write(`build_keeperhub_open_competition_canary: ${error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/keeperhub_direct_execution.mjs b/scripts/keeperhub_direct_execution.mjs new file mode 100644 index 00000000..c4645ae6 --- /dev/null +++ b/scripts/keeperhub_direct_execution.mjs @@ -0,0 +1,277 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +export const REQUEST_SCHEMA = "agent-bounties/keeperhub-direct-execution-request-v1"; +export const RECEIPT_SCHEMA = "agent-bounties/keeperhub-direct-execution-receipt-v1"; +export const BASE_SEPOLIA_CHAIN_ID = 84532; +export const BASE_SEPOLIA_OPEN_COMPETITION_FACTORY = + "0x7231f1312448fa60078fb56cdb6e2c392bd1269b"; + +const DEFAULT_BASE_URL = "https://app.keeperhub.com"; +const TERMINAL_STATUSES = new Set(["completed", "failed"]); + +function invariant(condition, message) { + if (!condition) throw new Error(message); +} + +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isAddress(value) { + return typeof value === "string" && /^0x[0-9a-fA-F]{40}$/.test(value); +} + +function isBytes32(value) { + return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value); +} + +function canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize); + if (!isPlainObject(value)) return value; + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalize(value[key])]), + ); +} + +export function requestFingerprint(request) { + return `sha256:${createHash("sha256") + .update(JSON.stringify(canonicalize(request))) + .digest("hex")}`; +} + +export function validateRequest(request) { + invariant(isPlainObject(request), "request must be a JSON object"); + invariant(request.schema_version === REQUEST_SCHEMA, "unsupported request schema"); + invariant(request.operation === "contract_call", "only contract_call is supported"); + invariant( + Number(request.chain_id) === BASE_SEPOLIA_CHAIN_ID, + "KeeperHub canary execution is restricted to Base Sepolia (84532)", + ); + invariant( + String(request.contract_address).toLowerCase() === BASE_SEPOLIA_OPEN_COMPETITION_FACTORY, + "contract address is not the rehearsed Base Sepolia Open Competition factory", + ); + invariant(request.function_name === "createCompetition", "function is not createCompetition"); + invariant(Array.isArray(request.function_args), "function_args must be a JSON array"); + invariant(request.function_args.length === 3, "createCompetition requires exactly three arguments"); + invariant( + request.function_args[1] === "0" || request.function_args[1] === 0, + "the hackathon canary must have zero initial funding", + ); + invariant(isBytes32(request.function_args[2]), "creation nonce must be bytes32"); + invariant(Array.isArray(request.abi) && request.abi.length > 0, "abi must be a non-empty JSON array"); + invariant(request.value === undefined || request.value === "0", "native value must be zero"); + invariant( + typeof request.evidence_boundary === "string" && request.evidence_boundary.length >= 40, + "evidence_boundary is required", + ); + invariant(request.simulate === undefined, "simulate is controlled by the command, not the request file"); + return request; +} + +export function buildKeeperHubBody(request, simulate) { + validateRequest(request); + invariant(typeof simulate === "boolean", "simulate must be a boolean"); + return { + contractAddress: request.contract_address, + chainId: Number(request.chain_id), + functionName: request.function_name, + functionArgs: JSON.stringify(request.function_args), + abi: JSON.stringify(request.abi), + value: "0", + simulate, + }; +} + +function validateApiKey(apiKey) { + invariant(typeof apiKey === "string" && /^kh_[A-Za-z0-9_-]{8,}$/.test(apiKey), "KH_API_KEY is missing or invalid"); +} + +function validateBaseUrl(baseUrl) { + const url = new URL(baseUrl); + invariant(url.protocol === "https:" || url.hostname === "127.0.0.1", "KeeperHub base URL must use HTTPS"); + return url.origin; +} + +async function parseResponse(response) { + const text = await response.text(); + let body = null; + try { + body = text ? JSON.parse(text) : {}; + } catch { + body = { detail: text.slice(0, 500) }; + } + if (!response.ok) { + const code = body?.error || body?.code || `http_${response.status}`; + const requestId = body?.request_id || response.headers.get("x-request-id") || "unknown"; + throw new Error(`KeeperHub request failed: ${code} (request_id=${requestId})`); + } + return body; +} + +async function keeperFetch({ fetchImpl, baseUrl, apiKey, path, method, body, idempotencyKey }) { + validateApiKey(apiKey); + const headers = { + Authorization: `Bearer ${apiKey}`, + Accept: "application/json", + "Content-Type": "application/json", + "x-request-id": `agent-bounties-${crypto.randomUUID()}`, + }; + if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey; + const response = await fetchImpl(`${validateBaseUrl(baseUrl)}${path}`, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }); + return { response, body: await parseResponse(response) }; +} + +export async function simulateRequest({ + request, + apiKey, + fetchImpl = fetch, + baseUrl = DEFAULT_BASE_URL, +}) { + const { body } = await keeperFetch({ + fetchImpl, + baseUrl, + apiKey, + path: "/api/execute/contract-call", + method: "POST", + body: buildKeeperHubBody(request, true), + }); + invariant(body.success === true, "KeeperHub simulation did not report success"); + invariant(body.wouldRevert === false, "KeeperHub simulation would revert"); + return { + schema_version: RECEIPT_SCHEMA, + mode: "simulation", + request_fingerprint: requestFingerprint(request), + chain_id: BASE_SEPOLIA_CHAIN_ID, + contract_address: BASE_SEPOLIA_OPEN_COMPETITION_FACTORY, + provider: "KeeperHub", + simulation: body, + evidence_boundary: + "A simulation is not an onchain transaction, bounty funding, settlement, or payment evidence.", + }; +} + +export async function executeRequest({ + request, + apiKey, + idempotencyKey, + fetchImpl = fetch, + baseUrl = DEFAULT_BASE_URL, + sleep = (milliseconds) => new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds)), + maxPolls = 60, +}) { + invariant( + typeof idempotencyKey === "string" && /^[A-Za-z0-9:._-]{16,128}$/.test(idempotencyKey), + "idempotency key must be 16-128 safe characters", + ); + const keeperBody = buildKeeperHubBody(request, false); + delete keeperBody.simulate; + const { body: accepted } = await keeperFetch({ + fetchImpl, + baseUrl, + apiKey, + path: "/api/execute/contract-call", + method: "POST", + body: keeperBody, + idempotencyKey, + }); + invariant(typeof accepted.executionId === "string" && accepted.executionId.length > 0, "missing executionId"); + + let statusBody = null; + for (let poll = 0; poll < maxPolls; poll += 1) { + const { response, body } = await keeperFetch({ + fetchImpl, + baseUrl, + apiKey, + path: `/api/execute/${encodeURIComponent(accepted.executionId)}/status`, + method: "GET", + }); + statusBody = body; + if (TERMINAL_STATUSES.has(body.status)) break; + const hintSeconds = Number(response.headers.get("x-poll-interval-hint") || "2"); + const boundedMilliseconds = Math.min(Math.max(hintSeconds, 1), 10) * 1_000; + await sleep(boundedMilliseconds); + } + + invariant(statusBody !== null, "KeeperHub status was not returned"); + invariant(statusBody.status === "completed", `KeeperHub execution ended as ${statusBody.status || "unknown"}`); + invariant(/^0x[0-9a-fA-F]{64}$/.test(statusBody.transactionHash), "completed execution has no transaction hash"); + invariant( + typeof statusBody.transactionLink === "string" && statusBody.transactionLink.startsWith("https://"), + "completed execution has no explorer link", + ); + + return { + schema_version: RECEIPT_SCHEMA, + mode: "execution", + provider: "KeeperHub", + request_fingerprint: requestFingerprint(request), + idempotency_key: idempotencyKey, + execution_id: accepted.executionId, + chain_id: BASE_SEPOLIA_CHAIN_ID, + contract_address: BASE_SEPOLIA_OPEN_COMPETITION_FACTORY, + status: statusBody.status, + transaction_hash: statusBody.transactionHash, + transaction_link: statusBody.transactionLink, + completed_at: statusBody.completedAt || null, + gas_used_wei: statusBody.gasUsedWei || null, + expected_effect: request.expected_effect || null, + evidence_boundary: + "This receipt proves one KeeperHub-submitted Base Sepolia transaction. It does not prove bounty funding, solver settlement, or payment.", + }; +} + +function parseCliArgs(argv) { + const command = argv[0]; + const options = {}; + for (let index = 1; index < argv.length; index += 1) { + const key = argv[index]; + invariant(key.startsWith("--"), `unexpected argument: ${key}`); + const value = argv[index + 1]; + invariant(value !== undefined && !value.startsWith("--"), `missing value for ${key}`); + options[key.slice(2)] = value; + index += 1; + } + return { command, options }; +} + +async function main() { + const { command, options } = parseCliArgs(process.argv.slice(2)); + invariant(command === "simulate" || command === "execute", "usage: keeperhub_direct_execution.mjs --request FILE [options]"); + invariant(options.request, "--request is required"); + const request = validateRequest(JSON.parse(await readFile(resolve(options.request), "utf8"))); + const apiKey = process.env.KH_API_KEY; + let receipt; + if (command === "simulate") { + receipt = await simulateRequest({ request, apiKey }); + } else { + invariant(options["idempotency-key"], "--idempotency-key is required for execution"); + invariant(options.receipt, "--receipt is required for execution"); + receipt = await executeRequest({ + request, + apiKey, + idempotencyKey: options["idempotency-key"], + }); + await writeFile(resolve(options.receipt), `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx" }); + } + process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`); +} + +const invokedAsScript = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; +if (invokedAsScript) { + main().catch((error) => { + process.stderr.write(`keeperhub_direct_execution: ${error.message}\n`); + process.exitCode = 1; + }); +} diff --git a/scripts/test_keeperhub_direct_execution.mjs b/scripts/test_keeperhub_direct_execution.mjs new file mode 100644 index 00000000..63280c55 --- /dev/null +++ b/scripts/test_keeperhub_direct_execution.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; + +import { buildCanaryRequest } from "./build_keeperhub_open_competition_canary.mjs"; +import { + buildKeeperHubBody, + executeRequest, + simulateRequest, + validateRequest, +} from "./keeperhub_direct_execution.mjs"; + +const WALLET = "0x884834e884d6e93462655a2820140ad03e6747bc"; +const API_KEY = "kh_test_key_for_adapter"; +const TX_HASH = `0x${"ab".repeat(32)}`; + +function response(status, body, headers = {}) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json", ...headers }, + }); +} + +const request = buildCanaryRequest({ + wallet: WALLET, + sourceUrl: "https://github.com/NSPG13/agent-bounties/issues/931", + nowSeconds: 1_786_600_000, +}); +validateRequest(request); + +const simulationBody = buildKeeperHubBody(request, true); +assert.equal(simulationBody.chainId, 84532); +assert.equal(simulationBody.simulate, true); +assert.equal(JSON.parse(simulationBody.functionArgs)[1], "0"); + +assert.throws( + () => validateRequest({ ...request, chain_id: 8453 }), + /restricted to Base Sepolia/, +); +assert.throws( + () => validateRequest({ ...request, function_args: [request.function_args[0], "1", request.function_args[2]] }), + /zero initial funding/, +); + +const simulateCalls = []; +const simulated = await simulateRequest({ + request, + apiKey: API_KEY, + baseUrl: "https://keeperhub.test", + fetchImpl: async (url, init) => { + simulateCalls.push({ url, init }); + return response(200, { + success: true, + status: "simulated", + from: WALLET, + to: request.contract_address, + value: "0", + gasEstimate: "180000", + simulatedReturnValue: ["0x0000000000000000000000000000000000000001", `0x${"cd".repeat(32)}`], + wouldRevert: false, + }); + }, +}); +assert.equal(simulated.mode, "simulation"); +assert.equal(simulateCalls.length, 1); +assert.equal(JSON.parse(simulateCalls[0].init.body).simulate, true); +assert.match(simulateCalls[0].init.headers.Authorization, /^Bearer kh_/); + +const executeCalls = []; +const executed = await executeRequest({ + request, + apiKey: API_KEY, + idempotencyKey: "keeperhub-test-0001", + baseUrl: "https://keeperhub.test", + sleep: async () => {}, + fetchImpl: async (url, init) => { + executeCalls.push({ url, init }); + if (url.endsWith("/api/execute/contract-call")) { + return response(202, { executionId: "direct_test_1", status: "completed" }); + } + return response( + 200, + { + executionId: "direct_test_1", + status: "completed", + type: "contract-call", + transactionHash: TX_HASH, + transactionLink: `https://sepolia.basescan.org/tx/${TX_HASH}`, + gasUsedWei: "12345", + completedAt: "2026-08-13T00:00:00Z", + }, + { "x-poll-interval-hint": "0" }, + ); + }, +}); +assert.equal(executed.status, "completed"); +assert.equal(executed.transaction_hash, TX_HASH); +assert.equal(executeCalls.length, 2); +assert.equal(executeCalls[0].init.headers["Idempotency-Key"], "keeperhub-test-0001"); +assert.equal(Object.hasOwn(JSON.parse(executeCalls[0].init.body), "simulate"), false); +assert.equal(executeCalls[1].init.method, "GET"); + +const serialized = JSON.stringify(executed); +assert.equal(serialized.includes(API_KEY), false); +console.log("keeperhub_direct_execution_tests=ok"); From 3937cc8360424b3d04bb65f6d25f66969c5e8392 Mon Sep 17 00:00:00 2001 From: NSPG13 Date: Wed, 12 Aug 2026 23:20:39 -0600 Subject: [PATCH 2/4] Run KeeperHub adapter tests in full check --- scripts/check.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check.py b/scripts/check.py index 220e3209..780b134c 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -257,6 +257,7 @@ def main() -> int: ["--check", "scripts/open-competition-v1-signer.js"], ["scripts/test-open-competition-v1-signer-console.js"], ["scripts/test-create-competition-flow.js"], + ["scripts/test_keeperhub_direct_execution.mjs"], ["--check", "site/standing-meta-v3-migration.js"], )]) py("-m", "pip", "install", "-r", "scripts/requirements-attest.txt") From 049b676ca3dd392dcfebd2da16fcf18bee0f011c Mon Sep 17 00:00:00 2001 From: NSPG13 Date: Thu, 13 Aug 2026 00:13:41 -0600 Subject: [PATCH 3/4] Record KeeperHub canary evidence --- ...nchain-canary-base-sepolia-2026-08-13.json | 48 +++++++++++++++++++ .../test_activate_routed_v3_replacements.py | 4 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json diff --git a/docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json b/docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json new file mode 100644 index 00000000..a0f3ed99 --- /dev/null +++ b/docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json @@ -0,0 +1,48 @@ +{ + "schema_version": "agent-bounties/keeperhub-agents-onchain-canary-evidence-v1", + "recorded_at": "2026-08-13T05:43:35.057Z", + "source": { + "commit": "3937cc8360424b3d04bb65f6d25f66969c5e8392", + "pull_request": "https://github.com/NSPG13/agent-bounties/pull/932" + }, + "network": { + "name": "Base Sepolia", + "chain_id": 84532, + "rpc_verification": "https://sepolia.base.org" + }, + "keeperhub": { + "execution_id": "z2lp1eatpds9fx766rktg", + "organization_wallet": "0xd8688bc66c7059a84d24cd9849207b838bf72bf2", + "simulation": { + "success": true, + "would_revert": false, + "gas_estimate": "504537", + "predicted_bounty": "0x5f2df60ff6264b9c20d5da4e13f168772af57a30", + "predicted_bounty_id": "0x85d90e337ccfb788d576012b3c892dc6f0775acdb2b744268e612c7dc66e1889" + } + }, + "transaction": { + "hash": "0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329", + "explorer_url": "https://sepolia.basescan.org/tx/0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329", + "status": "0x1", + "block_number": 45415763, + "block_hash": "0x4e8d87c1323db884c355afb9ed726e07eb352100d07469c568f99192784a7c98", + "gas_used": "560155", + "factory": "0x7231f1312448fa60078fb56cdb6e2c392bd1269b", + "bounty": "0x5f2df60ff6264b9c20d5da4e13f168772af57a30", + "bounty_id": "0x85d90e337ccfb788d576012b3c892dc6f0775acdb2b744268e612c7dc66e1889", + "creator": "0xd8688bc66c7059a84d24cd9849207b838bf72bf2", + "canonical_factory_registration": true, + "canonical_event": { + "name": "CanonicalCompetitionCreated", + "topic0": "0xd89805182f83e81946ee47273c3282be41947da1982d6d1788b61682be82dec5" + } + }, + "economics": { + "initial_funding_usdc_units": "0", + "target_usdc_units": "110000", + "native_value_wei": "0", + "public_inventory_state": "funding_needed" + }, + "evidence_boundary": "This evidence proves one KeeperHub-submitted Base Sepolia canary transaction and canonical Open Competition creation. It does not prove bounty funding, solver settlement, or payment." +} diff --git a/scripts/test_activate_routed_v3_replacements.py b/scripts/test_activate_routed_v3_replacements.py index 09d7efef..9dc8e727 100644 --- a/scripts/test_activate_routed_v3_replacements.py +++ b/scripts/test_activate_routed_v3_replacements.py @@ -210,7 +210,7 @@ def test_exact_active_wallet_policy_is_ready(self) -> None: ) state = MODULE.policy_state(PolicyCast(), deployment) self.assertEqual(state["policy_hash"], MODULE.active_wallet.POLICY_HASH) - self.assertEqual(state["policy_version"], 5) + self.assertEqual(state["policy_version"], MODULE.active_wallet.POLICY_VERSION) self.assertEqual(state["affordable_creations"], 4) def test_active_wallet_policy_drift_fails_closed(self) -> None: @@ -230,7 +230,7 @@ def test_active_wallet_policy_drift_fails_closed(self) -> None: "deterministic_verifier": "0x" + "93" * 20, "signed_quorum": "0x" + "94" * 32, "policy_hash": "0x" + "95" * 32, - "policy_version": 6, + "policy_version": MODULE.active_wallet.POLICY_VERSION + 1, "max_per_period": 11_000_000, } for field, observed in cases.items(): From 5138cae8dc4609bde023cf32406465ec5f4a87fd Mon Sep 17 00:00:00 2001 From: NSPG13 Date: Thu, 13 Aug 2026 00:57:09 -0600 Subject: [PATCH 4/4] Add live KeeperHub canary verifier --- docs/keeperhub-execution.md | 22 +++ scripts/check.py | 1 + .../test_verify_keeperhub_canary_evidence.mjs | 106 ++++++++++++ scripts/verify_keeperhub_canary_evidence.mjs | 161 ++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 scripts/test_verify_keeperhub_canary_evidence.mjs create mode 100644 scripts/verify_keeperhub_canary_evidence.mjs diff --git a/docs/keeperhub-execution.md b/docs/keeperhub-execution.md index 8fe5c4f9..ad51d0bb 100644 --- a/docs/keeperhub-execution.md +++ b/docs/keeperhub-execution.md @@ -19,6 +19,28 @@ execution API: The adapter rejects every other chain, contract, function, native value, and nonzero initial-funding request. +## Judge in 60 seconds + +The public KeeperHub execution is +[`0x80fb...b329`](https://sepolia.basescan.org/tx/0x80fb04d83d6135c2b1f9753d9fb449a693d9f1be0a84fddcc60f03ecee6ab329), +with KeeperHub execution ID `z2lp1eatpds9fx766rktg`. Its machine-readable +receipt is checked in at +[`docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json`](evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json). + +Verify the receipt directly against Base Sepolia: + +```powershell +node scripts/verify_keeperhub_canary_evidence.mjs ` + --evidence docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json ` + --rpc-url https://sepolia.base.org +``` + +The verifier fails closed unless the RPC reports Base Sepolia, the exact +transaction succeeded at the recorded block with the recorded gas usage, and +the factory emitted exactly one matching `CanonicalCompetitionCreated` event +for the recorded bounty ID, bounty address, and creator. It does not infer +funding or payment from a successful transaction. + ## Authentication Create an organization API key (`kh_`) in KeeperHub under **Settings → API diff --git a/scripts/check.py b/scripts/check.py index 780b134c..aae0a136 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -258,6 +258,7 @@ def main() -> int: ["scripts/test-open-competition-v1-signer-console.js"], ["scripts/test-create-competition-flow.js"], ["scripts/test_keeperhub_direct_execution.mjs"], + ["scripts/test_verify_keeperhub_canary_evidence.mjs"], ["--check", "site/standing-meta-v3-migration.js"], )]) py("-m", "pip", "install", "-r", "scripts/requirements-attest.txt") diff --git a/scripts/test_verify_keeperhub_canary_evidence.mjs b/scripts/test_verify_keeperhub_canary_evidence.mjs new file mode 100644 index 00000000..7ccb2e02 --- /dev/null +++ b/scripts/test_verify_keeperhub_canary_evidence.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +import { verifyKeeperHubCanaryEvidence } from "./verify_keeperhub_canary_evidence.mjs"; + +const evidence = JSON.parse( + await readFile( + new URL("../docs/evidence/keeperhub-agents-onchain-canary-base-sepolia-2026-08-13.json", import.meta.url), + "utf8", + ), +); + +function addressTopic(address) { + return `0x${"0".repeat(24)}${address.toLowerCase().slice(2)}`; +} + +function jsonResponse(result) { + return new Response(JSON.stringify({ jsonrpc: "2.0", id: 1, result }), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +function matchingReceipt() { + return { + transactionHash: evidence.transaction.hash, + status: evidence.transaction.status, + blockHash: evidence.transaction.block_hash, + blockNumber: `0x${evidence.transaction.block_number.toString(16)}`, + gasUsed: `0x${BigInt(evidence.transaction.gas_used).toString(16)}`, + logs: [ + { + address: evidence.transaction.factory, + removed: false, + topics: [ + evidence.transaction.canonical_event.topic0, + evidence.transaction.bounty_id, + addressTopic(evidence.transaction.bounty), + addressTopic(evidence.transaction.creator), + ], + }, + ], + }; +} + +function rpcFixture(receipt, chainId = "0x14a34") { + return async (_url, init) => { + const request = JSON.parse(init.body); + if (request.method === "eth_chainId") return jsonResponse(chainId); + if (request.method === "eth_getTransactionReceipt") return jsonResponse(receipt); + throw new Error(`unexpected method: ${request.method}`); + }; +} + +const verified = await verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: "https://sepolia.example", + fetchImpl: rpcFixture(matchingReceipt()), +}); +assert.equal(verified.verified, true); +assert.equal(verified.transaction_hash, evidence.transaction.hash); +assert.equal(verified.bounty, evidence.transaction.bounty); + +await assert.rejects( + verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: "https://sepolia.example", + fetchImpl: rpcFixture(matchingReceipt(), "0x2105"), + }), + /RPC is not Base Sepolia/, +); + +const failedReceipt = matchingReceipt(); +failedReceipt.status = "0x0"; +await assert.rejects( + verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: "https://sepolia.example", + fetchImpl: rpcFixture(failedReceipt), + }), + /transaction did not succeed/, +); + +const tamperedEvent = matchingReceipt(); +tamperedEvent.logs[0].topics[2] = addressTopic("0x0000000000000000000000000000000000000001"); +await assert.rejects( + verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: "https://sepolia.example", + fetchImpl: rpcFixture(tamperedEvent), + }), + /expected one canonical creation event, found 0/, +); + +await assert.rejects( + verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: "http://sepolia.example", + fetchImpl: rpcFixture(matchingReceipt()), + }), + /RPC URL must use HTTPS/, +); + +console.log("verify_keeperhub_canary_evidence_tests=ok"); diff --git a/scripts/verify_keeperhub_canary_evidence.mjs b/scripts/verify_keeperhub_canary_evidence.mjs new file mode 100644 index 00000000..c69f54bf --- /dev/null +++ b/scripts/verify_keeperhub_canary_evidence.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +export const EVIDENCE_SCHEMA = + "agent-bounties/keeperhub-agents-onchain-canary-evidence-v1"; +export const BASE_SEPOLIA_CHAIN_ID = 84532; +export const DEFAULT_BASE_SEPOLIA_RPC_URL = "https://sepolia.base.org"; + +function invariant(condition, message) { + if (!condition) throw new Error(message); +} + +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function normalizedHex(value, bytes, label) { + invariant( + typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${bytes * 2}}$`).test(value), + `${label} must be ${bytes} bytes`, + ); + return value.toLowerCase(); +} + +function addressTopic(address, label) { + return `0x${"0".repeat(24)}${normalizedHex(address, 20, label).slice(2)}`; +} + +function rpcOrigin(rpcUrl) { + const url = new URL(rpcUrl); + const local = url.hostname === "127.0.0.1" || url.hostname === "localhost"; + invariant(url.protocol === "https:" || (local && url.protocol === "http:"), "RPC URL must use HTTPS"); + return url.href; +} + +async function rpcCall({ rpcUrl, method, params, fetchImpl }) { + const response = await fetchImpl(rpcOrigin(rpcUrl), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + }); + invariant(response.ok, `RPC ${method} returned HTTP ${response.status}`); + const body = await response.json(); + invariant(isPlainObject(body), `RPC ${method} returned a malformed response`); + invariant(!body.error, `RPC ${method} failed: ${body.error?.message || "unknown error"}`); + return body.result; +} + +export async function verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl = DEFAULT_BASE_SEPOLIA_RPC_URL, + fetchImpl = fetch, +}) { + invariant(isPlainObject(evidence), "evidence must be a JSON object"); + invariant(evidence.schema_version === EVIDENCE_SCHEMA, "unsupported evidence schema"); + invariant(Number(evidence.network?.chain_id) === BASE_SEPOLIA_CHAIN_ID, "evidence is not Base Sepolia"); + + const transaction = evidence.transaction; + invariant(isPlainObject(transaction), "transaction evidence is required"); + const transactionHash = normalizedHex(transaction.hash, 32, "transaction hash"); + const blockHash = normalizedHex(transaction.block_hash, 32, "block hash"); + const factory = normalizedHex(transaction.factory, 20, "factory"); + const bounty = normalizedHex(transaction.bounty, 20, "bounty"); + const bountyId = normalizedHex(transaction.bounty_id, 32, "bounty ID"); + const creator = normalizedHex(transaction.creator, 20, "creator"); + const eventTopic = normalizedHex( + transaction.canonical_event?.topic0, + 32, + "canonical event topic", + ); + invariant( + transaction.canonical_event?.name === "CanonicalCompetitionCreated", + "unexpected canonical event name", + ); + invariant(transaction.canonical_factory_registration === true, "factory registration evidence is missing"); + + const chainIdHex = await rpcCall({ + rpcUrl, + method: "eth_chainId", + params: [], + fetchImpl, + }); + invariant(BigInt(chainIdHex) === BigInt(BASE_SEPOLIA_CHAIN_ID), "RPC is not Base Sepolia"); + + const receipt = await rpcCall({ + rpcUrl, + method: "eth_getTransactionReceipt", + params: [transactionHash], + fetchImpl, + }); + invariant(isPlainObject(receipt), "transaction receipt is unavailable"); + invariant(normalizedHex(receipt.transactionHash, 32, "receipt transaction hash") === transactionHash, "transaction hash mismatch"); + invariant(receipt.status === transaction.status && receipt.status === "0x1", "transaction did not succeed"); + invariant(normalizedHex(receipt.blockHash, 32, "receipt block hash") === blockHash, "block hash mismatch"); + invariant(BigInt(receipt.blockNumber) === BigInt(transaction.block_number), "block number mismatch"); + invariant(BigInt(receipt.gasUsed) === BigInt(transaction.gas_used), "gas used mismatch"); + invariant(Array.isArray(receipt.logs), "receipt logs are missing"); + + const expectedTopics = [ + eventTopic, + bountyId, + addressTopic(bounty, "bounty"), + addressTopic(creator, "creator"), + ]; + const canonicalLogs = receipt.logs.filter((log) => { + if (!isPlainObject(log) || log.removed === true) return false; + if (String(log.address).toLowerCase() !== factory) return false; + if (!Array.isArray(log.topics) || log.topics.length !== expectedTopics.length) return false; + return expectedTopics.every((topic, index) => String(log.topics[index]).toLowerCase() === topic); + }); + invariant(canonicalLogs.length === 1, `expected one canonical creation event, found ${canonicalLogs.length}`); + + return { + schema_version: EVIDENCE_SCHEMA, + verified: true, + chain_id: BASE_SEPOLIA_CHAIN_ID, + transaction_hash: transactionHash, + block_number: Number(BigInt(receipt.blockNumber)), + factory, + bounty, + bounty_id: bountyId, + creator, + canonical_event: "CanonicalCompetitionCreated", + evidence_boundary: evidence.evidence_boundary, + }; +} + +function parseCliArgs(argv) { + const options = {}; + for (let index = 0; index < argv.length; index += 1) { + const key = argv[index]; + invariant(key.startsWith("--"), `unexpected argument: ${key}`); + const value = argv[index + 1]; + invariant(value !== undefined && !value.startsWith("--"), `missing value for ${key}`); + options[key.slice(2)] = value; + index += 1; + } + return options; +} + +async function main() { + const options = parseCliArgs(process.argv.slice(2)); + invariant(options.evidence, "--evidence is required"); + const evidence = JSON.parse(await readFile(resolve(options.evidence), "utf8")); + const verified = await verifyKeeperHubCanaryEvidence({ + evidence, + rpcUrl: options["rpc-url"] || process.env.BASE_SEPOLIA_RPC_URL || DEFAULT_BASE_SEPOLIA_RPC_URL, + }); + process.stdout.write(`${JSON.stringify(verified, null, 2)}\n`); +} + +const invokedAsScript = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; +if (invokedAsScript) { + main().catch((error) => { + process.stderr.write(`verify_keeperhub_canary_evidence: ${error.message}\n`); + process.exitCode = 1; + }); +}