fix: harden upgrade publication and refresh
This commit is contained in:
+51
-17
@@ -89,7 +89,7 @@ export async function finalizeDeployment({ root = process.cwd(), rpc }) {
|
||||
return { path, manifest: confirmed };
|
||||
}
|
||||
|
||||
export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
||||
export async function finalizeUpgrade({ root = process.cwd(), rpc, writeManifest = atomicWriteJson }) {
|
||||
if (typeof rpc !== "function") throw new Error("finalizer requires an RPC function");
|
||||
const pendingPath = join(root, "deployments", "upgrade-pending.json");
|
||||
const pending = await readJson(pendingPath);
|
||||
@@ -101,15 +101,12 @@ export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
||||
const canonicalPath = join(root, "deployments", spec.canonical);
|
||||
const canonical = await readManifest(canonicalPath);
|
||||
const [activeBytes, canonicalBytes] = await Promise.all([readFile(activePath), readFile(canonicalPath)]);
|
||||
if (!activeBytes.equals(canonicalBytes) || JSON.stringify(active) !== JSON.stringify(canonical)) {
|
||||
throw new Error("active and canonical manifest identity must match before upgrade finalization");
|
||||
}
|
||||
|
||||
const chainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID");
|
||||
if (chainId !== active.chainId) throw new Error("RPC chain ID does not match active manifest");
|
||||
const methods = await readUpgradeMethods(root);
|
||||
|
||||
if (pending.mode === "noop") {
|
||||
assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes);
|
||||
const chainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID");
|
||||
if (chainId !== active.chainId) throw new Error("RPC chain ID does not match active manifest");
|
||||
const methods = await readUpgradeMethods(root);
|
||||
validateNoopMarker(pending, active);
|
||||
const latestBlock = parseRpcQuantity(await rpc("eth_blockNumber", []), "latest block number");
|
||||
if (latestBlock < pending.observedBlock) throw new Error("live block precedes no-op observation block");
|
||||
@@ -122,8 +119,11 @@ export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
||||
return { mode: "noop", path: canonicalPath, manifest: active };
|
||||
}
|
||||
|
||||
validateUpgradeMarker(pending, active);
|
||||
const broadcastPath = join(root, "broadcast", "UpgradeV2.s.sol", String(active.chainId), "run-latest.json");
|
||||
const previous = recoverableUpgradeBaseline(pending, active, canonical, activeBytes, canonicalBytes);
|
||||
const chainId = parseRpcQuantity(await rpc("eth_chainId", []), "RPC chain ID");
|
||||
if (chainId !== previous.chainId) throw new Error("RPC chain ID does not match active manifest");
|
||||
const methods = await readUpgradeMethods(root);
|
||||
const broadcastPath = join(root, "broadcast", "UpgradeV2.s.sol", String(previous.chainId), "run-latest.json");
|
||||
const broadcast = await readJson(broadcastPath);
|
||||
if (!Array.isArray(broadcast.transactions)) throw new Error("upgrade broadcast is partial: transactions are missing");
|
||||
const hashes = [...new Set(broadcast.transactions.map((transaction) => transaction?.hash)
|
||||
@@ -133,26 +133,60 @@ export async function finalizeUpgrade({ root = process.cwd(), rpc }) {
|
||||
transaction: await rpc("eth_getTransactionByHash", [hash]),
|
||||
})));
|
||||
const matching = resolvedTransactions.filter(({ transaction }) =>
|
||||
typeof transaction?.to === "string" && sameAddress(transaction.to, active.proxy));
|
||||
typeof transaction?.to === "string" && sameAddress(transaction.to, previous.proxy));
|
||||
if (matching.length !== 1) throw new Error(`expected exactly one live upgrade transaction to proxy, found ${matching.length}`);
|
||||
const receipt = await rpc("eth_getTransactionReceipt", [matching[0].hash]);
|
||||
if (!receipt || !isSuccessfulReceipt(receipt.status)) throw new Error("upgrade receipt was not successful");
|
||||
const upgradeBlock = parseRpcQuantity(receipt.blockNumber, "upgrade receipt block number");
|
||||
if (upgradeBlock === 0) throw new Error("upgrade receipt block number must be nonzero");
|
||||
if (!Array.isArray(receipt.logs) || !receipt.logs.some((log) => isUpgradeLog(log, active.proxy, pending.implementation))) {
|
||||
if (!Array.isArray(receipt.logs) || !receipt.logs.some((log) => isUpgradeLog(log, previous.proxy, pending.implementation))) {
|
||||
throw new Error("successful upgrade receipt is missing the expected Upgraded event");
|
||||
}
|
||||
|
||||
await assertLiveVersionAndImplementation({ rpc, active, implementation: pending.implementation, methods });
|
||||
await assertUpgradeSnapshot({ rpc, active, pending, methods });
|
||||
const updated = { ...active, implementation: pending.implementation };
|
||||
await assertLiveVersionAndImplementation({ rpc, active: previous, implementation: pending.implementation, methods });
|
||||
await assertUpgradeSnapshot({ rpc, active: previous, pending, methods });
|
||||
const updated = { ...previous, implementation: pending.implementation };
|
||||
validateManifest(updated);
|
||||
await atomicWriteJson(canonicalPath, updated);
|
||||
await atomicWriteJson(activePath, updated);
|
||||
const updatedBytes = Buffer.from(`${JSON.stringify(updated, null, 2)}\n`);
|
||||
if (!canonicalBytes.equals(updatedBytes)) await writeManifest(canonicalPath, updated);
|
||||
if (!activeBytes.equals(updatedBytes)) await writeManifest(activePath, updated);
|
||||
const [confirmedCanonical, confirmedActive] = await Promise.all([
|
||||
readFile(canonicalPath),
|
||||
readFile(activePath),
|
||||
]);
|
||||
if (!confirmedCanonical.equals(updatedBytes) || !confirmedActive.equals(updatedBytes)) {
|
||||
throw new Error("active and canonical manifests did not converge after upgrade finalization");
|
||||
}
|
||||
await rm(pendingPath);
|
||||
return { mode: "upgrade", path: canonicalPath, manifest: updated, upgradeBlock };
|
||||
}
|
||||
|
||||
function assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes) {
|
||||
if (!activeBytes.equals(canonicalBytes) || JSON.stringify(active) !== JSON.stringify(canonical)) {
|
||||
throw new Error("active and canonical manifest identity must match before upgrade finalization");
|
||||
}
|
||||
}
|
||||
|
||||
function recoverableUpgradeBaseline(pending, active, canonical, activeBytes, canonicalBytes) {
|
||||
assertAddress(pending?.previousImplementation, "upgrade previous implementation");
|
||||
assertAddress(pending?.implementation, "upgrade implementation");
|
||||
const allowedImplementation = (manifest) => sameAddress(manifest.implementation, pending.previousImplementation)
|
||||
|| sameAddress(manifest.implementation, pending.implementation);
|
||||
if (!allowedImplementation(active) || !allowedImplementation(canonical)) {
|
||||
throw new Error("active and canonical manifest divergence is not justified by the upgrade marker");
|
||||
}
|
||||
if (sameAddress(active.implementation, canonical.implementation)) {
|
||||
assertConfirmedManifestPair(active, canonical, activeBytes, canonicalBytes);
|
||||
}
|
||||
const previousActive = { ...active, implementation: pending.previousImplementation };
|
||||
const previousCanonical = { ...canonical, implementation: pending.previousImplementation };
|
||||
if (JSON.stringify(previousActive) !== JSON.stringify(previousCanonical)) {
|
||||
throw new Error("active and canonical manifest divergence is not justified by the upgrade marker");
|
||||
}
|
||||
validateUpgradeMarker(pending, previousActive);
|
||||
return previousActive;
|
||||
}
|
||||
|
||||
function validateNoopMarker(marker, active) {
|
||||
assertExactKeys(marker, ["mode", "chainId", "observedBlock", "ownerNonce", "proxy", "implementation"], "no-op marker");
|
||||
for (const field of ["chainId", "observedBlock", "ownerNonce"]) {
|
||||
|
||||
Reference in New Issue
Block a user