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
19 changes: 17 additions & 2 deletions .github/workflows/relayer-health.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Post-spike relayer health
name: Production judged-path health

on:
schedule:
Expand All @@ -14,13 +14,14 @@ concurrency:

jobs:
health:
name: Verify production submission and privacy boundary
name: Verify judged path, submission, and privacy boundary
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check HTTPS health and security boundary
env:
HEALTH_URL: https://afterlight-relayer-phase-a.qdworld001.workers.dev/health
APP_URL: https://afterlight.dolepee.com
run: |
set -euo pipefail
evidence_dir="$(mktemp -d)"
Expand All @@ -46,6 +47,8 @@ jobs:
.executor.executable == true and
.balance.status == "ok" and
.balance.alert == false and
.claimCapacity.status == "ready" and
.claimCapacity.fundingStatus == "ready" and
.privacy.payloadLogging == false and
.privacy.appKeysHeld == false and
.privacy.walletAddressesRequired == false
Expand All @@ -54,3 +57,15 @@ jobs:
grep -Eiq '^cache-control: no-store' "$evidence_dir/headers"
grep -Eiq '^content-security-policy:.*default-src .none.' "$evidence_dir/headers"
grep -Eiq '^x-content-type-options: nosniff' "$evidence_dir/headers"

curl \
--fail-with-body \
--silent \
--show-error \
--retry 3 \
--retry-all-errors \
--connect-timeout 10 \
--max-time 20 \
--output "$evidence_dir/app.html" \
"$APP_URL"
grep -Fq '<title>Afterlight' "$evidence_dir/app.html"
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ An owner privately funds a fixed reserve through STRK20 and remains in control t

[![CI](https://github.com/dolepee/afterlight/actions/workflows/ci.yml/badge.svg)](https://github.com/dolepee/afterlight/actions/workflows/ci.yml)

| Rubric | Present evidence |
|---|---|
| STRK20 integration depth | Private funding, cancellation/refund, and exact-note recovery through the canonical pool and custom Afterlight helper |
| Working Mainnet product | Public Ready X app, deployed contract, five qualifying receipts, and a founder-operated E3 lifecycle |
| Innovation | Authenticated inactivity, heartbeat, veto, designated-successor authorization, and one exact private destination |
| Open source | Reproducible artifacts, CI, read-only Mainnet verifier, threat model, operations guide, and MIT license |

## Current status

**Evidence level: E3 public completion.** The complete recovery mechanism has run through the deployed public app on Starknet Mainnet. Five successful transactions touch both the canonical STRK20 pool and Afterlight. The fresh public Recovery Drill completed private funding, heartbeat, request, veto, a second request, and [exact-note recovery](https://voyager.online/tx/0x722033f7fd0397ff4d3845428c98cad885b6a63824f7c78a2b7e1d7d6f5c1b6); Ready X then showed the successor's shielded balance increase from `7 STRK` to `8 STRK` while the neutral sponsor paid the pool and network fees.
Expand Down Expand Up @@ -75,6 +82,7 @@ scarb --profile spike-inline-56 build

npm --prefix client ci
npm --prefix client run verify:locked-artifacts
npm --prefix client run verify:mainnet
npm --prefix client test

npm --prefix relayer ci
Expand Down
10 changes: 7 additions & 3 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ operator primitives. It has no public product UI or production storage. Its
quote, preflight, and operator tools can read Mainnet; only the explicitly labelled
operator buttons can request a wallet declaration or deployment, and each still
requires a visible Ready X confirmation. The deployed Mainnet release and its
four qualifying receipts are recorded in [`../docs/MAINNET.md`](../docs/MAINNET.md).
five qualifying receipts are recorded in [`../docs/MAINNET.md`](../docs/MAINNET.md).

```sh
npm ci
Expand All @@ -20,8 +20,12 @@ Ready exactly an `OPEN` transfer followed by the helper invoke. They never add a
public self-withdraw. Ready adds its own private paymaster-fee withdrawal during
submission. Afterlight reproduced this exact-note route through its own helper
for both `CANCEL_REFUND` and `CLAIM` on Mainnet. The contract states, liability,
pool allowance, exact destination notes, and four qualifying receipts reconcile;
Ready's wallet-visible post-exit balance remains the final E2 visibility check.
pool allowance, exact destination notes, and five qualifying receipts reconcile;
fresh Ready reads proved the E2 and public E3 shielded-balance increases.

Run `npm run verify:mainnet` for a read-only check of the five qualifying
receipts, deployed class and configuration, neutral control senders, terminal
vault states, and zero locked liability.

Application secrets are held by `LocalStarkKey`; ordinary serialization exposes only the public key. Raw secret export requires the explicitly named backup method and confirmation constant. Relayer requests contain only signed public calldata for `HEARTBEAT`, `REQUEST`, or `VETO`.

Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"operator:build": "esbuild tools/mainnet-operator.ts --bundle --format=esm --platform=browser --outfile=tools/mainnet-operator.js",
"preflight:build": "esbuild tools/private-exit-preflight.ts --bundle --format=esm --platform=browser --minify --alias:starknet=./tools/starknet-narrow.ts --metafile=tools/private-exit-preflight.meta.json --outfile=tools/private-exit-preflight.js",
"verify:locked-artifacts": "node tools/verify-locked-artifacts.mjs",
"verify:mainnet": "node tools/verify-mainnet.mjs",
"quote:mainnet": "node tools/quote-declare-deploy.mjs",
"quote:ready-deploy": "node tools/quote-ready-deployment.mjs",
"quote:ready-build": "esbuild tools/ready-deployment-data.ts --bundle --format=esm --platform=browser --outfile=tools/ready-quote.js"
Expand Down
144 changes: 144 additions & 0 deletions client/tools/verify-mainnet.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { RpcProvider } from "starknet";

const ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
const RPC_URL = process.env.STARKNET_RPC_URL ?? "https://rpc.starknet.lava.build";
const provider = new RpcProvider({ nodeUrl: RPC_URL });
const RECEIPT_RPC_URLS = [
RPC_URL,
process.env.STARKNET_FALLBACK_RPC_URL ?? "https://api.cartridge.gg/x/starknet/mainnet/rpc/v0_10",
];
const receiptRpcUrls = [...new Set(RECEIPT_RPC_URLS)];
const receiptProviders = receiptRpcUrls.map(
(nodeUrl) => new RpcProvider({ nodeUrl }),
);

const expected = {
chainId: "0x534e5f4d41494e",
contract: "0x06e8b6e49b4366e0dc6a35eee722b417c718988eca3f4a0c298bdf8785261c25",
classHash: "0x05da9866f62cc6dd1e380e8d9206e78a752b460abdb802070e0be1208ec7b1a6",
pool: "0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a",
token: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
neutral: "0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46",
qualifyingSenders: {
"0x030ea14ac22e5806e382658971b686692af280bf2f02173a430f572921121722":
"0x073c97f6049ecfaac9284686454550e6d74769eb6ebd8fc014c12eba4ca0dd7",
"0x69e2345ae8816986a709de84f0dcb571b5d092400d6c53bf90197480102c0fb":
"0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46",
"0x036e003396fe360ae7fe4766646f493c0eb579d82509652559d40e460770682a":
"0x03dc021b06b8f0a5038b238b71ef5dd6d94b3efdbb7c5dafa1d914c5e89fa55a",
"0x11c990aea864e755630d41fd1292620c313b3f64407fc0b3a902544c67c8098":
"0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46",
"0x722033f7fd0397ff4d3845428c98cad885b6a63824f7c78a2b7e1d7d6f5c1b6":
"0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46",
},
config: [
"0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a",
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
"1000000000000000000", "2592000", "604800", "300", "300", "31536000", "900", "300",
],
vaults: [
{ name: "Vault A", id: "0x06d6c75f3625806e4e186e94e98c36074cb6a53b990cf8b5d98b91f1b9931d7", state: 4n },
{ name: "Vault B", id: "0x00df091b09941850386624da955ea4b9a7bedea70b1fe03734a5ddfa291fa5b2", state: 3n },
{ name: "Public E3 vault", id: "0x03ace87fc2d2fc6b1849ce77de6460900a34168541e4cea04e09e34e04d2b0", state: 3n },
],
controls: [
"0x0500ea24d721ce9c051ee7ef44bb4f786f26b0363fbfbe7c1b71facf62f9f7ab",
"0x02a5d7cd844e5dbae9b6c6a3f1ab53a2d2cedce33c5b20a53f04db247d69b437",
"0x06cb079f8415e74f609e05df8ce5357c5706f1f266b00539e236a76624a0b513",
"0x041a44e82131392092b54edc305bb6dc20708301623ceb519ed0006ee044fc90",
],
};

const sameFelt = (left, right) => BigInt(left) === BigInt(right);
const assert = (condition, message) => { if (!condition) throw new Error(message); };
const unwrapRpcValue = (response) => response?.value ?? response;
const succeeded = (receipt) =>
receipt.execution_status === "SUCCEEDED" && receipt.finality_status === "ACCEPTED_ON_L1";
Comment thread
dolepee marked this conversation as resolved.
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const getL1Receipt = async (transactionHash) => {
let receipt;
let lastError;
for (let attempt = 0; attempt < 3; attempt += 1) {
for (const receiptProvider of receiptProviders) {
try {
receipt = unwrapRpcValue(await receiptProvider.getTransactionReceipt(transactionHash));
if (succeeded(receipt)) return receipt;
} catch (error) {
lastError = error;
}
}
if (attempt < 2) await wait(250 * (attempt + 1));
}
throw new Error(
`${transactionHash} is not SUCCEEDED / ACCEPTED_ON_L1 ` +
`(observed ${receipt?.execution_status ?? "unknown"} / ${receipt?.finality_status ?? "unknown"}; ` +
`last RPC error: ${lastError instanceof Error ? lastError.message : "none"}).`,
);
};
const hasEventFrom = (receipt, address) =>
receipt.events.some((event) => sameFelt(event.from_address, address));
const calldataContains = (transaction, address) =>
Array.isArray(transaction.calldata) && transaction.calldata.some((felt) => sameFelt(felt, address));

assert(sameFelt(await provider.getChainId(), expected.chainId), "RPC is not Starknet Mainnet.");
assert(sameFelt(await provider.getClassHashAt(expected.contract), expected.classHash), "Deployed class hash mismatch.");

const config = await provider.callContract({ contractAddress: expected.contract, entrypoint: "get_config", calldata: [] });
assert(config.length === expected.config.length, "Unexpected deployed configuration length.");
expected.config.forEach((felt, index) =>
assert(sameFelt(config[index], felt), `Deployed configuration mismatch at index ${index}.`),
);

const locked = await provider.callContract({
contractAddress: expected.contract,
entrypoint: "get_locked_by_token",
calldata: [expected.token],
});
assert(locked.length === 2 && locked.every((felt) => BigInt(felt) === 0n), "Afterlight liability is not zero.");

const declaration = JSON.parse(fs.readFileSync(path.join(ROOT, "strk20.json"), "utf8"));
assert(declaration.contracts.length === 1 && sameFelt(declaration.contracts[0], expected.contract), "strk20.json contract mismatch.");
assert(declaration.demo_url === "https://afterlight.dolepee.com", "strk20.json demo URL mismatch.");
assert(declaration.transactions.length === 5, "Expected exactly five qualifying transactions.");

for (const transactionHash of declaration.transactions) {
const [receipt, transaction] = await Promise.all([
getL1Receipt(transactionHash), provider.getTransactionByHash(transactionHash),
]);
Comment thread
dolepee marked this conversation as resolved.
assert(hasEventFrom(receipt, expected.pool), `${transactionHash} does not touch the canonical pool.`);
assert(hasEventFrom(receipt, expected.contract) || calldataContains(transaction, expected.contract), `${transactionHash} is not owned by Afterlight.`);
assert(
sameFelt(transaction.sender_address, expected.qualifyingSenders[transactionHash]),
`${transactionHash} outer sender does not match the audited release.`,
);
}

for (const transactionHash of expected.controls) {
const [receipt, transaction] = await Promise.all([
getL1Receipt(transactionHash), provider.getTransactionByHash(transactionHash),
]);
assert(sameFelt(transaction.sender_address, expected.neutral), `${transactionHash} was not submitted by the neutral relayer.`);
assert(hasEventFrom(receipt, expected.contract), `${transactionHash} has no Afterlight event.`);
}

for (const vault of expected.vaults) {
const state = await provider.callContract({ contractAddress: expected.contract, entrypoint: "get_vault", calldata: [vault.id] });
assert(BigInt(state[0]) === 1n, `${vault.name} does not exist.`);
assert(BigInt(state[1]) === vault.state, `${vault.name} is not terminal in the expected state.`);
}

process.stdout.write(`${JSON.stringify({
evidence: "AFTERLIGHT_MAINNET_VERIFIED",
chainId: expected.chainId,
contract: expected.contract,
classHash: expected.classHash,
qualifyingTransactions: declaration.transactions.length,
neutralControls: expected.controls.length,
terminalVaults: expected.vaults.length,
lockedLiability: "0",
rpcOrigins: receiptRpcUrls.map((url) => new URL(url).origin),
})}\n`);
12 changes: 11 additions & 1 deletion docs/MAINNET.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ artifact-identity proof for the live pool class.
| Sierra class hash | `0x05da9866f62cc6dd1e380e8d9206e78a752b460abdb802070e0be1208ec7b1a6` | Declared on Mainnet |
| CASM/compiled class hash | `0x055ba10e36aac8e21b3437f1413f009f6b17d3633c307941a4412ce73566251` | Declaration lock |
| Afterlight contract | [`0x06e8…61c25`](https://starkscan.co/contract/0x06e8b6e49b4366e0dc6a35eee722b417c718988eca3f4a0c298bdf8785261c25) | Deployed on Mainnet |
| Neutral relayer account | `0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46` | Deployed; spike controls complete |
| Neutral relayer account | `0x05b0b8cbda8eca89b88ae6975c80a880b0164a853c6ed881a56e39e4622edd46` | Deployed; public E3 controls complete |

Run `scarb --profile spike-inline-56 build` and
`npm --prefix client run verify:locked-artifacts` to recompute the exact hashes.
A read-only live release verification is available as
`npm --prefix client run verify:mainnet`.
A different source, dependency resolution, compiler, profile, constructor, or
deployer must be treated as a different release and re-verified.

Expand All @@ -60,3 +62,11 @@ reconciliation is `6 STRK -> 7 STRK`: the exact `+1 STRK` recovery output was
added to the beneficiary while the neutral sponsor paid the separate `6 STRK`
pool fee. The public E3 vault is also `CLAIMED`; its immediate Ready X
reconciliation is `7 STRK -> 8 STRK`. Final video production remains pending.

The public E3 lifecycle was founder-operated. An unrelated owner-successor E4
completion is not claimed.

Historical identifiers such as the `spike-inline-56` compiler profile and the
relayer's `phase-a` hostname are pinned release identifiers. They do not mean
the deployed public release is still an inert preparation environment; changing
them would break reproducibility or the stable public endpoint.