Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ When a verb request omits `x402`, the runtime fabricates defaults from the live

When `VERIFY_SCHEMA_CACHED_ONLY=1` (the default), `/verify?schema=1` returns HTTP `202` with `validator_not_warmed_yet` if the validator for that verb has not been compiled yet. `POST /debug/prewarm` can queue validator warmup, and `GET /debug/validators` shows cache state.

If `/verify` exceeds `VERIFY_MAX_MS`, the runtime returns HTTP `502` with `failure_type: "availability"`, `retryable: true`, and the message `Verification service did not respond. Receipt may still be valid; retry recommended.` Clients should treat that as transient service unavailability, not a cryptographic proof failure.

## ENS verification inputs

When `ens=1`, the runtime resolves TXT records directly on the signer ENS name and reads:
Expand Down
6 changes: 6 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ This is expected when:

Use `/debug/prewarm`, retry later, or disable cached-only mode.

### `/verify` returns HTTP `502`

This indicates a transient verify availability failure, not proof invalidity. The runtime emits `failure_type: "availability"`, `retryable: true`, and a retry-oriented message so callers can distinguish service timeouts from cryptographic failures.

Given the current server implementation, this points more strongly to process stall/crash or an upstream proxy timeout than to cold schema loading alone, because cold validator loading under cached-only mode returns HTTP `202` instead of timing out. If the request used `ens=1&refresh=1` or `schema=1` with cached-only mode disabled, blocked upstream fetches or ENS/schema slowness are also plausible contributors.

### ENS verification fails before signature checks

Check:
Expand Down
43 changes: 43 additions & 0 deletions runtime/tests/runtime-signing.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync, spawn } from "node:child_process";
import http from "node:http";
import net from "node:net";
import { generateKeyPairSync, createHash } from "node:crypto";

Expand Down Expand Up @@ -246,6 +247,48 @@ test("/verify?ens=1&strict_kid=1 rejects a receipt when ENS publishes a differen
}
});

test("/verify surfaces transient timeout failures separately from cryptographic failures", async () => {
const keys = makeKeys();
const rpcPort = await freePort();
const rpcHang = http.createServer((req, res) => {
req.on("data", () => {});
req.on("end", () => {});
});
await new Promise((resolve) => rpcHang.listen(rpcPort, "127.0.0.1", resolve));

const srv = await startServer({
VERIFY_MAX_MS: "50",
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
ETH_RPC_URL: `http://127.0.0.1:${rpcPort}`,
});

try {
const response = await createDescribeReceipt(srv.base);

const verifyResp = await fetch(`${srv.base}/verify?ens=1&refresh=1`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(response),
});
const verifyJson = await verifyResp.json();

assert.equal(verifyResp.status, 502);
assert.equal(verifyJson.ok, false);
assert.equal(verifyJson.failure_type, "availability");
assert.equal(verifyJson.retryable, true);
assert.equal(verifyJson.reason, "verify_service_unavailable");
assert.match(String(verifyJson.message), /Receipt may still be valid; retry recommended\./);
assert.equal(verifyJson.checks?.hash_matches, null);
assert.equal(verifyJson.checks?.signature_valid, null);
assert.equal(verifyJson.checks?.ens_match, null);
} finally {
await stop(srv.proc);
await new Promise((resolve, reject) => rpcHang.close((err) => (err ? reject(err) : resolve())));
}
});

test("fabricated x402 defaults use v1.1.0", async () => {
const keys = makeKeys();
const srv = await startServer({
Expand Down
27 changes: 24 additions & 3 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1801,10 +1801,31 @@ app.post("/verify", async (req, res) => {
new Promise((_, rej) => setTimeout(() => rej(new Error("verify_timeout")), VERIFY_MAX_MS)),
]);
} catch (e) {
return res.status(500).json({
const errorMessage = e?.message || "verify failed";
const transient = errorMessage === "verify_timeout";

if (transient) {
console.error("[verify] transient availability failure", {
error: errorMessage,
want_ens: wantEns,
want_schema: wantSchema,
refresh,
strict_kid: strictKid,
verb: receipt?.x402?.verb ?? null,
signer_id: proof?.signer_id ?? null,
});
}

return res.status(transient ? 502 : 500).json({
ok: false,
error: e?.message || "verify failed",
checks: { schema_valid: null, hash_matches: null, signature_valid: false, ens_match: null },
error: errorMessage,
failure_type: transient ? "availability" : "application",
retryable: transient,
message: transient
? "Verification service did not respond. Receipt may still be valid; retry recommended."
: "Verification failed.",
reason: transient ? "verify_service_unavailable" : "verify_failed",
checks: { schema_valid: null, hash_matches: null, signature_valid: null, ens_match: null },
...instancePayload(),
});
}
Expand Down
Loading