diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..fce858d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,87 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this project is
+
+Validator Beat is a **client-side, public-good self-assessment** for Ethereum validator operators. Six banded questions → six "pizza slices" (each green/yellow/red) → one Stage (0, 1, or 2). The whole thing runs in the browser; nothing is submitted, stored, or fetched at runtime. The static export ships to GitHub Pages.
+
+Authoritative product plan: [`docs/PLAN.md`](./docs/PLAN.md). Read it before changing the rubric, stages, or slice set.
+
+## Domain context: why these six slices
+
+The assessment exists because Ethereum validators have several distinct **single-points-of-failure (SPoF)** that operators routinely conflate or overlook. Obol's distributed-validator-technology (DVT) thesis is that an operator should be able to lose any one piece of their stack — a key custodian, a client, a host, an OS, a CPU vendor, a jurisdiction — without (a) leaking signing power [the "Safety" failure mode, Stage 1] or (b) going offline [the "Liveness" failure mode, Stage 2].
+
+The six slices are deliberately ordered worst-failure-first:
+
+1. **Key Custody** — concentration of signing power (a Safety risk; one compromise can sign with the operator's stake)
+2. **Client Diversity** — supermajority-client bug exposure (a Safety risk; refuse-to-attest turns it into mere downtime)
+3. **Infrastructure** — hosting-provider concentration (a Liveness risk)
+4. **OS Diversity** — distro monoculture (supply-chain risk; can be Safety *or* Liveness depending on the compromise)
+5. **CPU Architecture** — ISA monoculture (hardware-level supply-chain / side-channel risk)
+6. **Geographic** — jurisdictional concentration (Liveness + censorship risk)
+
+The bottom four slices (Infra / OS / CPU / Geo) **assume the operator runs per-validator DVT or a multiplexer** — i.e. duties are split across independent nodes so diversity translates into uptime rather than just redundancy. This premise is stated in `pages/methodology.tsx` and in each question's `helper` text.
+
+Banding thresholds (⅓ / ⅔) are chosen to map to the consensus-layer math: a ⅔ supermajority can finalize a wrong chain, so any single party holding ⅔ is a red; ⅓ is the safety threshold below which no single party can block or force anything.
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Browser (no backend) │
+│ │
+│ questions.ts ──► AssessmentApp ──► Pizza (SVG) │
+│ (copy) (useAssessment ▲ │
+│ state machine) │ │
+│ ▲ │ │
+│ │ Results / Blockers / Tips │
+│ │ │ │
+│ └──── lib/rubric ────────────┘ │
+│ (pure functions: stage, blockers, shareCode) │
+└─────────────────────────────────────────────────────────────┘
+```
+
+- **`lib/rubric/`** is the single source of truth for scoring. It exports `SLICES` (the canonical ordered list — also defines pizza slice order and share-code character order), `computeStage`, `blockers`, `getTip`, `shareCode`, `decodeShareCode`. Pure, fully unit-tested.
+- **`lib/assessment/questions.ts`** holds question copy + banded options. Each option's `color` is the rubric input — there is no separate scoring step in v0.1; the user's chosen band *is* the slice color.
+- **`hooks/useAssessment.ts`** is the state machine (intro → 6 questions → results) with a 420 ms auto-advance after each answer.
+- **`pages/[code].tsx`** statically generates **all 729 share pages** (3⁶) at build time via `getStaticPaths` + `lib/theme/share-codes.ts`. Each share URL like `/GYRYGG` rehydrates the assessment to that result.
+- **`scripts/generate-og-images.ts`** runs *before* `next build` (see `package.json` `build` script) and pre-renders **729 OG preview PNGs** into `public/og/` via `sharp`. Both the static pages and the OG images must stay in sync with `decodeShareCode`.
+
+### Hard constraints from the static-export setup
+
+- `next.config.js` sets `output: "export"` — **no API routes, no `getServerSideProps`, no runtime image optimization** (`images.unoptimized: true`). All data must be derivable at build time or in the browser.
+- `basePath` / `assetPrefix` come from `NEXT_PUBLIC_BASE_PATH`. GitHub Pages project deploys need `/validator-beat`; the custom domain (`validatorbeat.com`) needs it empty. Don't hardcode paths — use `next/link` and `NEXT_PUBLIC_SITE_URL` (via `constants/index.ts` → `SITE_URL`).
+- `trailingSlash: true` — every internal link should match.
+- React/ReactDOM are aliased to local copies in both `next.config.js` and `jest.config.ts` to prevent `@obolnetwork/obol-ui` (which bundles its own peer deps) from pulling in a second React. **Don't remove these aliases or the `resolutions` block in `package.json`** without testing.
+
+### Path aliases
+
+`@components/*`, `@constants/*`, `@hooks/*`, `@lib/*`, `@public/*`, `@styles/*` — defined in both `tsconfig.json` and `jest.config.ts`. Add new top-level dirs to both if needed.
+
+## Commands
+
+| Command | What it does |
+|---|---|
+| `yarn dev` | Next dev server on :3000 |
+| `yarn test` | Jest (jsdom) — primarily `lib/rubric/*.test.ts` |
+| `yarn test -t "all green"` | Run a single test by name |
+| `yarn lint` | `next lint` (lints `pages`, `components`, `lib`, `constants`, `scripts`) |
+| `yarn tsc --noEmit` | Type-check only (also runs via husky pre-commit on staged files) |
+| `yarn build` | `build:og` → `next build` → static `out/` |
+| `yarn build:og` | Regenerate the 729 OG PNGs only (slow; run when share-card visuals change) |
+| `npx serve out` | Serve the static export locally to verify share pages, OG tags, and trailing slashes |
+
+Pre-commit (husky) runs `yarn test` + `yarn lint`. Lint-staged additionally runs `tsc --noEmit`, ESLint --fix, and Prettier on staged files.
+
+## Conventions worth knowing
+
+- **The rubric is the contract.** If you add, rename, or reorder a slice, you must update: `lib/rubric/types.ts` (`SliceId` union), `lib/rubric/index.ts` (`SLICES`, `TIPS`), `lib/assessment/questions.ts`, the `Pizza` component's slice order, and the share-code length/test (`/^[GYR_]{6}$/` regex in `decodeShareCode`). The share-code character order **is** `SLICES` order — breaking it invalidates every existing share link.
+- **Share codes are user-visible URLs.** Six chars, `G`/`Y`/`R`, one per slice. Treat as a stable wire format.
+- Stage rules live in one place — `computeStage` in `lib/rubric/index.ts`. Don't reimplement them in components.
+- Copy lives close to the rubric: question text in `lib/assessment/questions.ts`, slice `why` and remediation tips in `lib/rubric/index.ts`. Methodology page reads from `SLICES` so descriptions stay in sync.
+- Styling is a hybrid: `@obolnetwork/obol-ui` Stitches components (see `components/assessment/stitches.ts`) for the assessment shell, plus plain CSS files in `styles/` (CSS custom properties in `theme-tokens.css` are the palette source of truth — if you change pizza slice colors there, also update `lib/theme/tokens.ts`).
+
+## Deferred work (don't build yet unless asked)
+
+`data/operators/`, `content/methodology/`, `components/operators/`, and `lib/schemas/` are placeholder dirs for the **v1.2 operator registry** (YAML profiles + Zod validation + a `/operators` summary table). `scripts/validate-operators.ts` and `scripts/import-survey-csv.ts` are scaffolding for that phase. v0.1 deliberately does not ship any of this — the `/operators` route currently redirects to `/`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index b5e958d..9d58c98 100644
--- a/README.md
+++ b/README.md
@@ -84,3 +84,7 @@ Paste a live share link such as `https://validatorbeat.com/GYRYGG`. Debuggers ca
- Image loads: `https://validatorbeat.com/og/GYRYGG.png`
- `og:image` in page source matches that URL (must be absolute HTTPS)
- Slack / Discord / iMessage: paste the URL in a draft message to see the unfurl
+
+## License
+
+Licensed under the Apache License, Version 2.0. See [LICENSE](./LICENSE).
diff --git a/components/assessment/Intro.tsx b/components/assessment/Intro.tsx
index 2141c88..e2ac0d6 100644
--- a/components/assessment/Intro.tsx
+++ b/components/assessment/Intro.tsx
@@ -5,7 +5,6 @@ import {
IntroGoal,
IntroGoalText,
IntroLede,
- IntroNote,
IntroRoot,
IntroTitle,
StartButtonWrap,
@@ -22,8 +21,9 @@ export function Intro({ onStart }: IntroProps) {
A neutral resource for validator fault toleranceHow resilient is your validator setup?
- Six quick questions about how you run. Each one checks for a single point of failure — one
- party, client, provider, or place that, if it broke or turned hostile, could get you
+ Six quick questions about how you run. Each one checks for a single point of failure —
+ any party, client, provider, machine, or place that, if it broke or turned hostile, could
+ get you
slashed, knocked
offline, or
censored. Your answers fill the
@@ -37,11 +37,6 @@ export function Intro({ onStart }: IntroProps) {
-
- Assumes each validator runs as a distributed validator (DVT) or behind a{" "}
- multiplexer — its duties split across several nodes, so if one node's provider,
- OS, or region fails, the rest keep signing.
- Start the assessment →
diff --git a/components/assessment/Question.tsx b/components/assessment/Question.tsx
index 17c9220..b0b298f 100644
--- a/components/assessment/Question.tsx
+++ b/components/assessment/Question.tsx
@@ -12,6 +12,11 @@ import {
OptionSub,
QuestionCount,
QuestionHelper,
+ QuestionRefLink,
+ QuestionRefs,
+ QuestionRisk,
+ QuestionRiskBody,
+ QuestionRiskHead,
QuestionRoot,
QuestionSlice,
QuestionTitle,
@@ -74,6 +79,30 @@ export function Question({
);
})}
+ {(data.risk || (data.references && data.references.length > 0)) && (
+
+ {data.risk && (
+ <>
+ Why this matters
+ {data.risk}
+ >
+ )}
+ {data.references && data.references.length > 0 && (
+
+ {data.references.map((r) => (
+
+ {r.label} ↗
+
+ ))}
+
+ )}
+
+ )}
{index > 0 && onBack && (
← Back
diff --git a/components/assessment/Results.tsx b/components/assessment/Results.tsx
index c5dc2b7..3db2523 100644
--- a/components/assessment/Results.tsx
+++ b/components/assessment/Results.tsx
@@ -65,13 +65,13 @@ const STAGE = {
name: "Stage 0",
kind: "Getting started",
tone: "red" as const,
- line: "Every operator starts here. Clear the items in red below to reach Stage 1 — where no single party can get your validator slashed.",
+ line: "Every operator starts here. Clear the items in red below to reach Stage 1 — where no single failure can expose you to slashing.",
},
1: {
name: "Stage 1",
kind: "Safety",
tone: "yellow" as const,
- line: "No single party can get you slashed. One more climb to Stage 2 — where no single failure can take you offline or censor you.",
+ line: "No single failure can expose you to slashing. One more climb to Stage 2 — where no single point of failure can take you offline or censor you either.",
},
2: {
name: "Stage 2",
@@ -82,8 +82,8 @@ const STAGE = {
} as const;
const WHY_MAXED: Record = {
- keyCustody: "No single party can sign with your stake.",
- clientDiversity: "No supermajority-client bug can slash you.",
+ keyCustody: "No single compromise can sign with your stake.",
+ clientDiversity: "No supermajority-client fork can drag you in.",
infraDiversity: "No single provider can take your validator offline.",
osDiversity: "No single OS bug can stop your validator.",
cpuDiversity: "No single CPU-architecture supply-chain risk.",
@@ -92,7 +92,7 @@ const WHY_MAXED: Record = {
const SHARE_LINE: Record = {
0: "Has a single point of failure — for now.",
- 1: "Safe from slashing — no single party can get it slashed.",
+ 1: "Safe from slashing — no single failure can get it slashed.",
2: "Maximum resilience — can't be slashed, stopped, or censored.",
};
diff --git a/components/assessment/StageLadder.tsx b/components/assessment/StageLadder.tsx
index 6d743af..de460d8 100644
--- a/components/assessment/StageLadder.tsx
+++ b/components/assessment/StageLadder.tsx
@@ -24,7 +24,7 @@ const LADDER = [
n: 1 as const,
name: "Stage 1",
kind: "Safety",
- tag: "No single party can slash you",
+ tag: "No single failure can get you slashed",
tone: "yellow" as const,
},
{
diff --git a/components/assessment/stitches.ts b/components/assessment/stitches.ts
index 825a776..a16b2ba 100644
--- a/components/assessment/stitches.ts
+++ b/components/assessment/stitches.ts
@@ -237,16 +237,6 @@ export const IntroGoalText = styled(Text, {
"& b": { color: "$body" },
});
-export const IntroNote = styled(Text, {
- fontSize: "$2",
- lineHeight: 1.5,
- color: "$textMiddle",
- marginTop: "$xs",
- paddingLeft: "$xs",
- borderLeft: "2px solid $bg05",
- "& b": { color: "$textLight" },
-});
-
export const StartButtonWrap = styled(Box, {
alignSelf: "flex-start",
marginTop: "$md",
@@ -512,6 +502,46 @@ export const OptionCheck = styled(Text, {
},
});
+export const QuestionRisk = styled(Box, {
+ marginTop: "$md",
+ padding: "12px 14px",
+ backgroundColor: "$bg02",
+ border: "1px solid $bg05",
+ borderLeft: "3px solid $bg07",
+ borderRadius: "$3",
+});
+
+export const QuestionRiskHead = styled(Text, {
+ fontSize: "$1",
+ textTransform: "uppercase",
+ letterSpacing: "0.08em",
+ color: "$textMiddle",
+ fontWeight: 600,
+ marginBottom: 6,
+});
+
+export const QuestionRiskBody = styled(Text, {
+ fontSize: "$2",
+ lineHeight: 1.55,
+ color: "$textLight",
+});
+
+export const QuestionRefs = styled(Box, {
+ marginTop: 10,
+ display: "flex",
+ flexWrap: "wrap",
+ gap: "$xs",
+});
+
+export const QuestionRefLink = styled("a", {
+ fontSize: "$1",
+ color: "$textMiddle",
+ textDecoration: "underline",
+ textDecorationStyle: "dotted",
+ textUnderlineOffset: "2px",
+ "&:hover": { color: "$textLight" },
+});
+
export const BackButton = styled("button", {
all: "unset",
cursor: "pointer",
diff --git a/lib/assessment/questions.ts b/lib/assessment/questions.ts
index feeff00..e325dea 100644
--- a/lib/assessment/questions.ts
+++ b/lib/assessment/questions.ts
@@ -6,143 +6,183 @@ export type QuestionOption = {
sub: string;
};
+export type QuestionReference = {
+ label: string;
+ url: string;
+};
+
export type Question = {
q: string;
helper: string;
options: QuestionOption[];
+ /** Anonymized real-world incident illustrating the risk this slice mitigates. */
+ risk?: string;
+ /** Useful external resources — tools, dashboards, security features. */
+ references?: QuestionReference[];
};
export const QUESTIONS: Record = {
keyCustody: {
- q: "What's the largest share of your signing power that any one party controls — counting key backups the same as live keys?",
+ q: "What's the largest share of your signing power that any single point of failure could compromise, including both runtime keys and backups?",
helper:
- "A “party” is one machine, team, or person. With threshold signing, a party needs more than ⅔ of the key shares to sign on its own.",
+ "A “point of failure” is anywhere your signing power can converge: a person, a team, a machine, a custodian, or even one piece of software (an RCE bug in your validator client could leak whatever key material it holds). With threshold signing, no single compromise can sign unless it controls more than ⅔ of the shares.",
options: [
{
color: "green",
label: "No more than ⅓",
- sub: "Signing needs at least 3 independent parties to agree — no single compromise can sign.",
+ sub: "Producing a valid signature would take at least two separate compromises.",
},
{
color: "yellow",
label: "Between ⅓ and ⅔",
- sub: "One party holds a meaningful share, but still can't sign on its own.",
+ sub: "One failure holds a meaningful share, but still can't sign on its own.",
},
{
color: "red",
label: "More than ⅔",
- sub: "One party can sign on your behalf, by itself.",
+ sub: "A single failure can leak your private keys.",
},
],
+ risk: "A single key-management compromise once forced one large operator to preemptively exit roughly 10% of all Ethereum validators — billions of dollars of stake withdrawn, tens of millions of dollars in opportunity cost, as a precaution because no one could rule out that whole signing keys had been exposed. With threshold signing split across independent parties — for example a multi-operator distributed validator, or HA remote signers like Dirk or Web3Signer fronted by Vouch or Vero — no single compromise can reconstruct a usable key.",
+ references: [
+ { label: "VALOS: Key Custody Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-keys" },
+ { label: "VALOS: Key Management", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-key-management" },
+ { label: "VALOS: Signature Management", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-signature-management" },
+ { label: "Obol docs — distributed validators", url: "https://docs.obol.org/" },
+ ],
},
clientDiversity: {
- q: "How protected are you from a buggy client signing the wrong chain and getting you slashed?",
+ q: "If a client bug forks onto an incorrect chain, what stops your validator from signing along with it?",
helper:
- "“Refuse-to-attest” means your setup won't sign when its clients disagree, so a single client bug becomes downtime, not a slashing — via multi-operator DVT, a multiplexer like Vero, or a similar setup.",
+ "Two safeguards work together. First, halt on disagreement: run several independent clients and configure your stack (Charon, Vero, or Vouch all support this) to stop attesting when they disagree, so a client bug becomes downtime instead of slashing. Second, watch the supermajority: if every client you run sits inside a ⅔ supermajority that forks together, your clients never disagree and the halt never triggers. Check clientdiversity.org for current client shares.",
options: [
{
color: "green",
- label: "3+ independent clients, refuse-to-attest, plus a minority client",
- sub: "Your combined client share stays under ⅔ — a supermajority-client bug can't slash you.",
+ label: "Refuse-to-attest configured, 3+ independent clients, combined share under ⅔ of the network",
+ sub: "If a supermajority client forks, at least one of your clients disagrees and your validator halts instead of following.",
},
{
color: "yellow",
- label: "3+ independent clients that refuse to attest when they disagree",
- sub: "Safe from a single-client bug, but you're not adding minority-client share to the network.",
+ label: "Refuse-to-attest configured, 3+ independent clients, combined share could form a supermajority",
+ sub: "Safe from a single-client bug, but if all your clients fork the same way the halt never triggers and you sign the incorrect chain along with them.",
},
{
color: "red",
- label: "A single client, or no refuse-to-attest safeguard",
- sub: "A supermajority-client bug could drag you into signing the wrong chain.",
+ label: "Single client, or no refuse-to-attest safeguard",
+ sub: "A consensus-client bug could drag you into signing an incorrect chain, with no software safety net to catch it.",
},
],
+ risk: "On a recent public test network, a bug in one consensus client caused every validator using it to sign an incorrect chain. Because that client held a supermajority share of the testnet's validators, the result was mass slashing — the same pattern on mainnet would have destroyed billions of dollars of stake. Ethereum's slashing penalty already scales with how many validators are slashed in the same window, so a correlated event costs each affected validator far more than an isolated slashing would. Refuse-to-attest only saves you when your clients disagree; if they all run software in the bad supermajority, they fork in unison and the safeguard never fires.",
+ references: [
+ { label: "VALOS: Slashing Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-slashing" },
+ { label: "VALOS: Client Diversity", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-client-diversity" },
+ { label: "VALOS: Anti-Slashing DB", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-antislash-db" },
+ { label: "clientdiversity.org", url: "https://clientdiversity.org/" },
+ { label: "Obol docs — chain_split_halt", url: "https://docs.obol.org/" },
+ ],
},
infraDiversity: {
q: "Across the nodes that run your validators, what's the largest share on a single hosting provider?",
helper:
- "Diversity only buys uptime if your validator keeps signing when some nodes drop — i.e. it runs as a distributed validator (DVT) or behind a multiplexer. Count each cloud (AWS, Hetzner…) and “home / bare-metal” as its own provider.",
+ "This assumes an active/active setup: several machines cooperate to back the same stake, as in a multi-operator DVT or with a multiplexer like Vero fronting several beacon nodes. The share that matters is the share of those cooperating machines, not of stake. If your biggest provider goes down, it should only drop some of your machines while the validator keeps signing. Treat each cloud (AWS, Hetzner…) and home or bare-metal as its own provider.",
options: [
{
color: "green",
- label: "Less than 33%",
+ label: "Less than ⅓",
sub: "No single provider's outage can take your validator offline.",
},
{
color: "yellow",
- label: "33% to 66%",
+ label: "⅓ to ⅔",
sub: "One provider going down would drop a large share of your nodes.",
},
{
color: "red",
- label: "More than 66%",
+ label: "More than ⅔",
sub: "One provider going down takes your validator offline.",
},
],
+ risk: "A major hosting provider once suffered a security incident in which stored disk images may have been exposed. Any validator whose keystore and its decryption material lived on that provider's disks would have been at risk of complete key exfiltration in one stroke. Beyond breaches, single-provider outages routinely take large fractions of the network offline simultaneously — concentrating your nodes turns a vendor incident into your incident.",
+ references: [
+ { label: "VALOS: Infrastructure Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-infra" },
+ { label: "VALOS: Physically Distributed Infrastructure", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-distribute-hardware" },
+ { label: "VALOS: Utility Failure", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-protect-utilities" },
+ { label: "EIP-7716: Anti-correlation penalties", url: "https://eips.ethereum.org/EIPS/eip-7716" },
+ ],
},
osDiversity: {
- q: "How many distinct operating systems run across those nodes?",
+ q: "Could a compromise of a single operating system expose all of your signing material?",
helper:
- "Distinct Linux/Unix distributions — Ubuntu, Debian, Arch, NixOS, macOS, etc.",
+ "The concern is a compromise reaching every key at once, not uptime: an OS-level vulnerability, a poisoned package update, or a build-pipeline attack. Distinct Linux and Unix distributions count separately, so Ubuntu, Debian, Arch, NixOS, and macOS are each their own OS. Mixing them forces an attacker to break multiple independent supply chains to reach all of your signing material.",
options: [
{
color: "green",
- label: "3 or more",
- sub: "No single OS bug or bad update can stop your validator. e.g. Ubuntu + Arch + NixOS.",
- },
- {
- color: "yellow",
- label: "2 distinct distros",
- sub: "e.g. Ubuntu + macOS.",
+ label: "No: signing material is split across two or more distinct distros",
+ sub: "An OS-level compromise can only reach the share of your keys on that distro.",
},
{
color: "red",
- label: "1 — everything on one OS",
- sub: "e.g. 100% Ubuntu LTS.",
+ label: "Yes: all signing material runs on a single OS",
+ sub: "One OS-level vulnerability or poisoned update could reach every key you hold.",
},
],
+ risk: "Operating systems regularly ship critical remote-code-execution disclosures, and their package managers and build pipelines have repeatedly been targeted by supply-chain attacks. A single poisoned update to a popular distro could backdoor every validator running it — mixing distros forces an attacker to compromise multiple independent supply chains to reach you.",
+ references: [
+ { label: "VALOS: Hacking Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-hacking" },
+ { label: "VALOS: Supply-chain Malware", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-protect-against-malware" },
+ { label: "VALOS: Third-party Software Updates", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-update-software" },
+ ],
},
cpuDiversity: {
- q: "How many distinct CPU architectures run across those nodes?",
+ q: "Could a compromise of a single CPU architecture expose all of your signing material?",
helper:
- "The instruction set the chip speaks — x86-64, ARM64, RISC-V — not the chip model. RISC-V is barely available today, so a third architecture is frontier for almost everyone.",
+ "Count by instruction set, not chip model: x86-64, ARM64, and RISC-V are the architectures that matter. Side-channel and speculative-execution flaws like Spectre and Meltdown are bound to one architecture, so splitting your signing material across two limits how far any single flaw can reach. RISC-V is barely available today, so two is the practical ceiling for most operators.",
options: [
{
color: "green",
- label: "3 or more architectures",
- sub: "x86-64 + ARM64 + RISC-V — aspirational today.",
- },
- {
- color: "yellow",
- label: "2 architectures",
- sub: "typically x86-64 + ARM64.",
+ label: "No: signing material is split across two or more architectures",
+ sub: "Typically x86-64 plus ARM64 (Apple Silicon, AWS Graviton, Ampere), so a flaw in one architecture can only reach the keys running on it.",
},
{
color: "red",
- label: "1 architecture",
- sub: "typically 100% x86-64.",
+ label: "Yes: all signing material runs on a single architecture",
+ sub: "Typically 100% x86-64, where one CPU-level vulnerability could reach every key.",
},
],
+ risk: "CPU architectures have repeatedly disclosed speculative-execution and side-channel vulnerabilities — Spectre, Meltdown, and a long tail of successors — that let one process read memory belonging to another process on the same machine, including, in principle, signing keys held by a co-located component. A second architecture across your fleet limits the blast radius of any single hardware-class vulnerability.",
+ references: [
+ { label: "VALOS: Hacking Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-hacking" },
+ { label: "VALOS: Physically Distributed Infrastructure", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-distribute-hardware" },
+ ],
},
geoDiversity: {
- q: "What's the largest share of those nodes in a single country or legal jurisdiction?",
+ q: "What's the largest share of your nodes in a single country or region?",
helper:
- "Where the nodes physically run — grouped by country or jurisdiction, not data-centre.",
+ "Group the cooperating nodes that back your stake by the country or region where they physically run, not by data centre. In an active/active setup, a country-level outage like a grid blackout, weather event, or network disruption only drops the nodes in that region while the rest keep signing.",
options: [
{
color: "green",
- label: "Less than 33%",
- sub: "No single jurisdiction can take your validator offline or compel you to censor.",
+ label: "Less than ⅓",
+ sub: "No single region's outage can take your validator offline.",
},
{
color: "yellow",
- label: "33% to 66%",
- sub: "A large share of your nodes sit in one jurisdiction.",
+ label: "⅓ to ⅔",
+ sub: "One region going down would drop a large share of your nodes.",
},
{
color: "red",
- label: "More than 66%",
- sub: "One ruling or regional outage could take your validator offline.",
+ label: "More than ⅔",
+ sub: "One regional outage takes your validator offline.",
},
],
+ risk: "Entire countries lose grid power for hours or days — the 2025 Iberian Peninsula blackout cut electricity to tens of millions of people across two countries simultaneously. Natural disasters, accidental cascading failures, and sovereign actions all show up as common-mode risk for validators concentrated in one country or region.",
+ references: [
+ { label: "VALOS: Downtime Risk", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-risks-downtime" },
+ { label: "VALOS: Physically Distributed Infrastructure", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-distribute-hardware" },
+ { label: "VALOS: Environmental Threat Protection", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-mit-protect-from-environment" },
+ { label: "VALOS: Environmental Controls", url: "https://lidofinance.github.io/valos/valos-spec.html#sec-controls-environment" },
+ { label: "EIP-7716: Anti-correlation penalties", url: "https://eips.ethereum.org/EIPS/eip-7716" },
+ ],
},
};
diff --git a/lib/rubric/index.ts b/lib/rubric/index.ts
index afb1101..806a924 100644
--- a/lib/rubric/index.ts
+++ b/lib/rubric/index.ts
@@ -11,25 +11,25 @@ export const SLICES: SliceMeta[] = [
id: "keyCustody",
label: "Key Custody",
short: "Keys",
- why: "Concentrated signing material is a single point of failure — one compromise can sign with your stake.",
+ why: "Concentrated private keys are a single point of failure — one compromise and an attacker can sign to slash your stake.",
},
{
id: "clientDiversity",
label: "Client Diversity",
short: "Clients",
- why: "A supermajority-client bug can slash a single-client setup; refusing to attest on disagreement turns that into mere downtime.",
+ why: "A supermajority-client bug can cause you to lose 100% of funds; refusing to attest during a chain split turns that into mere downtime.",
},
{
id: "infraDiversity",
label: "Infrastructure",
short: "Infra",
- why: "One hosting provider's outage or compromise takes every validator hosted there with it.",
+ why: "One hosting provider's outage or compromise can take every validator hosted there with it.",
},
{
id: "osDiversity",
label: "OS Diversity",
short: "OS",
- why: "An OS monoculture is a supply-chain risk: one poisoned build pipeline can backdoor the whole fleet.",
+ why: "An OS monoculture is a supply-chain risk: one backdoor or zero-day could jeopardise a huge amount of validators.",
},
{
id: "cpuDiversity",
@@ -41,39 +41,39 @@ export const SLICES: SliceMeta[] = [
id: "geoDiversity",
label: "Geographic",
short: "Geo",
- why: "All validators in one jurisdiction can be hit by a single sovereign action, outage, or regulation.",
+ why: "A validator fully in one country or region can be impacted by a natural or man-made disaster.",
},
];
export const TIPS: Record = {
keyCustody: {
- red: "Split signing so no single party controls more than ⅔ — move to threshold signing and split your backup mnemonic across 2+ parties.",
+ red: "Split the private keys so no single entity custodies it in full — use distributed key generation or split your backup mnemonic across 2+ parties, and run the validator private keys across multi-node setups (Dirk, Web3Signer, Distributed Validators).",
yellow:
- "Distribute signing across 3+ independent parties (multi-operator DVT or distributed MPC), backups included, so no one party controls more than ⅓.",
+ "Distribute signing across 3+ independent parties (multi-operator DVT or distributed remote-signers), backups included, so no one party controls more than ⅓, and a failure of one won't introduce a liveness risk.",
},
clientDiversity: {
- red: "Run 3+ independent clients with a refuse-to-attest-on-disagreement setup (multi-operator DVT or a Vero-style multiplexer).",
+ red: "Run 3+ independent clients with a refuse-to-attest-on-disagreement setup (multi-operator DVT or a Vero/Vouch-style multiplexer).",
yellow:
"Add at least one minority client so your combined client share stays under ⅔ of the network.",
},
infraDiversity: {
red: "Move enough validators off your largest provider to get its share under ⅔.",
yellow:
- "Spread hosting so no provider holds more than ⅓ (add providers or self-host a portion).",
+ "Spread hosting so no provider holds more than ⅓ of your active/active setup(add providers or self-host a portion).",
},
osDiversity: {
- red: "Run a second OS distro across part of your fleet (e.g. add Debian or NixOS next to Ubuntu).",
+ red: "Run a second operating system distro across your multi-node validator (e.g. add Debian or NixOS next to Ubuntu) to ensure a compromised distro won't expose your private keys.",
yellow:
- "Run a third distinct distro so one distro's supply-chain compromise can't hit the whole fleet.",
+ "Run a third distinct distro so one distro's supply-chain compromise can't cause a liveness failure if enough partial keys have leaked to break your validator's consensus.",
},
cpuDiversity: {
- red: "Add ARM64 hardware (Apple Silicon, AWS Graviton, Ampere) next to your x86-64 nodes.",
+ red: "Run your validator across ARM64 hardware (Apple Silicon, AWS Graviton, Ampere) and x86-64 machines.",
yellow:
- "Add a third ISA (RISC-V) once viable — aspirational for nearly all operators today.",
+ "Add a third ISA (RISC-V) once viable — aspirational for nearly all setups today.",
},
geoDiversity: {
- red: "Move enough validators to a second country/region so no single one holds more than ⅔.",
- yellow: "Spread across regions so no single country/region holds more than ⅓.",
+ red: "Run your validator across a second country/region to reduce your risk exposure.",
+ yellow: "Run your validator across several regions so no single country/region runs more than ⅓ of machines and can cause a liveness outage.",
},
};
diff --git a/lib/share/og-meta.ts b/lib/share/og-meta.ts
index 0fc362b..fc6f0b4 100644
--- a/lib/share/og-meta.ts
+++ b/lib/share/og-meta.ts
@@ -13,7 +13,7 @@ export function shareOgMeta(answers: Answers) {
const stage = computeStage(answers);
if (stage == null) {
return {
- title: "Validator result",
+ title: "Validator Security Result",
description: SITE_FALLBACK_DESC,
};
}
@@ -25,9 +25,9 @@ export function shareOgMeta(answers: Answers) {
return {
title: `Stage ${stage} · ${STAGE_KIND[stage]}`,
- description: `${sliceLine}. Self-assessed validator resilience on Validator Beat.`,
+ description: `${sliceLine}. Self-assessed validator security on ValidatorBeat.com`,
};
}
const SITE_FALLBACK_DESC =
- "Six-slice validator resilience self-assessment — nothing stored.";
+ "Is your Ethereum validator secure and resilient? Take this self-assessment to find out.";
diff --git a/lib/share/pizza-og-svg.ts b/lib/share/pizza-og-svg.ts
index ee6af13..0a7e8c4 100644
--- a/lib/share/pizza-og-svg.ts
+++ b/lib/share/pizza-og-svg.ts
@@ -26,8 +26,8 @@ function wedgePath(cx: number, cy: number, r: number, a0: number, a1: number) {
const STAGE_LINE: Record = {
0: "Has a single point of failure — for now.",
- 1: "Safe from slashing — no single party can get it slashed.",
- 2: "Maximum resilience — can't be slashed, stopped, or censored.",
+ 1: "No single failure risks slashing.",
+ 2: "Can't be slashed or stopped by a single failure.",
};
const STAGE_KIND: Record = {
@@ -46,7 +46,11 @@ function escapeXml(s: string) {
return s.replace(/&/g, "&").replace(//g, ">");
}
-export function pizzaOgSvg(answers: Answers, code: string): string {
+export function pizzaOgSvg(
+ answers: Answers,
+ code: string,
+ displayHost: string = "validatorbeat.com",
+): string {
const stage = computeStage(answers);
const cx = 300;
const cy = 315;
@@ -77,7 +81,11 @@ export function pizzaOgSvg(answers: Answers, code: string): string {
Stage ${stage}${STAGE_KIND[stage].toUpperCase()}${escapeXml(STAGE_LINE[stage])}
- ${SLICES.map((s) => `${s.short} ${answers[s.id] ? answers[s.id]![0].toUpperCase() : "—"}`).join(" · ")}`;
+ ${SLICES.map((s) => {
+ const col = answers[s.id];
+ const dot = col ? PIZZA_FILL[col] : PIZZA_EMPTY_STROKE;
+ return `${escapeXml(s.short)} ●`;
+ }).join("· ")}`;
const hub =
stage != null
@@ -91,11 +99,11 @@ export function pizzaOgSvg(answers: Answers, code: string): string {
Validator Beat
- ${code}
+ ${escapeXml(displayHost)}/${code}
${wedges}
${hub}
${stageBlock}
- How resilient is your validator? Find out →
+ How resilient is your validator? Find out at ValidatorBeat.com→
`;
}
diff --git a/lib/theme/tokens.ts b/lib/theme/tokens.ts
index 082d34f..964c88b 100644
--- a/lib/theme/tokens.ts
+++ b/lib/theme/tokens.ts
@@ -4,16 +4,16 @@
*/
import type { SliceColor } from "@lib/rubric/types";
-export const THEME_BRAND = "#00a3ff";
+export const THEME_BRAND = "#16968e";
export const PIZZA_FILL: Record = {
- green: "#3d854d",
+ green: "#1f9b6b",
yellow: "#e7c300",
red: "#bb1122",
};
export const PIZZA_RING: Record = {
- green: "#8fd4a0",
+ green: "#82edcc",
yellow: "#ffe566",
red: "#e85c6f",
};
@@ -25,7 +25,7 @@ export const PIZZA_INK = "#000000";
export const CONFETTI_BRAND = [
THEME_BRAND,
- "#3369ff",
+ "#2fe4ab",
PIZZA_FILL.green,
PIZZA_FILL.yellow,
"#ffffff",
diff --git a/package.json b/package.json
index 135d9ef..696331e 100644
--- a/package.json
+++ b/package.json
@@ -2,12 +2,14 @@
"name": "validator-beat",
"version": "0.1.0",
"private": true,
+ "license": "Apache-2.0",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "next dev",
"build:og": "ts-node -r tsconfig-paths/register --compiler-options '{\"module\":\"CommonJS\"}' scripts/generate-og-images.ts",
+ "build:icons": "ts-node -r tsconfig-paths/register --compiler-options '{\"module\":\"CommonJS\"}' scripts/generate-icons.ts",
"build": "yarn build:og && next build",
"start": "next start",
"lint": "next lint",
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 826559b..8fd08be 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -9,11 +9,19 @@ const Body = styled("body", {
color: "var(--fg-1)",
});
+const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
+
export default function Document() {
return (
+
+
+
+
+
+
({
props: {
title: "Self-assessment",
description:
- "Six questions about your validator setup. Score your resilience in about a minute — nothing is submitted or stored.",
+ "Six questions about your Ethereum validator setup. Score your resilience in about a minute — nothing is submitted or stored.",
},
});
diff --git a/pages/methodology.tsx b/pages/methodology.tsx
index 41acf45..bf90ce4 100644
--- a/pages/methodology.tsx
+++ b/pages/methodology.tsx
@@ -38,11 +38,11 @@ export default function MethodologyPage() {
Stage 1 — Safety: No red slices, but not all green — no single
- party should be able to get you slashed.
+ failure should be able to expose you to slashing.
- Stage 2 — Liveness: All six slices green — no single failure should
- be able to slash you, stop you, or censor you.
+ Stage 2 — Liveness: All six slices green — no single point of
+ failure should be able to slash you, stop you, or censor you.
@@ -57,9 +57,83 @@ export default function MethodologyPage() {
))}
- For infrastructure, OS, CPU, and geography, questions assume each validator runs as a{" "}
- distributed validator (DVT) or behind a multiplexer —
- duties split across independent nodes so diversity translates into uptime.
+ For infrastructure, OS, CPU, and geography, diversity only translates into resilience
+ when your validator runs active/active: several cooperating nodes back
+ the same stake, with signing continuing as long as enough of them stay up. The stake
+ isn't partitioned across machines — it's one aggregate validator whose
+ cooperating machines you've diversified. Several setups achieve this —
+ multi-operator distributed validators (DVT) coordinated by Charon, or
+ validator clients like Vouch paired with multiplexers like{" "}
+ Vero and remote signers like Dirk or{" "}
+ Web3Signer. The common requirement: no single party holds enough key
+ material to sign alone — at least two independent parties involved, backups kept
+ separate, so compromising one doesn't leak the full private key.
+
+
+
+
+
Further reading
+
+
+
+ VALOS — Validator Operator Standards
+
+ {" "}— the canonical risk-and-mitigation catalogue for validator operators. Nearly
+ every risk surfaced in this assessment has a corresponding mitigation in VALOS.
+
+
+
+ clientdiversity.org
+
+ {" "}— live network share for each Ethereum consensus and execution client.
+
+ Ethereum's penalty math is already super-linear in correlation. Slashing penalties
+ scale with how many validators are slashed in the same window — an event that takes
+ out many validators at once costs each affected validator far more than an isolated
+ slashing would.{" "}
+
+ EIP-7716
+ {" "}
+ (“Anti-correlation attestation penalties”) proposes the same shape for
+ outages: more diversified entities get lower penalties, while entities with
+ high correlations in their setup face more severe ones. Spreading across the six
+ slices here isn't just defensive — it materially reduces the magnitude of any
+ single bad day, today for slashing and (under EIP-7716) tomorrow for downtime.
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..143d8a3
Binary files /dev/null and b/public/apple-touch-icon.png differ
diff --git a/public/favicon-16.png b/public/favicon-16.png
new file mode 100644
index 0000000..eb2940d
Binary files /dev/null and b/public/favicon-16.png differ
diff --git a/public/favicon-32.png b/public/favicon-32.png
new file mode 100644
index 0000000..2916c38
Binary files /dev/null and b/public/favicon-32.png differ
diff --git a/public/icon-192.png b/public/icon-192.png
new file mode 100644
index 0000000..d8907f0
Binary files /dev/null and b/public/icon-192.png differ
diff --git a/public/icon-512.png b/public/icon-512.png
new file mode 100644
index 0000000..1c8d4ea
Binary files /dev/null and b/public/icon-512.png differ
diff --git a/public/icon.svg b/public/icon.svg
new file mode 100644
index 0000000..5fe3b40
--- /dev/null
+++ b/public/icon.svg
@@ -0,0 +1,22 @@
+
+
diff --git a/public/site.webmanifest b/public/site.webmanifest
new file mode 100644
index 0000000..e7f0d79
--- /dev/null
+++ b/public/site.webmanifest
@@ -0,0 +1,12 @@
+{
+ "name": "Validator Beat",
+ "short_name": "Validator Beat",
+ "description": "Self-assess how resilient your Ethereum validator setup is.",
+ "icons": [
+ { "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
+ { "src": "icon-512.png", "sizes": "512x512", "type": "image/png" }
+ ],
+ "theme_color": "#16968E",
+ "background_color": "#f7f7f7",
+ "display": "standalone"
+}
diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts
new file mode 100644
index 0000000..956363e
--- /dev/null
+++ b/scripts/generate-icons.ts
@@ -0,0 +1,29 @@
+import fs from "fs";
+import path from "path";
+import sharp from "sharp";
+
+const PUBLIC_DIR = path.join(__dirname, "..", "public");
+const SOURCE_SVG = path.join(PUBLIC_DIR, "icon.svg");
+
+const SIZES: Array<{ name: string; size: number }> = [
+ { name: "favicon-16.png", size: 16 },
+ { name: "favicon-32.png", size: 32 },
+ { name: "apple-touch-icon.png", size: 180 },
+ { name: "icon-192.png", size: 192 },
+ { name: "icon-512.png", size: 512 },
+];
+
+async function main() {
+ const svg = fs.readFileSync(SOURCE_SVG);
+ for (const { name, size } of SIZES) {
+ const out = path.join(PUBLIC_DIR, name);
+ await sharp(svg).resize(size, size).png().toFile(out);
+ console.log(`Wrote ${name} (${size}x${size})`);
+ }
+ console.log(`Generated ${SIZES.length} icons from icon.svg`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/scripts/generate-og-images.ts b/scripts/generate-og-images.ts
index 1541577..12b4b29 100644
--- a/scripts/generate-og-images.ts
+++ b/scripts/generate-og-images.ts
@@ -7,15 +7,21 @@ import { allShareCodes } from "../lib/theme/share-codes";
const OUT_DIR = path.join(__dirname, "..", "public", "og");
+const SITE_URL =
+ process.env.NEXT_PUBLIC_SITE_URL ?? "https://validatorbeat.com";
+const DISPLAY_HOST = SITE_URL.replace(/^https?:\/\//, "").replace(/\/+$/, "");
+
async function main() {
fs.mkdirSync(OUT_DIR, { recursive: true });
const codes = allShareCodes();
let done = 0;
+ console.log(`OG images: using display host "${DISPLAY_HOST}"`);
+
for (const code of codes) {
const answers = decodeShareCode(code);
if (!answers) continue;
- const svg = pizzaOgSvg(answers, code);
+ const svg = pizzaOgSvg(answers, code, DISPLAY_HOST);
const out = path.join(OUT_DIR, `${code}.png`);
await sharp(Buffer.from(svg)).png().toFile(out);
done++;
diff --git a/styles/theme-tokens.css b/styles/theme-tokens.css
index 32ba56c..ae96599 100644
--- a/styles/theme-tokens.css
+++ b/styles/theme-tokens.css
@@ -1,18 +1,20 @@
/* =============================================================================
VALIDATOR BEAT — THEME (edit this file to change colors)
- Palette aligned with Lido Governance forum (research.lido.fi):
- light surfaces, #00A3FF accent, semantic success / warning / danger.
+ Light surfaces with semantic success / warning / danger. Brand accents draw
+ from the Obol palette (obol-ui DESIGN.md: obolDarkDark / obolGreen / obolGreenHover)
+ while staying readable on white backgrounds.
Only change the hex values in the block below. The rest of the app maps
these automatically (CSS variables + pizza SVG colors in lib/theme/tokens.ts).
============================================================================= */
:root {
- /* ---- Brand accent (links, primary buttons, wordmark highlight) ---- */
- --theme-brand: #00a3ff;
- --theme-brand-hover: #0088d4;
- --theme-brand-highlight: #3369ff;
+ /* ---- Brand accent (links, primary buttons, wordmark highlight) ----
+ Obol-palette greens, picked for contrast on white surfaces. */
+ --theme-brand: #16968e; /* obolDarkDark — readable on white */
+ --theme-brand-hover: #0f7c76; /* darker neighbour in Obol palette */
+ --theme-brand-highlight: #2fe4ab; /* obolGreen — decorative tints / confetti */
/* ---- Page surfaces ---- */
--theme-surface-page: #f7f7f7;
@@ -29,13 +31,15 @@
--theme-text-faint: #cccccc;
--theme-text-on-brand: #ffffff;
- /* ---- Pizza / risk slices (green = good, yellow = partial, red = SPOF) ---- */
- --theme-risk-green: #3d854d;
+ /* ---- Pizza / risk slices (green = good, yellow = partial, red = SPOF) ----
+ Stage-2 green nudged toward the Obol mint family while staying legible as
+ "green" alongside the yellow and red in the traffic-light triad. */
+ --theme-risk-green: #1f9b6b;
--theme-risk-yellow: #e7c300;
--theme-risk-red: #bb1122;
/* ---- Pizza ring tints (lighter than slice fill, for center progress ring) ---- */
- --theme-risk-green-ring: #8fd4a0;
+ --theme-risk-green-ring: #82edcc; /* obolGreenHover */
--theme-risk-yellow-ring: #ffe566;
--theme-risk-red-ring: #e85c6f;