feat: script deterministic V1 demo state

This commit is contained in:
golem
2026-08-21 02:17:18 -06:00
parent 6dcbb03f3c
commit 52935acf5e
10 changed files with 1010 additions and 1 deletions
+220
View File
@@ -0,0 +1,220 @@
import { randomUUID } from "node:crypto";
import { readFile, rename, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
export const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
const NETWORKS = {
anvil: { name: "anvil", chainId: 31337, canonical: "anvil.json", recovery: "make reset-local" },
"base-sepolia": { name: "base-sepolia", chainId: 84532, canonical: "base-sepolia.json", recovery: "make archive-base-manifest" },
};
export function networkSpec(network) {
const normalized = network === "baseSepolia" ? "base-sepolia" : network;
const spec = NETWORKS[normalized];
if (!spec) throw new Error(`unsupported deployment network: ${network}`);
return spec;
}
export async function preflightDeploy({ root = process.cwd(), network }) {
const spec = networkSpec(network);
const target = join(root, "deployments", spec.canonical);
try {
await readFile(target);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
throw new Error(`refusing to overwrite ${target}; recover safely with: ${spec.recovery}`);
}
export async function finalizeDeployment({ root = process.cwd(), rpc }) {
if (typeof rpc !== "function") throw new Error("finalizer requires an RPC function");
const pendingPath = join(root, "deployments", "pending.json");
const pending = await readManifest(pendingPath, { pending: true });
const spec = networkSpec(pending.network);
if (pending.chainId !== spec.chainId) throw new Error(`pending manifest chain ID does not match ${pending.network}`);
if (pending.deploymentBlock !== 0) throw new Error("pending manifest deploymentBlock must be 0");
const broadcastPath = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId), "run-latest.json");
const broadcast = await readJson(broadcastPath);
if (!Array.isArray(broadcast.transactions)) throw new Error("broadcast is partial: transactions are missing");
const proxyTransactions = broadcast.transactions.filter(
(transaction) => typeof transaction?.contractAddress === "string"
&& transaction.contractAddress.toLowerCase() === pending.proxy.toLowerCase()
&& transaction.transactionType === "CREATE"
&& typeof transaction.hash === "string"
);
if (proxyTransactions.length !== 1) {
throw new Error(`expected exactly one proxy creation transaction, found ${proxyTransactions.length}`);
}
const proxyTransaction = proxyTransactions[0];
const receipt = await rpc("eth_getTransactionReceipt", [proxyTransaction.hash]);
if (!receipt) throw new Error(`missing receipt for proxy transaction ${proxyTransaction.hash}`);
if (!isSuccessfulReceipt(receipt.status)) throw new Error(`proxy receipt ${proxyTransaction.hash} was not successful`);
const deploymentBlock = parseRpcQuantity(receipt.blockNumber, "receipt block number");
if (deploymentBlock === 0) throw new Error("receipt block number must be nonzero");
const actualChainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID");
if (actualChainId !== pending.chainId) {
throw new Error(`RPC chain ID ${actualChainId} does not match pending manifest chain ID ${pending.chainId}`);
}
for (const [label, address] of [["token", pending.token], ["proxy", pending.proxy], ["implementation", pending.implementation]]) {
const code = await rpc("eth_getCode", [address, "latest"]);
if (typeof code !== "string" || !/^0x[0-9a-fA-F]+$/.test(code) || code.length <= 2) {
throw new Error(`${label} ${address} has no code`);
}
}
const storage = await rpc("eth_getStorageAt", [pending.proxy, IMPLEMENTATION_SLOT, "latest"]);
if (slotAddress(storage) !== pending.implementation.toLowerCase()) {
throw new Error("proxy implementation slot does not match pending manifest implementation");
}
const confirmed = { ...pending, deploymentBlock };
validateManifest(confirmed);
const path = join(root, "deployments", spec.canonical);
await atomicWriteJson(path, confirmed);
return { path, manifest: confirmed };
}
export async function readManifest(path, { pending = false } = {}) {
const manifest = await readJson(path);
validateManifest(manifest, { pending });
return manifest;
}
export function validateManifest(manifest, { pending = false } = {}) {
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw new Error("manifest must be a JSON object");
rejectSecretBearingContent(manifest);
if (manifest.schemaVersion !== 1) throw new Error("manifest schemaVersion must be 1");
const spec = networkSpec(manifest.network);
if (manifest.chainId !== spec.chainId) throw new Error(`manifest chain ID does not match ${manifest.network}`);
if (!Number.isSafeInteger(manifest.deploymentBlock) || manifest.deploymentBlock < (pending ? 0 : 1)) {
throw new Error(`manifest deploymentBlock must be ${pending ? "a nonnegative integer" : "at least 1"}`);
}
assertPublicUrl(manifest.rpcUrl, "rpcUrl", false);
assertPublicUrl(manifest.explorerUrl, "explorerUrl", true);
for (const field of ["token", "proxy", "implementation", "owner"]) assertAddress(manifest[field], field);
if (!Array.isArray(manifest.actorLabels) || !Array.isArray(manifest.actors) || manifest.actorLabels.length !== manifest.actors.length || manifest.actors.length === 0) {
throw new Error("manifest actors and actorLabels must be nonempty parallel arrays");
}
const labels = new Set();
const actors = new Set();
for (let index = 0; index < manifest.actors.length; index += 1) {
const label = manifest.actorLabels[index];
if (typeof label !== "string" || label.trim() === "" || labels.has(label)) throw new Error("manifest actor labels must be unique nonempty strings");
labels.add(label);
assertAddress(manifest.actors[index], `actors[${index}]`);
const actor = manifest.actors[index].toLowerCase();
if (actors.has(actor)) throw new Error("manifest actors must be unique");
actors.add(actor);
}
}
export async function atomicWriteJson(path, value) {
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`);
}
export async function atomicWrite(path, contents, io = { writeFile, rename }) {
const temporary = join(dirname(path), `.${randomUUID()}.tmp`);
await io.writeFile(temporary, contents, { mode: 0o600 });
await io.rename(temporary, path);
}
function assertAddress(value, label) {
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value) || /^0x0{40}$/i.test(value)) {
throw new Error(`manifest ${label} must be a nonzero address`);
}
}
function assertPublicUrl(value, label, allowEmpty) {
if (allowEmpty && value === "") return;
if (typeof value !== "string") throw new Error(`manifest ${label} must be a URL`);
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error(`manifest ${label} must be a URL`);
}
if (!/^https?:$/.test(parsed.protocol)) throw new Error(`manifest ${label} must use http or https`);
if (parsed.username || parsed.password) throw new Error(`manifest ${label} contains credentials`);
for (const key of parsed.searchParams.keys()) {
if (/(?:key|token|secret|password|credential|private)/i.test(key)) {
throw new Error(`manifest ${label} contains a secret-bearing query parameter`);
}
}
}
function rejectSecretBearingContent(value, path = "") {
if (Array.isArray(value)) {
value.forEach((item, index) => rejectSecretBearingContent(item, `${path}[${index}]`));
return;
}
if (!value || typeof value !== "object") return;
for (const [key, nested] of Object.entries(value)) {
const nestedPath = path ? `${path}.${key}` : key;
if (/(?:private[_ -]?key|mnemonic|secret|password|credential|api[_ -]?key)/i.test(key)) {
throw new Error(`manifest contains secret-bearing field ${nestedPath}`);
}
rejectSecretBearingContent(nested, nestedPath);
}
}
function isSuccessfulReceipt(status) {
return status === "0x1" || status === 1 || status === "1";
}
function parseRpcQuantity(value, label) {
if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is not a hexadecimal RPC quantity`);
const parsed = Number.parseInt(value, 16);
if (!Number.isSafeInteger(parsed)) throw new Error(`${label} exceeds JavaScript safe integer range`);
return parsed;
}
function slotAddress(value) {
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(value)) throw new Error("proxy implementation slot response is invalid");
return `0x${value.slice(-40)}`.toLowerCase();
}
async function readJson(path) {
try {
return JSON.parse(await readFile(path, "utf8"));
} catch (error) {
if (error instanceof SyntaxError) throw new Error(`invalid JSON at ${path}`);
throw error;
}
}
function fetchRpc(rpcUrl) {
let nextId = 1;
return async (method, params) => {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params }),
});
if (!response.ok) throw new Error(`RPC ${method} returned HTTP ${response.status}`);
const body = await response.json();
if (body.error) throw new Error(`RPC ${method} failed: ${body.error.message ?? "unknown error"}`);
return body.result;
};
}
async function main(argv) {
const [command, network, ...rest] = argv;
if (command === "preflight-deploy" && network && rest.length === 0) return preflightDeploy({ network });
if (command === "deploy") {
if (network !== "--rpc-url" || typeof rest[0] !== "string" || rest.length !== 1) throw new Error("usage: finalize-manifest.mjs deploy --rpc-url <url>");
return finalizeDeployment({ rpc: fetchRpc(rest[0]) });
}
throw new Error("usage: finalize-manifest.mjs preflight-deploy <anvil|base-sepolia> | deploy --rpc-url <url>");
}
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
main(process.argv.slice(2)).catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
+27
View File
@@ -0,0 +1,27 @@
import { readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { atomicWrite, networkSpec, readManifest } from "./finalize-manifest.mjs";
export async function selectManifest({ root = process.cwd(), network }) {
const spec = networkSpec(network);
const source = join(root, "deployments", spec.canonical);
await readManifest(source);
const contents = await readFile(source);
const active = join(root, "deployments", "active.json");
await atomicWrite(active, contents);
return { source, active };
}
async function main(argv) {
if (argv.length !== 1) throw new Error("usage: select-manifest.mjs <anvil|base-sepolia>");
return selectManifest({ network: argv[0] });
}
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
main(process.argv.slice(2)).catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
+169
View File
@@ -0,0 +1,169 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import test from "node:test";
import { atomicWrite, finalizeDeployment, preflightDeploy } from "./finalize-manifest.mjs";
import { selectManifest } from "./select-manifest.mjs";
const TOKEN = "0x1000000000000000000000000000000000000001";
const PROXY = "0x2000000000000000000000000000000000000002";
const IMPLEMENTATION = "0x3000000000000000000000000000000000000003";
const OWNER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
test("preflight rejects an existing target canonical manifest with the safe recovery command", async () => {
await withFixture(async (root) => {
await writeJson(join(root, "deployments", "anvil.json"), manifest());
await assert.rejects(
() => preflightDeploy({ root, network: "anvil" }),
/make reset-local/
);
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532 }));
await assert.rejects(
() => preflightDeploy({ root, network: "base-sepolia" }),
/make archive-base-manifest/
);
});
});
test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => {
await withFixture(async (root) => {
const pending = manifest({ deploymentBlock: 0 });
await writeJson(join(root, "deployments", "pending.json"), pending);
await writeJson(join(root, "deployments", "active.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 }));
await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY });
const output = await finalizeDeployment({ root, rpc: fakeRpc() });
assert.equal(output.path, join(root, "deployments", "anvil.json"));
assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 });
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 88 }));
});
});
test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secret data without touching confirmed files", async () => {
const cases = [
["failed receipt", { rpc: fakeRpc({ receipt: { status: "0x0", blockNumber: "0x2a" } }) }, /not successful/],
["missing receipt", { rpc: fakeRpc({ receipt: null }) }, /missing receipt/],
["ambiguous proxy transaction", { broadcast: { extraProxy: true } }, /exactly one/],
["partial broadcast", { broadcast: { omitProxy: true } }, /exactly one/],
["non-creation proxy transaction", { broadcast: { transactionType: "CALL" } }, /exactly one/],
["wrong chain", { rpc: fakeRpc({ chainId: "0x14a34" }) }, /chain ID/],
["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/],
["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/],
["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret/i],
];
for (const [name, options, expected] of cases) {
await withFixture(async (root) => {
const pending = options.pending ?? manifest({ deploymentBlock: 0 });
await writeJson(join(root, "deployments", "pending.json"), pending);
await writeJson(join(root, "deployments", "anvil.json"), manifest({ deploymentBlock: 7 }));
await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 }));
await writeBroadcast(root, pending, options.broadcast);
const beforeCanonical = await readFile(join(root, "deployments", "anvil.json"));
const beforeActive = await readFile(join(root, "deployments", "active.json"));
await assert.rejects(() => finalizeDeployment({ root, rpc: options.rpc ?? fakeRpc() }), expected, name);
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical, name);
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive, name);
});
}
});
test("selection atomically replaces active with only a valid named canonical manifest", async () => {
await withFixture(async (root) => {
const anvil = manifest({ deploymentBlock: 31 });
const base = manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 32 });
await writeJson(join(root, "deployments", "anvil.json"), anvil);
await writeJson(join(root, "deployments", "base-sepolia.json"), base);
await selectManifest({ root, network: "anvil" });
const anvilBytes = await readFile(join(root, "deployments", "anvil.json"));
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes);
await selectManifest({ root, network: "base-sepolia" });
assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes);
assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base);
await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "base-sepolia", chainId: 84532, deploymentBlock: 0 }));
const beforeActive = await readFile(join(root, "deployments", "active.json"));
await assert.rejects(() => selectManifest({ root, network: "base-sepolia" }), /deploymentBlock/);
assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive);
});
});
test("atomic writes stage a same-directory temporary file before renaming it into place", async () => {
const target = "/tmp/deployments/active.json";
const calls = [];
const io = {
writeFile: async (path, contents) => calls.push(["write", path, contents]),
rename: async (source, destination) => calls.push(["rename", source, destination]),
};
await atomicWrite(target, "confirmed", io);
assert.equal(calls[0][0], "write");
assert.equal(dirname(calls[0][1]), dirname(target));
assert.notEqual(calls[0][1], target);
assert.deepEqual(calls[1], ["rename", calls[0][1], target]);
});
function manifest(overrides = {}) {
return {
schemaVersion: 1,
network: "anvil",
chainId: 31337,
deploymentBlock: 1,
rpcUrl: "http://127.0.0.1:8545",
explorerUrl: "",
token: TOKEN,
proxy: PROXY,
implementation: IMPLEMENTATION,
owner: OWNER,
actorLabels: ["owner", "Alice", "Bob"],
actors: [OWNER, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"],
...overrides,
};
}
async function withFixture(fn) {
const root = await mkdtemp(join(tmpdir(), "uups-finalizer-"));
try {
await import("node:fs/promises").then(({ mkdir }) => mkdir(join(root, "deployments"), { recursive: true }));
await fn(root);
} finally {
await rm(root, { recursive: true, force: true });
}
}
async function writeBroadcast(root, pending, options = {}) {
const directory = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId));
await import("node:fs/promises").then(({ mkdir }) => mkdir(directory, { recursive: true }));
const transactions = options.omitProxy
? [{ hash: "0xbbb", transactionType: "CREATE", contractAddress: pending.token }]
: [{ hash: "0xaaa", transactionType: options.transactionType ?? "CREATE", contractAddress: pending.proxy }];
if (options.extraProxy) transactions.push({ hash: "0xccc", transactionType: "CREATE", contractAddress: pending.proxy });
await writeJson(join(directory, "run-latest.json"), { transactions });
}
function fakeRpc(overrides = {}) {
return async (method, params) => {
if (method === "eth_chainId") return overrides.chainId ?? "0x7a69";
if (method === "eth_getTransactionReceipt") return Object.hasOwn(overrides, "receipt") ? overrides.receipt : { status: "0x1", blockNumber: "0x2a" };
if (method === "eth_getCode") return params[0].toLowerCase() === overrides.missingCode?.toLowerCase() ? "0x" : "0x6000";
if (method === "eth_getStorageAt") {
assert.equal(params[1], IMPLEMENTATION_SLOT);
return `0x000000000000000000000000${(overrides.slot ?? IMPLEMENTATION).slice(2)}`;
}
throw new Error(`unexpected RPC method: ${method}`);
};
}
async function writeJson(path, value) {
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
}
async function readJson(path) {
return JSON.parse(await readFile(path, "utf8"));
}